From b06fc28aa32f477e1785cd998385fdb490bc5ebf Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sat, 29 Aug 2020 11:45:08 +0200 Subject: REST API: allow override of creation and update dates Note that if they're not provided, default behaviour will apply: creation and update dates will be autogenerated, and not empty. Fixes #1223 --- application/api/ApiUtils.php | 11 ++++++++++- application/api/controllers/ApiController.php | 3 ++- application/api/controllers/Links.php | 4 ++-- 3 files changed, 14 insertions(+), 4 deletions(-) (limited to 'application') diff --git a/application/api/ApiUtils.php b/application/api/ApiUtils.php index faebb8f5..4a6326f0 100644 --- a/application/api/ApiUtils.php +++ b/application/api/ApiUtils.php @@ -94,7 +94,7 @@ class ApiUtils * * @return Bookmark instance. */ - public static function buildLinkFromRequest($input, $defaultPrivate) + public static function buildBookmarkFromRequest($input, $defaultPrivate): Bookmark { $bookmark = new Bookmark(); $url = ! empty($input['url']) ? cleanup_url($input['url']) : ''; @@ -110,6 +110,15 @@ class ApiUtils $bookmark->setTags(! empty($input['tags']) ? $input['tags'] : []); $bookmark->setPrivate($private); + $created = \DateTime::createFromFormat(\DateTime::ATOM, $input['created'] ?? ''); + if ($created instanceof \DateTimeInterface) { + $bookmark->setCreated($created); + } + $updated = \DateTime::createFromFormat(\DateTime::ATOM, $input['updated'] ?? ''); + if ($updated instanceof \DateTimeInterface) { + $bookmark->setUpdated($updated); + } + return $bookmark; } diff --git a/application/api/controllers/ApiController.php b/application/api/controllers/ApiController.php index c4b3d0c3..88a845eb 100644 --- a/application/api/controllers/ApiController.php +++ b/application/api/controllers/ApiController.php @@ -4,6 +4,7 @@ namespace Shaarli\Api\Controllers; use Shaarli\Bookmark\BookmarkServiceInterface; use Shaarli\Config\ConfigManager; +use Shaarli\History; use Slim\Container; /** @@ -31,7 +32,7 @@ abstract class ApiController protected $bookmarkService; /** - * @var HistoryController + * @var History */ protected $history; diff --git a/application/api/controllers/Links.php b/application/api/controllers/Links.php index 29247950..778097fd 100644 --- a/application/api/controllers/Links.php +++ b/application/api/controllers/Links.php @@ -116,7 +116,7 @@ class Links extends ApiController public function postLink($request, $response) { $data = $request->getParsedBody(); - $bookmark = ApiUtils::buildLinkFromRequest($data, $this->conf->get('privacy.default_private_links')); + $bookmark = ApiUtils::buildBookmarkFromRequest($data, $this->conf->get('privacy.default_private_links')); // duplicate by URL, return 409 Conflict if (! empty($bookmark->getUrl()) && ! empty($dup = $this->bookmarkService->findByUrl($bookmark->getUrl())) @@ -155,7 +155,7 @@ class Links extends ApiController $index = index_url($this->ci['environment']); $data = $request->getParsedBody(); - $requestBookmark = ApiUtils::buildLinkFromRequest($data, $this->conf->get('privacy.default_private_links')); + $requestBookmark = ApiUtils::buildBookmarkFromRequest($data, $this->conf->get('privacy.default_private_links')); // duplicate URL on a different link, return 409 Conflict if (! empty($requestBookmark->getUrl()) && ! empty($dup = $this->bookmarkService->findByUrl($requestBookmark->getUrl())) -- cgit v1.2.3 From 2cd0509b503332b1989f06da45d569d4d2929be5 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Thu, 3 Sep 2020 17:46:26 +0200 Subject: Improve regex to extract HTML metadata (title, description, etc.) Also added a bunch of tests to cover more use cases. Fixes #1375 --- application/bookmark/LinkUtils.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'application') diff --git a/application/bookmark/LinkUtils.php b/application/bookmark/LinkUtils.php index 68914fca..03e1b82a 100644 --- a/application/bookmark/LinkUtils.php +++ b/application/bookmark/LinkUtils.php @@ -66,11 +66,13 @@ function html_extract_tag($tag, $html) { $propertiesKey = ['property', 'name', 'itemprop']; $properties = implode('|', $propertiesKey); + // We need a OR here to accept either 'property=og:noquote' or 'property="og:unrelated og:my-tag"' + $orCondition = '["\']?(?:og:)?'. $tag .'["\']?|["\'][^\'"]*?(?:og:)?' . $tag . '[^\'"]*?[\'"]'; // Try to retrieve OpenGraph image. - $ogRegex = '#]+(?:'. $properties .')=["\']?(?:og:)?'. $tag .'["\'\s][^>]*content=["\']?(.*?)["\'/>]#'; + $ogRegex = '#]+(?:'. $properties .')=(?:'. $orCondition .')[^>]*content=["\'](.*?)["\'].*?>#'; // If the attributes are not in the order property => content (e.g. Github) // New regex to keep this readable... more or less. - $ogRegexReverse = '#]+content=["\']([^"\']+)[^>]+(?:'. $properties .')=["\']?(?:og)?:'. $tag .'["\'\s/>]#'; + $ogRegexReverse = '#]+content=["\'](.*?)["\'][^>]+(?:'. $properties .')=(?:'. $orCondition .').*?>#'; if (preg_match($ogRegex, $html, $matches) > 0 || preg_match($ogRegexReverse, $html, $matches) > 0 -- cgit v1.2.3 From 8fabcd0224b1122a48b495326854bb3562cd2e9d Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Thu, 27 Aug 2020 15:25:18 +0200 Subject: Add Markdown Extra formatter Library: [Parsedown Extra](https://github.com/erusev/parsedown-extra) Also sort dependencies alphabetically. Fixes #1169 --- .../formatter/BookmarkMarkdownExtraFormatter.php | 24 ++++++++++++++++++++++ .../front/controller/admin/ConfigureController.php | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 application/formatter/BookmarkMarkdownExtraFormatter.php (limited to 'application') diff --git a/application/formatter/BookmarkMarkdownExtraFormatter.php b/application/formatter/BookmarkMarkdownExtraFormatter.php new file mode 100644 index 00000000..0694b23f --- /dev/null +++ b/application/formatter/BookmarkMarkdownExtraFormatter.php @@ -0,0 +1,24 @@ +parsedown = new \ParsedownExtra(); + } +} diff --git a/application/front/controller/admin/ConfigureController.php b/application/front/controller/admin/ConfigureController.php index e675fcca..0ed7ad81 100644 --- a/application/front/controller/admin/ConfigureController.php +++ b/application/front/controller/admin/ConfigureController.php @@ -30,7 +30,7 @@ class ConfigureController extends ShaarliAdminController 'theme_available', ThemeUtils::getThemes($this->container->conf->get('resource.raintpl_tpl')) ); - $this->assignView('formatter_available', ['default', 'markdown']); + $this->assignView('formatter_available', ['default', 'markdown', 'markdownExtra']); list($continents, $cities) = generateTimeZoneData( timezone_identifiers_list(), $this->container->conf->get('general.timezone') -- cgit v1.2.3 From fd1ddad98df45bc3c18be7980c1cbe68ce6b219c Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sat, 26 Sep 2020 14:18:01 +0200 Subject: Add mutex on datastore I/O operations To make sure that there is no concurrent operation on the datastore file. Fixes #1132 --- application/api/ApiMiddleware.php | 2 ++ application/bookmark/BookmarkFileService.php | 9 ++++-- application/bookmark/BookmarkIO.php | 35 +++++++++++++++++------ application/bookmark/BookmarkServiceInterface.php | 11 ------- application/container/ContainerBuilder.php | 2 ++ 5 files changed, 38 insertions(+), 21 deletions(-) (limited to 'application') diff --git a/application/api/ApiMiddleware.php b/application/api/ApiMiddleware.php index f5b53b01..adc8b266 100644 --- a/application/api/ApiMiddleware.php +++ b/application/api/ApiMiddleware.php @@ -1,6 +1,7 @@ container->get('history'), + new FlockMutex(fopen(SHAARLI_MUTEX_FILE, 'r'), 2), true ); $this->container['db'] = $linkDb; diff --git a/application/bookmark/BookmarkFileService.php b/application/bookmark/BookmarkFileService.php index c9ec2609..1ba00712 100644 --- a/application/bookmark/BookmarkFileService.php +++ b/application/bookmark/BookmarkFileService.php @@ -5,6 +5,7 @@ namespace Shaarli\Bookmark; use Exception; +use malkusch\lock\mutex\Mutex; use Shaarli\Bookmark\Exception\BookmarkNotFoundException; use Shaarli\Bookmark\Exception\DatastoreNotInitializedException; use Shaarli\Bookmark\Exception\EmptyDataStoreException; @@ -47,15 +48,19 @@ class BookmarkFileService implements BookmarkServiceInterface /** @var bool true for logged in users. Default value to retrieve private bookmarks. */ protected $isLoggedIn; + /** @var Mutex */ + protected $mutex; + /** * @inheritDoc */ - public function __construct(ConfigManager $conf, History $history, $isLoggedIn) + public function __construct(ConfigManager $conf, History $history, Mutex $mutex, $isLoggedIn) { $this->conf = $conf; $this->history = $history; + $this->mutex = $mutex; $this->pageCacheManager = new PageCacheManager($this->conf->get('resource.page_cache'), $isLoggedIn); - $this->bookmarksIO = new BookmarkIO($this->conf); + $this->bookmarksIO = new BookmarkIO($this->conf, $this->mutex); $this->isLoggedIn = $isLoggedIn; if (!$this->isLoggedIn && $this->conf->get('privacy.hide_public_links', false)) { diff --git a/application/bookmark/BookmarkIO.php b/application/bookmark/BookmarkIO.php index 6bf7f365..099653b0 100644 --- a/application/bookmark/BookmarkIO.php +++ b/application/bookmark/BookmarkIO.php @@ -2,6 +2,8 @@ namespace Shaarli\Bookmark; +use malkusch\lock\mutex\Mutex; +use malkusch\lock\mutex\NoMutex; use Shaarli\Bookmark\Exception\DatastoreNotInitializedException; use Shaarli\Bookmark\Exception\EmptyDataStoreException; use Shaarli\Bookmark\Exception\NotWritableDataStoreException; @@ -27,11 +29,14 @@ class BookmarkIO */ protected $conf; + + /** @var Mutex */ + protected $mutex; + /** * string Datastore PHP prefix */ protected static $phpPrefix = 'conf = $conf; $this->datastore = $conf->get('resource.datastore'); + $this->mutex = $mutex; } /** @@ -67,11 +77,16 @@ class BookmarkIO throw new NotWritableDataStoreException($this->datastore); } + $content = null; + $this->mutex->synchronized(function () use (&$content) { + $content = file_get_contents($this->datastore); + }); + // Note that gzinflate is faster than gzuncompress. // See: http://www.php.net/manual/en/function.gzdeflate.php#96439 $links = unserialize(gzinflate(base64_decode( - substr(file_get_contents($this->datastore), - strlen(self::$phpPrefix), -strlen(self::$phpSuffix))))); + substr($content, strlen(self::$phpPrefix), -strlen(self::$phpSuffix)) + ))); if (empty($links)) { if (filesize($this->datastore) > 100) { @@ -100,9 +115,13 @@ class BookmarkIO throw new NotWritableDataStoreException(dirname($this->datastore)); } - file_put_contents( - $this->datastore, - self::$phpPrefix.base64_encode(gzdeflate(serialize($links))).self::$phpSuffix - ); + $data = self::$phpPrefix.base64_encode(gzdeflate(serialize($links))).self::$phpSuffix; + + $this->mutex->synchronized(function () use ($data) { + file_put_contents( + $this->datastore, + $data + ); + }); } } diff --git a/application/bookmark/BookmarkServiceInterface.php b/application/bookmark/BookmarkServiceInterface.php index b9b483eb..638cfa5f 100644 --- a/application/bookmark/BookmarkServiceInterface.php +++ b/application/bookmark/BookmarkServiceInterface.php @@ -5,8 +5,6 @@ namespace Shaarli\Bookmark; use Shaarli\Bookmark\Exception\BookmarkNotFoundException; use Shaarli\Bookmark\Exception\NotWritableDataStoreException; -use Shaarli\Config\ConfigManager; -use Shaarli\History; /** * Class BookmarksService @@ -15,15 +13,6 @@ use Shaarli\History; */ interface BookmarkServiceInterface { - /** - * BookmarksService constructor. - * - * @param ConfigManager $conf instance - * @param History $history instance - * @param bool $isLoggedIn true if the current user is logged in - */ - public function __construct(ConfigManager $conf, History $history, $isLoggedIn); - /** * Find a bookmark by hash * diff --git a/application/container/ContainerBuilder.php b/application/container/ContainerBuilder.php index 55bb51b5..c21d58dd 100644 --- a/application/container/ContainerBuilder.php +++ b/application/container/ContainerBuilder.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Shaarli\Container; +use malkusch\lock\mutex\FlockMutex; use Shaarli\Bookmark\BookmarkFileService; use Shaarli\Bookmark\BookmarkServiceInterface; use Shaarli\Config\ConfigManager; @@ -84,6 +85,7 @@ class ContainerBuilder return new BookmarkFileService( $container->conf, $container->history, + new FlockMutex(fopen(SHAARLI_MUTEX_FILE, 'r'), 2), $container->loginManager->isLoggedIn() ); }; -- cgit v1.2.3 From efb7d21b52eb033530e80e5e49d175e6e3b031f4 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Fri, 2 Oct 2020 17:50:59 +0200 Subject: Add strict types for bookmarks management Parameters typing and using strict types overall increase the codebase quality by enforcing the a given parameter will have the expected type. It also removes the need to unnecessary unit tests checking methods behavior with invalid input. --- application/api/ApiUtils.php | 6 +- application/api/controllers/Links.php | 19 ++-- application/bookmark/Bookmark.php | 121 +++++++++++---------- application/bookmark/BookmarkArray.php | 14 ++- application/bookmark/BookmarkFileService.php | 66 +++++------ application/bookmark/BookmarkFilter.php | 47 ++++---- application/bookmark/BookmarkIO.php | 6 +- application/bookmark/BookmarkInitializer.php | 6 +- application/bookmark/BookmarkServiceInterface.php | 72 ++++++------ application/feed/FeedBuilder.php | 2 +- .../controller/admin/ThumbnailsController.php | 2 +- application/security/LoginManager.php | 2 +- 12 files changed, 187 insertions(+), 176 deletions(-) (limited to 'application') diff --git a/application/api/ApiUtils.php b/application/api/ApiUtils.php index 4a6326f0..eb1ca9bc 100644 --- a/application/api/ApiUtils.php +++ b/application/api/ApiUtils.php @@ -89,12 +89,12 @@ class ApiUtils * If no URL is provided, it will generate a local note URL. * If no title is provided, it will use the URL as title. * - * @param array $input Request Link. - * @param bool $defaultPrivate Request Link. + * @param array|null $input Request Link. + * @param bool $defaultPrivate Setting defined if a bookmark is private by default. * * @return Bookmark instance. */ - public static function buildBookmarkFromRequest($input, $defaultPrivate): Bookmark + public static function buildBookmarkFromRequest(?array $input, bool $defaultPrivate): Bookmark { $bookmark = new Bookmark(); $url = ! empty($input['url']) ? cleanup_url($input['url']) : ''; diff --git a/application/api/controllers/Links.php b/application/api/controllers/Links.php index 778097fd..73a1b84e 100644 --- a/application/api/controllers/Links.php +++ b/application/api/controllers/Links.php @@ -96,11 +96,12 @@ class Links extends ApiController */ public function getLink($request, $response, $args) { - if (!$this->bookmarkService->exists($args['id'])) { + $id = is_integer_mixed($args['id']) ? (int) $args['id'] : null; + if ($id === null || ! $this->bookmarkService->exists($id)) { throw new ApiLinkNotFoundException(); } $index = index_url($this->ci['environment']); - $out = ApiUtils::formatLink($this->bookmarkService->get($args['id']), $index); + $out = ApiUtils::formatLink($this->bookmarkService->get($id), $index); return $response->withJson($out, 200, $this->jsonStyle); } @@ -115,7 +116,7 @@ class Links extends ApiController */ public function postLink($request, $response) { - $data = $request->getParsedBody(); + $data = (array) ($request->getParsedBody() ?? []); $bookmark = ApiUtils::buildBookmarkFromRequest($data, $this->conf->get('privacy.default_private_links')); // duplicate by URL, return 409 Conflict if (! empty($bookmark->getUrl()) @@ -148,7 +149,8 @@ class Links extends ApiController */ public function putLink($request, $response, $args) { - if (! $this->bookmarkService->exists($args['id'])) { + $id = is_integer_mixed($args['id']) ? (int) $args['id'] : null; + if ($id === null || !$this->bookmarkService->exists($id)) { throw new ApiLinkNotFoundException(); } @@ -159,7 +161,7 @@ class Links extends ApiController // duplicate URL on a different link, return 409 Conflict if (! empty($requestBookmark->getUrl()) && ! empty($dup = $this->bookmarkService->findByUrl($requestBookmark->getUrl())) - && $dup->getId() != $args['id'] + && $dup->getId() != $id ) { return $response->withJson( ApiUtils::formatLink($dup, $index), @@ -168,7 +170,7 @@ class Links extends ApiController ); } - $responseBookmark = $this->bookmarkService->get($args['id']); + $responseBookmark = $this->bookmarkService->get($id); $responseBookmark = ApiUtils::updateLink($responseBookmark, $requestBookmark); $this->bookmarkService->set($responseBookmark); @@ -189,10 +191,11 @@ class Links extends ApiController */ public function deleteLink($request, $response, $args) { - if (! $this->bookmarkService->exists($args['id'])) { + $id = is_integer_mixed($args['id']) ? (int) $args['id'] : null; + if ($id === null || !$this->bookmarkService->exists($id)) { throw new ApiLinkNotFoundException(); } - $bookmark = $this->bookmarkService->get($args['id']); + $bookmark = $this->bookmarkService->get($id); $this->bookmarkService->remove($bookmark); return $response->withStatus(204); diff --git a/application/bookmark/Bookmark.php b/application/bookmark/Bookmark.php index 1beb8be2..fa45d2fc 100644 --- a/application/bookmark/Bookmark.php +++ b/application/bookmark/Bookmark.php @@ -1,5 +1,7 @@ id = $data['id']; - $this->shortUrl = $data['shorturl']; - $this->url = $data['url']; - $this->title = $data['title']; - $this->description = $data['description']; - $this->thumbnail = isset($data['thumbnail']) ? $data['thumbnail'] : null; - $this->sticky = isset($data['sticky']) ? $data['sticky'] : false; - $this->created = $data['created']; + $this->id = $data['id'] ?? null; + $this->shortUrl = $data['shorturl'] ?? null; + $this->url = $data['url'] ?? null; + $this->title = $data['title'] ?? null; + $this->description = $data['description'] ?? null; + $this->thumbnail = $data['thumbnail'] ?? null; + $this->sticky = $data['sticky'] ?? false; + $this->created = $data['created'] ?? null; if (is_array($data['tags'])) { $this->tags = $data['tags']; } else { - $this->tags = preg_split('/\s+/', $data['tags'], -1, PREG_SPLIT_NO_EMPTY); + $this->tags = preg_split('/\s+/', $data['tags'] ?? '', -1, PREG_SPLIT_NO_EMPTY); } if (! empty($data['updated'])) { $this->updated = $data['updated']; } - $this->private = $data['private'] ? true : false; + $this->private = ($data['private'] ?? false) ? true : false; return $this; } @@ -95,13 +97,12 @@ class Bookmark * * @throws InvalidBookmarkException */ - public function validate() + public function validate(): void { if ($this->id === null || ! is_int($this->id) || empty($this->shortUrl) || empty($this->created) - || ! $this->created instanceof DateTimeInterface ) { throw new InvalidBookmarkException($this); } @@ -119,11 +120,11 @@ class Bookmark * - created: with the current datetime * - shortUrl: with a generated small hash from the date and the given ID * - * @param int $id + * @param int|null $id * * @return Bookmark */ - public function setId($id) + public function setId(?int $id): Bookmark { $this->id = $id; if (empty($this->created)) { @@ -139,9 +140,9 @@ class Bookmark /** * Get the Id. * - * @return int + * @return int|null */ - public function getId() + public function getId(): ?int { return $this->id; } @@ -149,9 +150,9 @@ class Bookmark /** * Get the ShortUrl. * - * @return string + * @return string|null */ - public function getShortUrl() + public function getShortUrl(): ?string { return $this->shortUrl; } @@ -159,9 +160,9 @@ class Bookmark /** * Get the Url. * - * @return string + * @return string|null */ - public function getUrl() + public function getUrl(): ?string { return $this->url; } @@ -171,7 +172,7 @@ class Bookmark * * @return string */ - public function getTitle() + public function getTitle(): ?string { return $this->title; } @@ -181,7 +182,7 @@ class Bookmark * * @return string */ - public function getDescription() + public function getDescription(): string { return ! empty($this->description) ? $this->description : ''; } @@ -191,7 +192,7 @@ class Bookmark * * @return DateTimeInterface */ - public function getCreated() + public function getCreated(): ?DateTimeInterface { return $this->created; } @@ -201,7 +202,7 @@ class Bookmark * * @return DateTimeInterface */ - public function getUpdated() + public function getUpdated(): ?DateTimeInterface { return $this->updated; } @@ -209,11 +210,11 @@ class Bookmark /** * Set the ShortUrl. * - * @param string $shortUrl + * @param string|null $shortUrl * * @return Bookmark */ - public function setShortUrl($shortUrl) + public function setShortUrl(?string $shortUrl): Bookmark { $this->shortUrl = $shortUrl; @@ -223,14 +224,14 @@ class Bookmark /** * Set the Url. * - * @param string $url - * @param array $allowedProtocols + * @param string|null $url + * @param string[] $allowedProtocols * * @return Bookmark */ - public function setUrl($url, $allowedProtocols = []) + public function setUrl(?string $url, array $allowedProtocols = []): Bookmark { - $url = trim($url); + $url = $url !== null ? trim($url) : ''; if (! empty($url)) { $url = whitelist_protocols($url, $allowedProtocols); } @@ -242,13 +243,13 @@ class Bookmark /** * Set the Title. * - * @param string $title + * @param string|null $title * * @return Bookmark */ - public function setTitle($title) + public function setTitle(?string $title): Bookmark { - $this->title = trim($title); + $this->title = $title !== null ? trim($title) : ''; return $this; } @@ -256,11 +257,11 @@ class Bookmark /** * Set the Description. * - * @param string $description + * @param string|null $description * * @return Bookmark */ - public function setDescription($description) + public function setDescription(?string $description): Bookmark { $this->description = $description; @@ -271,11 +272,11 @@ class Bookmark * Set the Created. * Note: you shouldn't set this manually except for special cases (like bookmark import) * - * @param DateTimeInterface $created + * @param DateTimeInterface|null $created * * @return Bookmark */ - public function setCreated($created) + public function setCreated(?DateTimeInterface $created): Bookmark { $this->created = $created; @@ -285,11 +286,11 @@ class Bookmark /** * Set the Updated. * - * @param DateTimeInterface $updated + * @param DateTimeInterface|null $updated * * @return Bookmark */ - public function setUpdated($updated) + public function setUpdated(?DateTimeInterface $updated): Bookmark { $this->updated = $updated; @@ -301,7 +302,7 @@ class Bookmark * * @return bool */ - public function isPrivate() + public function isPrivate(): bool { return $this->private ? true : false; } @@ -309,11 +310,11 @@ class Bookmark /** * Set the Private. * - * @param bool $private + * @param bool|null $private * * @return Bookmark */ - public function setPrivate($private) + public function setPrivate(?bool $private): Bookmark { $this->private = $private ? true : false; @@ -323,9 +324,9 @@ class Bookmark /** * Get the Tags. * - * @return array + * @return string[] */ - public function getTags() + public function getTags(): array { return is_array($this->tags) ? $this->tags : []; } @@ -333,13 +334,13 @@ class Bookmark /** * Set the Tags. * - * @param array $tags + * @param string[]|null $tags * * @return Bookmark */ - public function setTags($tags) + public function setTags(?array $tags): Bookmark { - $this->setTagsString(implode(' ', $tags)); + $this->setTagsString(implode(' ', $tags ?? [])); return $this; } @@ -357,11 +358,11 @@ class Bookmark /** * Set the Thumbnail. * - * @param string|bool $thumbnail Thumbnail's URL - false if no thumbnail could be found + * @param string|bool|null $thumbnail Thumbnail's URL - false if no thumbnail could be found * * @return Bookmark */ - public function setThumbnail($thumbnail) + public function setThumbnail($thumbnail): Bookmark { $this->thumbnail = $thumbnail; @@ -373,7 +374,7 @@ class Bookmark * * @return bool */ - public function isSticky() + public function isSticky(): bool { return $this->sticky ? true : false; } @@ -381,11 +382,11 @@ class Bookmark /** * Set the Sticky. * - * @param bool $sticky + * @param bool|null $sticky * * @return Bookmark */ - public function setSticky($sticky) + public function setSticky(?bool $sticky): Bookmark { $this->sticky = $sticky ? true : false; @@ -395,7 +396,7 @@ class Bookmark /** * @return string Bookmark's tags as a string, separated by a space */ - public function getTagsString() + public function getTagsString(): string { return implode(' ', $this->getTags()); } @@ -403,7 +404,7 @@ class Bookmark /** * @return bool */ - public function isNote() + public function isNote(): bool { // We check empty value to get a valid result if the link has not been saved yet return empty($this->url) || startsWith($this->url, '/shaare/') || $this->url[0] === '?'; @@ -416,14 +417,14 @@ class Bookmark * - multiple spaces will be removed * - trailing dash in tags will be removed * - * @param string $tags + * @param string|null $tags * * @return $this */ - public function setTagsString($tags) + public function setTagsString(?string $tags): Bookmark { // Remove first '-' char in tags. - $tags = preg_replace('/(^| )\-/', '$1', $tags); + $tags = preg_replace('/(^| )\-/', '$1', $tags ?? ''); // Explode all tags separted by spaces or commas $tags = preg_split('/[\s,]+/', $tags); // Remove eventual empty values @@ -440,7 +441,7 @@ class Bookmark * @param string $fromTag * @param string $toTag */ - public function renameTag($fromTag, $toTag) + public function renameTag(string $fromTag, string $toTag): void { if (($pos = array_search($fromTag, $this->tags)) !== false) { $this->tags[$pos] = trim($toTag); @@ -452,7 +453,7 @@ class Bookmark * * @param string $tag */ - public function deleteTag($tag) + public function deleteTag(string $tag): void { if (($pos = array_search($tag, $this->tags)) !== false) { unset($this->tags[$pos]); diff --git a/application/bookmark/BookmarkArray.php b/application/bookmark/BookmarkArray.php index 3bd5eb20..67bb3b73 100644 --- a/application/bookmark/BookmarkArray.php +++ b/application/bookmark/BookmarkArray.php @@ -1,5 +1,7 @@ ids[$id])) { + if ($id !== null && isset($this->ids[$id])) { return $this->ids[$id]; } return null; @@ -205,7 +207,7 @@ class BookmarkArray implements \Iterator, \Countable, \ArrayAccess * * @return int next ID. */ - public function getNextId() + public function getNextId(): int { if (!empty($this->ids)) { return max(array_keys($this->ids)) + 1; @@ -214,11 +216,11 @@ class BookmarkArray implements \Iterator, \Countable, \ArrayAccess } /** - * @param $url + * @param string $url * * @return Bookmark|null */ - public function getByUrl($url) + public function getByUrl(string $url): ?Bookmark { if (! empty($url) && isset($this->urls[$url]) diff --git a/application/bookmark/BookmarkFileService.php b/application/bookmark/BookmarkFileService.php index 1ba00712..804b2520 100644 --- a/application/bookmark/BookmarkFileService.php +++ b/application/bookmark/BookmarkFileService.php @@ -1,9 +1,10 @@ conf = $conf; $this->history = $history; @@ -96,7 +97,7 @@ class BookmarkFileService implements BookmarkServiceInterface /** * @inheritDoc */ - public function findByHash($hash) + public function findByHash(string $hash): Bookmark { $bookmark = $this->bookmarkFilter->filter(BookmarkFilter::$FILTER_HASH, $hash); // PHP 7.3 introduced array_key_first() to avoid this hack @@ -111,7 +112,7 @@ class BookmarkFileService implements BookmarkServiceInterface /** * @inheritDoc */ - public function findByUrl($url) + public function findByUrl(string $url): ?Bookmark { return $this->bookmarks->getByUrl($url); } @@ -120,10 +121,10 @@ class BookmarkFileService implements BookmarkServiceInterface * @inheritDoc */ public function search( - $request = [], - $visibility = null, - $caseSensitive = false, - $untaggedOnly = false, + array $request = [], + string $visibility = null, + bool $caseSensitive = false, + bool $untaggedOnly = false, bool $ignoreSticky = false ) { if ($visibility === null) { @@ -131,8 +132,8 @@ class BookmarkFileService implements BookmarkServiceInterface } // Filter bookmark database according to parameters. - $searchtags = isset($request['searchtags']) ? $request['searchtags'] : ''; - $searchterm = isset($request['searchterm']) ? $request['searchterm'] : ''; + $searchTags = isset($request['searchtags']) ? $request['searchtags'] : ''; + $searchTerm = isset($request['searchterm']) ? $request['searchterm'] : ''; if ($ignoreSticky) { $this->bookmarks->reorder('DESC', true); @@ -140,7 +141,7 @@ class BookmarkFileService implements BookmarkServiceInterface return $this->bookmarkFilter->filter( BookmarkFilter::$FILTER_TAG | BookmarkFilter::$FILTER_TEXT, - [$searchtags, $searchterm], + [$searchTags, $searchTerm], $caseSensitive, $visibility, $untaggedOnly @@ -150,7 +151,7 @@ class BookmarkFileService implements BookmarkServiceInterface /** * @inheritDoc */ - public function get($id, $visibility = null) + public function get(int $id, string $visibility = null): Bookmark { if (! isset($this->bookmarks[$id])) { throw new BookmarkNotFoundException(); @@ -173,20 +174,17 @@ class BookmarkFileService implements BookmarkServiceInterface /** * @inheritDoc */ - public function set($bookmark, $save = true) + public function set(Bookmark $bookmark, bool $save = true): Bookmark { if (true !== $this->isLoggedIn) { throw new Exception(t('You\'re not authorized to alter the datastore')); } - if (! $bookmark instanceof Bookmark) { - throw new Exception(t('Provided data is invalid')); - } if (! isset($this->bookmarks[$bookmark->getId()])) { throw new BookmarkNotFoundException(); } $bookmark->validate(); - $bookmark->setUpdated(new \DateTime()); + $bookmark->setUpdated(new DateTime()); $this->bookmarks[$bookmark->getId()] = $bookmark; if ($save === true) { $this->save(); @@ -198,15 +196,12 @@ class BookmarkFileService implements BookmarkServiceInterface /** * @inheritDoc */ - public function add($bookmark, $save = true) + public function add(Bookmark $bookmark, bool $save = true): Bookmark { if (true !== $this->isLoggedIn) { throw new Exception(t('You\'re not authorized to alter the datastore')); } - if (! $bookmark instanceof Bookmark) { - throw new Exception(t('Provided data is invalid')); - } - if (! empty($bookmark->getId())) { + if (!empty($bookmark->getId())) { throw new Exception(t('This bookmarks already exists')); } $bookmark->setId($this->bookmarks->getNextId()); @@ -223,14 +218,11 @@ class BookmarkFileService implements BookmarkServiceInterface /** * @inheritDoc */ - public function addOrSet($bookmark, $save = true) + public function addOrSet(Bookmark $bookmark, bool $save = true): Bookmark { if (true !== $this->isLoggedIn) { throw new Exception(t('You\'re not authorized to alter the datastore')); } - if (! $bookmark instanceof Bookmark) { - throw new Exception('Provided data is invalid'); - } if ($bookmark->getId() === null) { return $this->add($bookmark, $save); } @@ -240,14 +232,11 @@ class BookmarkFileService implements BookmarkServiceInterface /** * @inheritDoc */ - public function remove($bookmark, $save = true) + public function remove(Bookmark $bookmark, bool $save = true): void { if (true !== $this->isLoggedIn) { throw new Exception(t('You\'re not authorized to alter the datastore')); } - if (! $bookmark instanceof Bookmark) { - throw new Exception(t('Provided data is invalid')); - } if (! isset($this->bookmarks[$bookmark->getId()])) { throw new BookmarkNotFoundException(); } @@ -262,7 +251,7 @@ class BookmarkFileService implements BookmarkServiceInterface /** * @inheritDoc */ - public function exists($id, $visibility = null) + public function exists(int $id, string $visibility = null): bool { if (! isset($this->bookmarks[$id])) { return false; @@ -285,7 +274,7 @@ class BookmarkFileService implements BookmarkServiceInterface /** * @inheritDoc */ - public function count($visibility = null) + public function count(string $visibility = null): int { return count($this->search([], $visibility)); } @@ -293,7 +282,7 @@ class BookmarkFileService implements BookmarkServiceInterface /** * @inheritDoc */ - public function save() + public function save(): void { if (true !== $this->isLoggedIn) { // TODO: raise an Exception instead @@ -308,7 +297,7 @@ class BookmarkFileService implements BookmarkServiceInterface /** * @inheritDoc */ - public function bookmarksCountPerTag($filteringTags = [], $visibility = null) + public function bookmarksCountPerTag(array $filteringTags = [], string $visibility = null): array { $bookmarks = $this->search(['searchtags' => $filteringTags], $visibility); $tags = []; @@ -344,13 +333,14 @@ class BookmarkFileService implements BookmarkServiceInterface $keys = array_keys($tags); $tmpTags = array_combine($keys, $keys); array_multisort($tags, SORT_DESC, $tmpTags, SORT_ASC, $tags); + return $tags; } /** * @inheritDoc */ - public function days() + public function days(): array { $bookmarkDays = []; foreach ($this->search() as $bookmark) { @@ -365,7 +355,7 @@ class BookmarkFileService implements BookmarkServiceInterface /** * @inheritDoc */ - public function filterDay($request) + public function filterDay(string $request) { $visibility = $this->isLoggedIn ? BookmarkFilter::$ALL : BookmarkFilter::$PUBLIC; @@ -375,7 +365,7 @@ class BookmarkFileService implements BookmarkServiceInterface /** * @inheritDoc */ - public function initialize() + public function initialize(): void { $initializer = new BookmarkInitializer($this); $initializer->initialize(); @@ -388,7 +378,7 @@ class BookmarkFileService implements BookmarkServiceInterface /** * Handles migration to the new database format (BookmarksArray). */ - protected function migrate() + protected function migrate(): void { $bookmarkDb = new LegacyLinkDB( $this->conf->get('resource.datastore'), diff --git a/application/bookmark/BookmarkFilter.php b/application/bookmark/BookmarkFilter.php index 6636bbfe..4232f114 100644 --- a/application/bookmark/BookmarkFilter.php +++ b/application/bookmark/BookmarkFilter.php @@ -1,5 +1,7 @@ bookmarks; @@ -151,11 +158,11 @@ class BookmarkFilter * * @param string $smallHash permalink hash. * - * @return array $filtered array containing permalink data. + * @return Bookmark[] $filtered array containing permalink data. * - * @throws \Shaarli\Bookmark\Exception\BookmarkNotFoundException if the smallhash doesn't match any link. + * @throws BookmarkNotFoundException if the smallhash doesn't match any link. */ - private function filterSmallHash($smallHash) + private function filterSmallHash(string $smallHash) { foreach ($this->bookmarks as $key => $l) { if ($smallHash == $l->getShortUrl()) { @@ -186,9 +193,9 @@ class BookmarkFilter * @param string $searchterms search query. * @param string $visibility Optional: return only all/private/public bookmarks. * - * @return array search results. + * @return Bookmark[] search results. */ - private function filterFulltext($searchterms, $visibility = 'all') + private function filterFulltext(string $searchterms, string $visibility = 'all') { if (empty($searchterms)) { return $this->noFilter($visibility); @@ -268,7 +275,7 @@ class BookmarkFilter * * @return string generated regex fragment */ - private static function tag2regex($tag) + private static function tag2regex(string $tag): string { $len = strlen($tag); if (!$len || $tag === "-" || $tag === "*") { @@ -314,13 +321,13 @@ class BookmarkFilter * You can specify one or more tags, separated by space or a comma, e.g. * print_r($mydb->filterTags('linux programming')); * - * @param string $tags list of tags separated by commas or blank spaces. - * @param bool $casesensitive ignore case if false. - * @param string $visibility Optional: return only all/private/public bookmarks. + * @param string|array $tags list of tags, separated by commas or blank spaces if passed as string. + * @param bool $casesensitive ignore case if false. + * @param string $visibility Optional: return only all/private/public bookmarks. * - * @return array filtered bookmarks. + * @return Bookmark[] filtered bookmarks. */ - public function filterTags($tags, $casesensitive = false, $visibility = 'all') + public function filterTags($tags, bool $casesensitive = false, string $visibility = 'all') { // get single tags (we may get passed an array, even though the docs say different) $inputTags = $tags; @@ -396,9 +403,9 @@ class BookmarkFilter * * @param string $visibility return only all/private/public bookmarks. * - * @return array filtered bookmarks. + * @return Bookmark[] filtered bookmarks. */ - public function filterUntagged($visibility) + public function filterUntagged(string $visibility) { $filtered = []; foreach ($this->bookmarks as $key => $link) { @@ -427,11 +434,11 @@ class BookmarkFilter * @param string $day day to filter. * @param string $visibility return only all/private/public bookmarks. - * @return array all link matching given day. + * @return Bookmark[] all link matching given day. * * @throws Exception if date format is invalid. */ - public function filterDay($day, $visibility) + public function filterDay(string $day, string $visibility) { if (!checkDateFormat('Ymd', $day)) { throw new Exception('Invalid date format'); @@ -460,9 +467,9 @@ class BookmarkFilter * @param string $tags string containing a list of tags. * @param bool $casesensitive will convert everything to lowercase if false. * - * @return array filtered tags string. + * @return string[] filtered tags string. */ - public static function tagsStrToArray($tags, $casesensitive) + public static function tagsStrToArray(string $tags, bool $casesensitive): array { // We use UTF-8 conversion to handle various graphemes (i.e. cyrillic, or greek) $tagsOut = $casesensitive ? $tags : mb_convert_case($tags, MB_CASE_LOWER, 'UTF-8'); diff --git a/application/bookmark/BookmarkIO.php b/application/bookmark/BookmarkIO.php index 099653b0..f40fa476 100644 --- a/application/bookmark/BookmarkIO.php +++ b/application/bookmark/BookmarkIO.php @@ -1,5 +1,7 @@ bookmarkService = $bookmarkService; } @@ -31,7 +33,7 @@ class BookmarkInitializer /** * Initialize the data store with default bookmarks */ - public function initialize() + public function initialize(): void { $bookmark = new Bookmark(); $bookmark->setTitle('quicksilver (loop) on Vimeo ' . t('(private bookmark with thumbnail demo)')); diff --git a/application/bookmark/BookmarkServiceInterface.php b/application/bookmark/BookmarkServiceInterface.php index 638cfa5f..37a54d03 100644 --- a/application/bookmark/BookmarkServiceInterface.php +++ b/application/bookmark/BookmarkServiceInterface.php @@ -1,7 +1,8 @@ bookmarksCount */ - public function bookmarksCountPerTag($filteringTags = [], $visibility = 'all'); + public function bookmarksCountPerTag(array $filteringTags = [], ?string $visibility = null): array; /** * Returns the list of days containing articles (oldest first) * * @return array containing days (in format YYYYMMDD). */ - public function days(); + public function days(): array; /** * Returns the list of articles for a given day. @@ -166,10 +170,10 @@ interface BookmarkServiceInterface * * @throws BookmarkNotFoundException */ - public function filterDay($request); + public function filterDay(string $request); /** * Creates the default database after a fresh install. */ - public function initialize(); + public function initialize(): void; } diff --git a/application/feed/FeedBuilder.php b/application/feed/FeedBuilder.php index f6def630..f70fce4f 100644 --- a/application/feed/FeedBuilder.php +++ b/application/feed/FeedBuilder.php @@ -102,7 +102,7 @@ class FeedBuilder } // Optionally filter the results: - $linksToDisplay = $this->linkDB->search($userInput, null, false, false, true); + $linksToDisplay = $this->linkDB->search($userInput ?? [], null, false, false, true); $nblinksToDisplay = $this->getNbLinks(count($linksToDisplay), $userInput); diff --git a/application/front/controller/admin/ThumbnailsController.php b/application/front/controller/admin/ThumbnailsController.php index 81c87ed0..4dc09d38 100644 --- a/application/front/controller/admin/ThumbnailsController.php +++ b/application/front/controller/admin/ThumbnailsController.php @@ -52,7 +52,7 @@ class ThumbnailsController extends ShaarliAdminController } try { - $bookmark = $this->container->bookmarkService->get($id); + $bookmark = $this->container->bookmarkService->get((int) $id); } catch (BookmarkNotFoundException $e) { return $response->withStatus(404); } diff --git a/application/security/LoginManager.php b/application/security/LoginManager.php index d74c3118..65048f10 100644 --- a/application/security/LoginManager.php +++ b/application/security/LoginManager.php @@ -118,7 +118,7 @@ class LoginManager * * @return true when the user is logged in, false otherwise */ - public function isLoggedIn() + public function isLoggedIn(): bool { if ($this->openShaarli) { return true; -- cgit v1.2.3 From 4cf3564d28dc8e4d08a3e64f09ad045ffbde97ae Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Fri, 25 Sep 2020 13:29:36 +0200 Subject: Add a setting to retrieve bookmark metadata asynchrounously - There is a new standalone script (metadata.js) which requests a new controller to get bookmark metadata and fill the form async - This feature is enabled with the new setting: general.enable_async_metadata (enabled by default) - general.retrieve_description is now enabled by default - A small rotating loader animation has a been added to bookmark inputs when metadata is being retrieved (default template) - Custom JS htmlentities has been removed and mathiasbynens/he library is used instead Fixes #1563 --- application/config/ConfigManager.php | 3 +- application/container/ContainerBuilder.php | 5 ++ application/container/ShaarliContainer.php | 2 + .../controller/admin/ManageShaareController.php | 36 ++++-------- .../front/controller/admin/MetadataController.php | 29 +++++++++ application/http/MetadataRetriever.php | 68 ++++++++++++++++++++++ 6 files changed, 118 insertions(+), 25 deletions(-) create mode 100644 application/front/controller/admin/MetadataController.php create mode 100644 application/http/MetadataRetriever.php (limited to 'application') diff --git a/application/config/ConfigManager.php b/application/config/ConfigManager.php index 4c98be30..fb085023 100644 --- a/application/config/ConfigManager.php +++ b/application/config/ConfigManager.php @@ -366,7 +366,8 @@ class ConfigManager $this->setEmpty('general.links_per_page', 20); $this->setEmpty('general.enabled_plugins', self::$DEFAULT_PLUGINS); $this->setEmpty('general.default_note_title', 'Note: '); - $this->setEmpty('general.retrieve_description', false); + $this->setEmpty('general.retrieve_description', true); + $this->setEmpty('general.enable_async_metadata', true); $this->setEmpty('updates.check_updates', false); $this->setEmpty('updates.check_updates_branch', 'stable'); diff --git a/application/container/ContainerBuilder.php b/application/container/ContainerBuilder.php index c21d58dd..fd94a1c3 100644 --- a/application/container/ContainerBuilder.php +++ b/application/container/ContainerBuilder.php @@ -14,6 +14,7 @@ use Shaarli\Front\Controller\Visitor\ErrorController; use Shaarli\Front\Controller\Visitor\ErrorNotFoundController; use Shaarli\History; use Shaarli\Http\HttpAccess; +use Shaarli\Http\MetadataRetriever; use Shaarli\Netscape\NetscapeBookmarkUtils; use Shaarli\Plugin\PluginManager; use Shaarli\Render\PageBuilder; @@ -90,6 +91,10 @@ class ContainerBuilder ); }; + $container['metadataRetriever'] = function (ShaarliContainer $container): MetadataRetriever { + return new MetadataRetriever($container->conf, $container->httpAccess); + }; + $container['pageBuilder'] = function (ShaarliContainer $container): PageBuilder { return new PageBuilder( $container->conf, diff --git a/application/container/ShaarliContainer.php b/application/container/ShaarliContainer.php index 66e669aa..3a7c238f 100644 --- a/application/container/ShaarliContainer.php +++ b/application/container/ShaarliContainer.php @@ -10,6 +10,7 @@ use Shaarli\Feed\FeedBuilder; use Shaarli\Formatter\FormatterFactory; use Shaarli\History; use Shaarli\Http\HttpAccess; +use Shaarli\Http\MetadataRetriever; use Shaarli\Netscape\NetscapeBookmarkUtils; use Shaarli\Plugin\PluginManager; use Shaarli\Render\PageBuilder; @@ -35,6 +36,7 @@ use Slim\Container; * @property History $history * @property HttpAccess $httpAccess * @property LoginManager $loginManager + * @property MetadataRetriever $metadataRetriever * @property NetscapeBookmarkUtils $netscapeBookmarkUtils * @property callable $notFoundHandler Overrides default Slim exception display * @property PageBuilder $pageBuilder diff --git a/application/front/controller/admin/ManageShaareController.php b/application/front/controller/admin/ManageShaareController.php index bb083486..df2f1631 100644 --- a/application/front/controller/admin/ManageShaareController.php +++ b/application/front/controller/admin/ManageShaareController.php @@ -53,36 +53,22 @@ class ManageShaareController extends ShaarliAdminController // If this is an HTTP(S) link, we try go get the page to extract // the title (otherwise we will to straight to the edit form.) - if (empty($title) && strpos(get_url_scheme($url) ?: '', 'http') !== false) { - $retrieveDescription = $this->container->conf->get('general.retrieve_description'); - // Short timeout to keep the application responsive - // The callback will fill $charset and $title with data from the downloaded page. - $this->container->httpAccess->getHttpResponse( - $url, - $this->container->conf->get('general.download_timeout', 30), - $this->container->conf->get('general.download_max_size', 4194304), - $this->container->httpAccess->getCurlDownloadCallback( - $charset, - $title, - $description, - $tags, - $retrieveDescription - ) - ); - if (! empty($title) && strtolower($charset) !== 'utf-8' && mb_check_encoding($charset)) { - $title = mb_convert_encoding($title, 'utf-8', $charset); - } + 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) && empty($title)) { - $title = $this->container->conf->get('general.default_note_title', t('Note: ')); + if (empty($url)) { + $metadata['title'] = $this->container->conf->get('general.default_note_title', t('Note: ')); } $link = [ - 'title' => $title, + 'title' => $title ?? $metadata['title'] ?? '', 'url' => $url ?? '', - 'description' => $description ?? '', - 'tags' => $tags ?? '', + 'description' => $description ?? $metadata['description'] ?? '', + 'tags' => $tags ?? $metadata['tags'] ?? '', 'private' => $private, ]; } else { @@ -352,6 +338,8 @@ class ManageShaareController extends ShaarliAdminController '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); diff --git a/application/front/controller/admin/MetadataController.php b/application/front/controller/admin/MetadataController.php new file mode 100644 index 00000000..ff845944 --- /dev/null +++ b/application/front/controller/admin/MetadataController.php @@ -0,0 +1,29 @@ +getParam('url'); + + // Only try to extract metadata from URL with HTTP(s) scheme + if (!empty($url) && strpos(get_url_scheme($url) ?: '', 'http') !== false) { + return $response->withJson($this->container->metadataRetriever->retrieve($url)); + } + + return $response->withJson([]); + } +} diff --git a/application/http/MetadataRetriever.php b/application/http/MetadataRetriever.php new file mode 100644 index 00000000..2ca982e2 --- /dev/null +++ b/application/http/MetadataRetriever.php @@ -0,0 +1,68 @@ +conf = $conf; + $this->httpAccess = $httpAccess; + } + + /** + * Retrieve metadata for given URL. + * + * @return array [ + * 'title' => , + * 'description' => , + * 'tags' => , + * ] + */ + public function retrieve(string $url): array + { + $charset = null; + $title = null; + $description = null; + $tags = null; + $retrieveDescription = $this->conf->get('general.retrieve_description'); + + // Short timeout to keep the application responsive + // The callback will fill $charset and $title with data from the downloaded page. + $this->httpAccess->getHttpResponse( + $url, + $this->conf->get('general.download_timeout', 30), + $this->conf->get('general.download_max_size', 4194304), + $this->httpAccess->getCurlDownloadCallback( + $charset, + $title, + $description, + $tags, + $retrieveDescription + ) + ); + + if (!empty($title) && strtolower($charset) !== 'utf-8') { + $title = mb_convert_encoding($title, 'utf-8', $charset); + } + + return [ + 'title' => $title, + 'description' => $description, + 'tags' => $tags, + ]; + } +} -- cgit v1.2.3 From 5334090be04e66da5cb5c3ad487604b3733c5cac Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Thu, 15 Oct 2020 11:20:33 +0200 Subject: Improve metadata retrieval (performances and accuracy) - Use dedicated function to download headers to avoid apply multiple regexps on headers - Also try to extract title from meta tags --- application/http/HttpAccess.php | 22 ++++-- application/http/HttpUtils.php | 123 ++++++++++++++++++++------------- application/http/MetadataRetriever.php | 1 + 3 files changed, 91 insertions(+), 55 deletions(-) (limited to 'application') diff --git a/application/http/HttpAccess.php b/application/http/HttpAccess.php index 81d9e076..646a5264 100644 --- a/application/http/HttpAccess.php +++ b/application/http/HttpAccess.php @@ -14,9 +14,14 @@ namespace Shaarli\Http; */ class HttpAccess { - public function getHttpResponse($url, $timeout = 30, $maxBytes = 4194304, $curlWriteFunction = null) - { - return get_http_response($url, $timeout, $maxBytes, $curlWriteFunction); + public function getHttpResponse( + $url, + $timeout = 30, + $maxBytes = 4194304, + $curlHeaderFunction = null, + $curlWriteFunction = null + ) { + return get_http_response($url, $timeout, $maxBytes, $curlHeaderFunction, $curlWriteFunction); } public function getCurlDownloadCallback( @@ -24,16 +29,19 @@ class HttpAccess &$title, &$description, &$keywords, - $retrieveDescription, - $curlGetInfo = 'curl_getinfo' + $retrieveDescription ) { return get_curl_download_callback( $charset, $title, $description, $keywords, - $retrieveDescription, - $curlGetInfo + $retrieveDescription ); } + + public function getCurlHeaderCallback(&$charset, $curlGetInfo = 'curl_getinfo') + { + return get_curl_header_callback($charset, $curlGetInfo); + } } diff --git a/application/http/HttpUtils.php b/application/http/HttpUtils.php index 9f414073..28c12969 100644 --- a/application/http/HttpUtils.php +++ b/application/http/HttpUtils.php @@ -6,12 +6,14 @@ use Shaarli\Http\Url; * GET an HTTP URL to retrieve its content * Uses the cURL library or a fallback method * - * @param string $url URL to get (http://...) - * @param int $timeout network timeout (in seconds) - * @param int $maxBytes maximum downloaded bytes (default: 4 MiB) - * @param callable|string $curlWriteFunction Optional callback called during the download (cURL CURLOPT_WRITEFUNCTION). - * Can be used to add download conditions on the - * headers (response code, content type, etc.). + * @param string $url URL to get (http://...) + * @param int $timeout network timeout (in seconds) + * @param int $maxBytes maximum downloaded bytes (default: 4 MiB) + * @param callable|string $curlHeaderFunction Optional callback called during the download of headers + * (CURLOPT_HEADERFUNCTION) + * @param callable|string $curlWriteFunction Optional callback called during the download (cURL CURLOPT_WRITEFUNCTION). + * Can be used to add download conditions on the + * headers (response code, content type, etc.). * * @return array HTTP response headers, downloaded content * @@ -35,8 +37,13 @@ use Shaarli\Http\Url; * @see http://stackoverflow.com/q/9183178 * @see http://stackoverflow.com/q/1462720 */ -function get_http_response($url, $timeout = 30, $maxBytes = 4194304, $curlWriteFunction = null) -{ +function get_http_response( + $url, + $timeout = 30, + $maxBytes = 4194304, + $curlHeaderFunction = null, + $curlWriteFunction = null +) { $urlObj = new Url($url); $cleanUrl = $urlObj->idnToAscii(); @@ -70,7 +77,8 @@ function get_http_response($url, $timeout = 30, $maxBytes = 4194304, $curlWriteF // General cURL settings curl_setopt($ch, CURLOPT_AUTOREFERER, true); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); - curl_setopt($ch, CURLOPT_HEADER, true); + // Default header download if the $curlHeaderFunction is not defined + curl_setopt($ch, CURLOPT_HEADER, !is_callable($curlHeaderFunction)); curl_setopt( $ch, CURLOPT_HTTPHEADER, @@ -81,25 +89,21 @@ function get_http_response($url, $timeout = 30, $maxBytes = 4194304, $curlWriteF curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); curl_setopt($ch, CURLOPT_USERAGENT, $userAgent); - if (is_callable($curlWriteFunction)) { - curl_setopt($ch, CURLOPT_WRITEFUNCTION, $curlWriteFunction); - } - // Max download size management curl_setopt($ch, CURLOPT_BUFFERSIZE, 1024*16); curl_setopt($ch, CURLOPT_NOPROGRESS, false); + if (is_callable($curlHeaderFunction)) { + curl_setopt($ch, CURLOPT_HEADERFUNCTION, $curlHeaderFunction); + } + if (is_callable($curlWriteFunction)) { + curl_setopt($ch, CURLOPT_WRITEFUNCTION, $curlWriteFunction); + } curl_setopt( $ch, CURLOPT_PROGRESSFUNCTION, - function ($arg0, $arg1, $arg2, $arg3, $arg4 = 0) use ($maxBytes) { - if (version_compare(phpversion(), '5.5', '<')) { - // PHP version lower than 5.5 - // Callback has 4 arguments - $downloaded = $arg1; - } else { - // Callback has 5 arguments - $downloaded = $arg2; - } + function ($arg0, $arg1, $arg2, $arg3, $arg4) use ($maxBytes) { + $downloaded = $arg2; + // Non-zero return stops downloading return ($downloaded > $maxBytes) ? 1 : 0; } @@ -489,6 +493,46 @@ function is_https($server) return ! empty($server['HTTPS']); } +/** + * Get cURL callback function for CURLOPT_WRITEFUNCTION + * + * @param string $charset to extract from the downloaded page (reference) + * @param string $curlGetInfo Optionally overrides curl_getinfo function + * + * @return Closure + */ +function get_curl_header_callback( + &$charset, + $curlGetInfo = 'curl_getinfo' +) { + $isRedirected = false; + + return function ($ch, $data) use ($curlGetInfo, &$charset, &$isRedirected) { + $responseCode = $curlGetInfo($ch, CURLINFO_RESPONSE_CODE); + $chunkLength = strlen($data); + if (!empty($responseCode) && in_array($responseCode, [301, 302])) { + $isRedirected = true; + return $chunkLength; + } + if (!empty($responseCode) && $responseCode !== 200) { + return false; + } + // After a redirection, the content type will keep the previous request value + // until it finds the next content-type header. + if (! $isRedirected || strpos(strtolower($data), 'content-type') !== false) { + $contentType = $curlGetInfo($ch, CURLINFO_CONTENT_TYPE); + } + if (!empty($contentType) && strpos($contentType, 'text/html') === false) { + return false; + } + if (!empty($contentType) && empty($charset)) { + $charset = header_extract_charset($contentType); + } + + return $chunkLength; + }; +} + /** * Get cURL callback function for CURLOPT_WRITEFUNCTION * @@ -506,10 +550,8 @@ function get_curl_download_callback( &$title, &$description, &$keywords, - $retrieveDescription, - $curlGetInfo = 'curl_getinfo' + $retrieveDescription ) { - $isRedirected = false; $currentChunk = 0; $foundChunk = null; @@ -524,37 +566,18 @@ function get_curl_download_callback( * * @return int|bool length of $data or false if we need to stop the download */ - return function (&$ch, $data) use ( + return function ($ch, $data) use ( $retrieveDescription, - $curlGetInfo, &$charset, &$title, &$description, &$keywords, - &$isRedirected, &$currentChunk, &$foundChunk ) { + $chunkLength = strlen($data); $currentChunk++; - $responseCode = $curlGetInfo($ch, CURLINFO_RESPONSE_CODE); - if (!empty($responseCode) && in_array($responseCode, [301, 302])) { - $isRedirected = true; - return strlen($data); - } - if (!empty($responseCode) && $responseCode !== 200) { - return false; - } - // After a redirection, the content type will keep the previous request value - // until it finds the next content-type header. - if (! $isRedirected || strpos(strtolower($data), 'content-type') !== false) { - $contentType = $curlGetInfo($ch, CURLINFO_CONTENT_TYPE); - } - if (!empty($contentType) && strpos($contentType, 'text/html') === false) { - return false; - } - if (!empty($contentType) && empty($charset)) { - $charset = header_extract_charset($contentType); - } + if (empty($charset)) { $charset = html_extract_charset($data); } @@ -562,6 +585,10 @@ function get_curl_download_callback( $title = html_extract_title($data); $foundChunk = ! empty($title) ? $currentChunk : $foundChunk; } + if (empty($title)) { + $title = html_extract_tag('title', $data); + $foundChunk = ! empty($title) ? $currentChunk : $foundChunk; + } if ($retrieveDescription && empty($description)) { $description = html_extract_tag('description', $data); $foundChunk = ! empty($description) ? $currentChunk : $foundChunk; @@ -591,6 +618,6 @@ function get_curl_download_callback( return false; } - return strlen($data); + return $chunkLength; }; } diff --git a/application/http/MetadataRetriever.php b/application/http/MetadataRetriever.php index 2ca982e2..ba9bd40c 100644 --- a/application/http/MetadataRetriever.php +++ b/application/http/MetadataRetriever.php @@ -46,6 +46,7 @@ class MetadataRetriever $url, $this->conf->get('general.download_timeout', 30), $this->conf->get('general.download_max_size', 4194304), + $this->httpAccess->getCurlHeaderCallback($charset), $this->httpAccess->getCurlDownloadCallback( $charset, $title, -- cgit v1.2.3 From 4b3aca66238f4ec31ab67c990fd388738e959289 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Fri, 16 Oct 2020 12:04:46 +0200 Subject: Strict types: fix an issue in daily where the date could be an int --- application/bookmark/BookmarkFileService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'application') diff --git a/application/bookmark/BookmarkFileService.php b/application/bookmark/BookmarkFileService.php index 804b2520..eb7899bf 100644 --- a/application/bookmark/BookmarkFileService.php +++ b/application/bookmark/BookmarkFileService.php @@ -349,7 +349,7 @@ class BookmarkFileService implements BookmarkServiceInterface $bookmarkDays = array_keys($bookmarkDays); sort($bookmarkDays); - return $bookmarkDays; + return array_map('strval', $bookmarkDays); } /** -- cgit v1.2.3 From 7f5250421be4832b9679d8140bc4a71c8005dfa3 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Fri, 16 Oct 2020 12:47:11 +0200 Subject: Support using Shaarli without URL rewriting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Shaarli can be fully used by prefixing any URL with /index.php/ - {$base_path} used in templates already works with this configuration - Assets path (outside of theme's assets) must be prefixed with {$root_url}/ - Documentation section in « Server configuration » Fixes #1590 --- application/render/PageBuilder.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'application') diff --git a/application/render/PageBuilder.php b/application/render/PageBuilder.php index 41b357dd..2d6d2dbe 100644 --- a/application/render/PageBuilder.php +++ b/application/render/PageBuilder.php @@ -174,10 +174,12 @@ class PageBuilder } } + $rootPath = preg_replace('#/index\.php$#', '', $basePath); $this->assign('base_path', $basePath); + $this->assign('root_path', $rootPath); $this->assign( 'asset_path', - $basePath . '/' . + $rootPath . '/' . rtrim($this->conf->get('resource.raintpl_tpl', 'tpl'), '/') . '/' . $this->conf->get('resource.theme', 'default') ); -- cgit v1.2.3 From 3adbdc2a83e6b77a4ca62094c5d857524e39d211 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Fri, 16 Oct 2020 13:06:06 +0200 Subject: Inject ROOT_PATH in plugin instead of regenerating it everywhere --- application/front/controller/visitor/ShaarliVisitorController.php | 1 + application/plugin/PluginManager.php | 1 + 2 files changed, 2 insertions(+) (limited to 'application') diff --git a/application/front/controller/visitor/ShaarliVisitorController.php b/application/front/controller/visitor/ShaarliVisitorController.php index 55c075a2..54f9fe03 100644 --- a/application/front/controller/visitor/ShaarliVisitorController.php +++ b/application/front/controller/visitor/ShaarliVisitorController.php @@ -106,6 +106,7 @@ abstract class ShaarliVisitorController 'target' => $template, 'loggedin' => $this->container->loginManager->isLoggedIn(), 'basePath' => $this->container->basePath, + 'rootPath' => preg_replace('#/index\.php$#', '', $this->container->basePath), 'bookmarkService' => $this->container->bookmarkService ]; } diff --git a/application/plugin/PluginManager.php b/application/plugin/PluginManager.php index 1b2197c9..da66dea3 100644 --- a/application/plugin/PluginManager.php +++ b/application/plugin/PluginManager.php @@ -104,6 +104,7 @@ class PluginManager 'target' => '_PAGE_', 'loggedin' => '_LOGGEDIN_', 'basePath' => '_BASE_PATH_', + 'rootPath' => '_ROOT_PATH_', 'bookmarkService' => '_BOOKMARK_SERVICE_', ]; -- cgit v1.2.3 From 4e3875c0ce7f3b17e3d358dc5ecb1f8bed64546b Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Mon, 12 Oct 2020 11:35:55 +0200 Subject: Feature: highlight fulltext search results How it works: 1. when a fulltext search is made, Shaarli looks for the first occurence position of every term matching the search. No change here, but we store these positions in an array, in Bookmark's additionalContent. 2. when formatting bookmarks (through BookmarkFormatter implementation): 1. first we insert specific tokens at every search result positions 2. we format the content (escape HTML, apply markdown, etc.) 3. as a last step, we replace our token with displayable span elements Cons: this tightens coupling between search filters and formatters Pros: it was absolutely necessary not to perform the search twice. this solution has close to no impact on performances. Fixes #205 --- application/bookmark/Bookmark.php | 46 +++++++ application/bookmark/BookmarkFilter.php | 111 ++++++++++++++--- application/formatter/BookmarkDefaultFormatter.php | 132 ++++++++++++++++++++- application/formatter/BookmarkFormatter.php | 79 ++++++++++-- .../formatter/BookmarkMarkdownFormatter.php | 6 +- 5 files changed, 342 insertions(+), 32 deletions(-) (limited to 'application') diff --git a/application/bookmark/Bookmark.php b/application/bookmark/Bookmark.php index fa45d2fc..ea565d1f 100644 --- a/application/bookmark/Bookmark.php +++ b/application/bookmark/Bookmark.php @@ -54,6 +54,9 @@ class Bookmark /** @var bool True if the bookmark can only be seen while logged in */ protected $private; + /** @var mixed[] Available to store any additional content for a bookmark. Currently used for search highlight. */ + protected $additionalContent = []; + /** * Initialize a link from array data. Especially useful to create a Bookmark from former link storage format. * @@ -95,6 +98,8 @@ class Bookmark * - the URL with the permalink * - the title with the URL * + * Also make sure that we do not save search highlights in the datastore. + * * @throws InvalidBookmarkException */ public function validate(): void @@ -112,6 +117,9 @@ class Bookmark if (empty($this->title)) { $this->title = $this->url; } + if (array_key_exists('search_highlight', $this->additionalContent)) { + unset($this->additionalContent['search_highlight']); + } } /** @@ -435,6 +443,44 @@ class Bookmark return $this; } + /** + * Get entire additionalContent array. + * + * @return mixed[] + */ + public function getAdditionalContent(): array + { + return $this->additionalContent; + } + + /** + * Set a single entry in additionalContent, by key. + * + * @param string $key + * @param mixed|null $value Any type of value can be set. + * + * @return $this + */ + public function addAdditionalContentEntry(string $key, $value): self + { + $this->additionalContent[$key] = $value; + + return $this; + } + + /** + * Get a single entry in additionalContent, by key. + * + * @param string $key + * @param mixed|null $default + * + * @return mixed|null can be any type or even null. + */ + public function getAdditionalContentEntry(string $key, $default = null) + { + return array_key_exists($key, $this->additionalContent) ? $this->additionalContent[$key] : $default; + } + /** * Rename a tag in tags list. * diff --git a/application/bookmark/BookmarkFilter.php b/application/bookmark/BookmarkFilter.php index 4232f114..c79386ea 100644 --- a/application/bookmark/BookmarkFilter.php +++ b/application/bookmark/BookmarkFilter.php @@ -201,7 +201,7 @@ class BookmarkFilter return $this->noFilter($visibility); } - $filtered = array(); + $filtered = []; $search = mb_convert_case(html_entity_decode($searchterms), MB_CASE_LOWER, 'UTF-8'); $exactRegex = '/"([^"]+)"/'; // Retrieve exact search terms. @@ -213,8 +213,8 @@ class BookmarkFilter $explodedSearchAnd = array_values(array_filter($explodedSearchAnd)); // Filter excluding terms and update andSearch. - $excludeSearch = array(); - $andSearch = array(); + $excludeSearch = []; + $andSearch = []; foreach ($explodedSearchAnd as $needle) { if ($needle[0] == '-' && strlen($needle) > 1) { $excludeSearch[] = substr($needle, 1); @@ -234,33 +234,38 @@ class BookmarkFilter } } - // Concatenate link fields to search across fields. - // Adds a '\' separator for exact search terms. - $content = mb_convert_case($link->getTitle(), MB_CASE_LOWER, 'UTF-8') .'\\'; - $content .= mb_convert_case($link->getDescription(), MB_CASE_LOWER, 'UTF-8') .'\\'; - $content .= mb_convert_case($link->getUrl(), MB_CASE_LOWER, 'UTF-8') .'\\'; - $content .= mb_convert_case($link->getTagsString(), MB_CASE_LOWER, 'UTF-8') .'\\'; + $lengths = []; + $content = $this->buildFullTextSearchableLink($link, $lengths); // Be optimistic $found = true; + $foundPositions = []; // First, we look for exact term search - for ($i = 0; $i < count($exactSearch) && $found; $i++) { - $found = strpos($content, $exactSearch[$i]) !== false; - } - - // Iterate over keywords, if keyword is not found, + // Then iterate over keywords, if keyword is not found, // no need to check for the others. We want all or nothing. - for ($i = 0; $i < count($andSearch) && $found; $i++) { - $found = strpos($content, $andSearch[$i]) !== false; + foreach ([$exactSearch, $andSearch] as $search) { + for ($i = 0; $i < count($search) && $found !== false; $i++) { + $found = mb_strpos($content, $search[$i]); + if ($found === false) { + break; + } + + $foundPositions[] = ['start' => $found, 'end' => $found + mb_strlen($search[$i])]; + } } // Exclude terms. - for ($i = 0; $i < count($excludeSearch) && $found; $i++) { + for ($i = 0; $i < count($excludeSearch) && $found !== false; $i++) { $found = strpos($content, $excludeSearch[$i]) === false; } - if ($found) { + if ($found !== false) { + $link->addAdditionalContentEntry( + 'search_highlight', + $this->postProcessFoundPositions($lengths, $foundPositions) + ); + $filtered[$id] = $link; } } @@ -477,4 +482,74 @@ class BookmarkFilter return preg_split('/\s+/', $tagsOut, -1, PREG_SPLIT_NO_EMPTY); } + + /** + * This method finalize the content of the foundPositions array, + * by associated all search results to their associated bookmark field, + * making sure that there is no overlapping results, etc. + * + * @param array $fieldLengths Start and end positions of every bookmark fields in the aggregated bookmark content. + * @param array $foundPositions Positions where the search results were found in the aggregated content. + * + * @return array Updated $foundPositions, by bookmark field. + */ + protected function postProcessFoundPositions(array $fieldLengths, array $foundPositions): array + { + // Sort results by starting position ASC. + usort($foundPositions, function (array $entryA, array $entryB): int { + return $entryA['start'] > $entryB['start'] ? 1 : -1; + }); + + $out = []; + $currentMax = -1; + foreach ($foundPositions as $foundPosition) { + // we do not allow overlapping highlights + if ($foundPosition['start'] < $currentMax) { + continue; + } + + $currentMax = $foundPosition['end']; + foreach ($fieldLengths as $part => $length) { + if ($foundPosition['start'] < $length['start'] || $foundPosition['start'] > $length['end']) { + continue; + } + + $out[$part][] = [ + 'start' => $foundPosition['start'] - $length['start'], + 'end' => $foundPosition['end'] - $length['start'], + ]; + break; + } + } + + return $out; + } + + /** + * Concatenate link fields to search across fields. Adds a '\' separator for exact search terms. + * Also populate $length array with starting and ending positions of every bookmark field + * inside concatenated content. + * + * @param Bookmark $link + * @param array $lengths (by reference) + * + * @return string Lowercase concatenated fields content. + */ + protected function buildFullTextSearchableLink(Bookmark $link, array &$lengths): string + { + $content = mb_convert_case($link->getTitle(), MB_CASE_LOWER, 'UTF-8') .'\\'; + $content .= mb_convert_case($link->getDescription(), MB_CASE_LOWER, 'UTF-8') .'\\'; + $content .= mb_convert_case($link->getUrl(), MB_CASE_LOWER, 'UTF-8') .'\\'; + $content .= mb_convert_case($link->getTagsString(), MB_CASE_LOWER, 'UTF-8') .'\\'; + + $lengths['title'] = ['start' => 0, 'end' => mb_strlen($link->getTitle())]; + $nextField = $lengths['title']['end'] + 1; + $lengths['description'] = ['start' => $nextField, 'end' => $nextField + mb_strlen($link->getDescription())]; + $nextField = $lengths['description']['end'] + 1; + $lengths['url'] = ['start' => $nextField, 'end' => $nextField + mb_strlen($link->getUrl())]; + $nextField = $lengths['url']['end'] + 1; + $lengths['tags'] = ['start' => $nextField, 'end' => $nextField + mb_strlen($link->getTagsString())]; + + return $content; + } } diff --git a/application/formatter/BookmarkDefaultFormatter.php b/application/formatter/BookmarkDefaultFormatter.php index 9d4a0fa0..d58a5e39 100644 --- a/application/formatter/BookmarkDefaultFormatter.php +++ b/application/formatter/BookmarkDefaultFormatter.php @@ -12,10 +12,13 @@ namespace Shaarli\Formatter; */ class BookmarkDefaultFormatter extends BookmarkFormatter { + const SEARCH_HIGHLIGHT_OPEN = '|@@HIGHLIGHT'; + const SEARCH_HIGHLIGHT_CLOSE = 'HIGHLIGHT@@|'; + /** * @inheritdoc */ - public function formatTitle($bookmark) + protected function formatTitle($bookmark) { return escape($bookmark->getTitle()); } @@ -23,10 +26,28 @@ class BookmarkDefaultFormatter extends BookmarkFormatter /** * @inheritdoc */ - public function formatDescription($bookmark) + protected function formatTitleHtml($bookmark) + { + $title = $this->tokenizeSearchHighlightField( + $bookmark->getTitle() ?? '', + $bookmark->getAdditionalContentEntry('search_highlight')['title'] ?? [] + ); + + return $this->replaceTokens(escape($title)); + } + + /** + * @inheritdoc + */ + protected function formatDescription($bookmark) { $indexUrl = ! empty($this->contextData['index_url']) ? $this->contextData['index_url'] : ''; - return format_description(escape($bookmark->getDescription()), $indexUrl); + $description = $this->tokenizeSearchHighlightField( + $bookmark->getDescription() ?? '', + $bookmark->getAdditionalContentEntry('search_highlight')['description'] ?? [] + ); + + return $this->replaceTokens(format_description(escape($description), $indexUrl)); } /** @@ -40,7 +61,27 @@ class BookmarkDefaultFormatter extends BookmarkFormatter /** * @inheritdoc */ - public function formatTagString($bookmark) + protected function formatTagListHtml($bookmark) + { + if (empty($bookmark->getAdditionalContentEntry('search_highlight')['tags'])) { + return $this->formatTagList($bookmark); + } + + $tags = $this->tokenizeSearchHighlightField( + $bookmark->getTagsString(), + $bookmark->getAdditionalContentEntry('search_highlight')['tags'] + ); + $tags = $this->filterTagList(explode(' ', $tags)); + $tags = escape($tags); + $tags = $this->replaceTokensArray($tags); + + return $tags; + } + + /** + * @inheritdoc + */ + protected function formatTagString($bookmark) { return implode(' ', $this->formatTagList($bookmark)); } @@ -48,7 +89,7 @@ class BookmarkDefaultFormatter extends BookmarkFormatter /** * @inheritdoc */ - public function formatUrl($bookmark) + protected function formatUrl($bookmark) { if ($bookmark->isNote() && isset($this->contextData['index_url'])) { return rtrim($this->contextData['index_url'], '/') . '/' . escape(ltrim($bookmark->getUrl(), '/')); @@ -77,6 +118,19 @@ class BookmarkDefaultFormatter extends BookmarkFormatter return escape($bookmark->getUrl()); } + /** + * @inheritdoc + */ + protected function formatUrlHtml($bookmark) + { + $url = $this->tokenizeSearchHighlightField( + $bookmark->getUrl() ?? '', + $bookmark->getAdditionalContentEntry('search_highlight')['url'] ?? [] + ); + + return $this->replaceTokens(escape($url)); + } + /** * @inheritdoc */ @@ -84,4 +138,72 @@ class BookmarkDefaultFormatter extends BookmarkFormatter { return escape($bookmark->getThumbnail()); } + + /** + * Insert search highlight token in provided field content based on a list of search result positions + * + * @param string $fieldContent + * @param array|null $positions List of of search results with 'start' and 'end' positions. + * + * @return string Updated $fieldContent. + */ + protected function tokenizeSearchHighlightField(string $fieldContent, ?array $positions): string + { + if (empty($positions)) { + return $fieldContent; + } + + $insertedTokens = 0; + $tokenLength = strlen(static::SEARCH_HIGHLIGHT_OPEN); + foreach ($positions as $position) { + $position = [ + 'start' => $position['start'] + ($insertedTokens * $tokenLength), + 'end' => $position['end'] + ($insertedTokens * $tokenLength), + ]; + + $content = mb_substr($fieldContent, 0, $position['start']); + $content .= static::SEARCH_HIGHLIGHT_OPEN; + $content .= mb_substr($fieldContent, $position['start'], $position['end'] - $position['start']); + $content .= static::SEARCH_HIGHLIGHT_CLOSE; + $content .= mb_substr($fieldContent, $position['end']); + + $fieldContent = $content; + + $insertedTokens += 2; + } + + return $fieldContent; + } + + /** + * Replace search highlight tokens with HTML highlighted span. + * + * @param string $fieldContent + * + * @return string updated content. + */ + protected function replaceTokens(string $fieldContent): string + { + return str_replace( + [static::SEARCH_HIGHLIGHT_OPEN, static::SEARCH_HIGHLIGHT_CLOSE], + ['', ''], + $fieldContent + ); + } + + /** + * Apply replaceTokens to an array of content strings. + * + * @param string[] $fieldContents + * + * @return array + */ + protected function replaceTokensArray(array $fieldContents): array + { + foreach ($fieldContents as &$entry) { + $entry = $this->replaceTokens($entry); + } + + return $fieldContents; + } } diff --git a/application/formatter/BookmarkFormatter.php b/application/formatter/BookmarkFormatter.php index 0042dafe..e1b7f705 100644 --- a/application/formatter/BookmarkFormatter.php +++ b/application/formatter/BookmarkFormatter.php @@ -2,7 +2,7 @@ namespace Shaarli\Formatter; -use DateTime; +use DateTimeInterface; use Shaarli\Bookmark\Bookmark; use Shaarli\Config\ConfigManager; @@ -11,6 +11,29 @@ use Shaarli\Config\ConfigManager; * * Abstract class processing all bookmark attributes through methods designed to be overridden. * + * List of available formatted fields: + * - id ID + * - shorturl Unique identifier, used in permalinks + * - url URL, can be altered in some way, e.g. passing through an HTTP reverse proxy + * - real_url (legacy) same as `url` + * - url_html URL to be displayed in HTML content (it can contain HTML tags) + * - title Title + * - title_html Title to be displayed in HTML content (it can contain HTML tags) + * - description Description content. It most likely contains HTML tags + * - thumbnail Thumbnail: path to local cache file, false if there is none, null if hasn't been retrieved + * - taglist List of tags (array) + * - taglist_urlencoded List of tags (array) URL encoded: it must be used to create a link to a URL containing a tag + * - taglist_html List of tags (array) to be displayed in HTML content (it can contain HTML tags) + * - tags Tags separated by a single whitespace + * - tags_urlencoded Tags separated by a single whitespace, URL encoded: must be used to create a link + * - sticky Is sticky (bool) + * - private Is private (bool) + * - class Additional CSS class + * - created Creation DateTime + * - updated Last edit DateTime + * - timestamp Creation timestamp + * - updated_timestamp Last edit timestamp + * * @package Shaarli\Formatter */ abstract class BookmarkFormatter @@ -55,13 +78,16 @@ abstract class BookmarkFormatter $out['shorturl'] = $this->formatShortUrl($bookmark); $out['url'] = $this->formatUrl($bookmark); $out['real_url'] = $this->formatRealUrl($bookmark); + $out['url_html'] = $this->formatUrlHtml($bookmark); $out['title'] = $this->formatTitle($bookmark); + $out['title_html'] = $this->formatTitleHtml($bookmark); $out['description'] = $this->formatDescription($bookmark); $out['thumbnail'] = $this->formatThumbnail($bookmark); - $out['urlencoded_taglist'] = $this->formatUrlEncodedTagList($bookmark); $out['taglist'] = $this->formatTagList($bookmark); - $out['urlencoded_tags'] = $this->formatUrlEncodedTagString($bookmark); + $out['taglist_urlencoded'] = $this->formatTagListUrlEncoded($bookmark); + $out['taglist_html'] = $this->formatTagListHtml($bookmark); $out['tags'] = $this->formatTagString($bookmark); + $out['tags_urlencoded'] = $this->formatTagStringUrlEncoded($bookmark); $out['sticky'] = $bookmark->isSticky(); $out['private'] = $bookmark->isPrivate(); $out['class'] = $this->formatClass($bookmark); @@ -69,6 +95,7 @@ abstract class BookmarkFormatter $out['updated'] = $this->formatUpdated($bookmark); $out['timestamp'] = $this->formatCreatedTimestamp($bookmark); $out['updated_timestamp'] = $this->formatUpdatedTimestamp($bookmark); + return $out; } @@ -135,6 +162,18 @@ abstract class BookmarkFormatter return $this->formatUrl($bookmark); } + /** + * Format Url Html: to be displayed in HTML content, it can contains HTML tags. + * + * @param Bookmark $bookmark instance + * + * @return string formatted Url HTML + */ + protected function formatUrlHtml($bookmark) + { + return $this->formatUrl($bookmark); + } + /** * Format Title * @@ -147,6 +186,18 @@ abstract class BookmarkFormatter return $bookmark->getTitle(); } + /** + * Format Title HTML: to be displayed in HTML content, it can contains HTML tags. + * + * @param Bookmark $bookmark instance + * + * @return string formatted Title + */ + protected function formatTitleHtml($bookmark) + { + return $bookmark->getTitle(); + } + /** * Format Description * @@ -190,11 +241,23 @@ abstract class BookmarkFormatter * * @return array formatted Tags */ - protected function formatUrlEncodedTagList($bookmark) + protected function formatTagListUrlEncoded($bookmark) { return array_map('urlencode', $this->filterTagList($bookmark->getTags())); } + /** + * Format Tags HTML: to be displayed in HTML content, it can contains HTML tags. + * + * @param Bookmark $bookmark instance + * + * @return array formatted Tags + */ + protected function formatTagListHtml($bookmark) + { + return $this->formatTagList($bookmark); + } + /** * Format TagString * @@ -214,9 +277,9 @@ abstract class BookmarkFormatter * * @return string formatted TagString */ - protected function formatUrlEncodedTagString($bookmark) + protected function formatTagStringUrlEncoded($bookmark) { - return implode(' ', $this->formatUrlEncodedTagList($bookmark)); + return implode(' ', $this->formatTagListUrlEncoded($bookmark)); } /** @@ -237,7 +300,7 @@ abstract class BookmarkFormatter * * @param Bookmark $bookmark instance * - * @return DateTime instance + * @return DateTimeInterface instance */ protected function formatCreated(Bookmark $bookmark) { @@ -249,7 +312,7 @@ abstract class BookmarkFormatter * * @param Bookmark $bookmark instance * - * @return DateTime instance + * @return DateTimeInterface instance */ protected function formatUpdated(Bookmark $bookmark) { diff --git a/application/formatter/BookmarkMarkdownFormatter.php b/application/formatter/BookmarkMarkdownFormatter.php index 5d244d4c..f7714be9 100644 --- a/application/formatter/BookmarkMarkdownFormatter.php +++ b/application/formatter/BookmarkMarkdownFormatter.php @@ -56,7 +56,10 @@ class BookmarkMarkdownFormatter extends BookmarkDefaultFormatter return parent::formatDescription($bookmark); } - $processedDescription = $bookmark->getDescription(); + $processedDescription = $this->tokenizeSearchHighlightField( + $bookmark->getDescription() ?? '', + $bookmark->getAdditionalContentEntry('search_highlight')['description'] ?? [] + ); $processedDescription = $this->filterProtocols($processedDescription); $processedDescription = $this->formatHashTags($processedDescription); $processedDescription = $this->reverseEscapedHtml($processedDescription); @@ -65,6 +68,7 @@ class BookmarkMarkdownFormatter extends BookmarkDefaultFormatter ->setBreaksEnabled(true) ->text($processedDescription); $processedDescription = $this->sanitizeHtml($processedDescription); + $processedDescription = $this->replaceTokens($processedDescription); if (!empty($processedDescription)) { $processedDescription = '
'. $processedDescription . '
'; -- cgit v1.2.3 From 21e72da9ee34cec56b10c83ae0c75b4bf320dfcb Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Thu, 15 Oct 2020 11:46:24 +0200 Subject: Asynchronous retrieval of bookmark's thumbnails This feature is based general.enable_async_metadata setting and works with existing metadata.js file. The script is compatible with any template: - the thumbnail div bloc must have attribute - the bookmark bloc must have attribute with the bookmark ID as value Fixes #1564 --- application/bookmark/Bookmark.php | 18 ++++++++++++++++++ .../front/controller/admin/ManageShaareController.php | 3 ++- .../controller/visitor/BookmarkListController.php | 10 ++++------ 3 files changed, 24 insertions(+), 7 deletions(-) (limited to 'application') diff --git a/application/bookmark/Bookmark.php b/application/bookmark/Bookmark.php index ea565d1f..4810c5e6 100644 --- a/application/bookmark/Bookmark.php +++ b/application/bookmark/Bookmark.php @@ -377,6 +377,24 @@ class Bookmark return $this; } + /** + * Return true if: + * - the bookmark's thumbnail is not already set to false (= not found) + * - it's not a note + * - it's an HTTP(S) link + * - the thumbnail has not yet be retrieved (null) or its associated cache file doesn't exist anymore + * + * @return bool True if the bookmark's thumbnail needs to be retrieved. + */ + public function shouldUpdateThumbnail(): bool + { + return $this->thumbnail !== false + && !$this->isNote() + && startsWith(strtolower($this->url), 'http') + && (null === $this->thumbnail || !is_file($this->thumbnail)) + ; + } + /** * Get the Sticky. * diff --git a/application/front/controller/admin/ManageShaareController.php b/application/front/controller/admin/ManageShaareController.php index df2f1631..908ebae3 100644 --- a/application/front/controller/admin/ManageShaareController.php +++ b/application/front/controller/admin/ManageShaareController.php @@ -129,7 +129,8 @@ class ManageShaareController extends ShaarliAdminController $bookmark->setTagsString($request->getParam('lf_tags')); if ($this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE - && false === $bookmark->isNote() + && true !== $this->container->conf->get('general.enable_async_metadata', true) + && $bookmark->shouldUpdateThumbnail() ) { $bookmark->setThumbnail($this->container->thumbnailer->get($bookmark->getUrl())); } diff --git a/application/front/controller/visitor/BookmarkListController.php b/application/front/controller/visitor/BookmarkListController.php index 18368751..a8019ead 100644 --- a/application/front/controller/visitor/BookmarkListController.php +++ b/application/front/controller/visitor/BookmarkListController.php @@ -169,14 +169,11 @@ class BookmarkListController extends ShaarliVisitorController */ protected function updateThumbnail(Bookmark $bookmark, bool $writeDatastore = true): bool { - // Logged in, thumbnails enabled, not a note, is HTTP - // and (never retrieved yet or no valid cache file) + // 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 - && false !== $bookmark->getThumbnail() - && !$bookmark->isNote() - && (null === $bookmark->getThumbnail() || !is_file($bookmark->getThumbnail())) - && startsWith(strtolower($bookmark->getUrl()), 'http') + && $bookmark->shouldUpdateThumbnail() ) { $bookmark->setThumbnail($this->container->thumbnailer->get($bookmark->getUrl())); $this->container->bookmarkService->set($bookmark, $writeDatastore); @@ -198,6 +195,7 @@ class BookmarkListController extends ShaarliVisitorController 'page_max' => '', 'search_tags' => '', 'result_count' => '', + 'async_metadata' => $this->container->conf->get('general.enable_async_metadata', true) ]; } -- cgit v1.2.3 From b38a1b0209f546d4824a0db81a34c4e30fcdebaf Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Tue, 20 Oct 2020 11:47:07 +0200 Subject: Use PSR-3 logger for login attempts Fixes #1122 --- application/Utils.php | 24 ++++---- application/container/ContainerBuilder.php | 10 +++- application/container/ShaarliContainer.php | 2 + .../front/controller/visitor/LoginController.php | 1 - application/render/PageBuilder.php | 29 ++++++--- application/security/BanManager.php | 28 ++++----- application/security/LoginManager.php | 69 ++++++++++------------ 7 files changed, 87 insertions(+), 76 deletions(-) (limited to 'application') diff --git a/application/Utils.php b/application/Utils.php index bcfda65c..7a9d2645 100644 --- a/application/Utils.php +++ b/application/Utils.php @@ -4,21 +4,23 @@ */ /** - * Logs a message to a text file + * Format log using provided data. * - * The log format is compatible with fail2ban. + * @param string $message the message to log + * @param string|null $clientIp the client's remote IPv4/IPv6 address * - * @param string $logFile where to write the logs - * @param string $clientIp the client's remote IPv4/IPv6 address - * @param string $message the message to log + * @return string Formatted message to log */ -function logm($logFile, $clientIp, $message) +function format_log(string $message, string $clientIp = null): string { - file_put_contents( - $logFile, - date('Y/m/d H:i:s').' - '.$clientIp.' - '.strval($message).PHP_EOL, - FILE_APPEND - ); + $out = $message; + + if (!empty($clientIp)) { + // Note: we keep the first dash to avoid breaking fail2ban configs + $out = '- ' . $clientIp . ' - ' . $out; + } + + return $out; } /** diff --git a/application/container/ContainerBuilder.php b/application/container/ContainerBuilder.php index fd94a1c3..d84418ad 100644 --- a/application/container/ContainerBuilder.php +++ b/application/container/ContainerBuilder.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Shaarli\Container; use malkusch\lock\mutex\FlockMutex; +use Psr\Log\LoggerInterface; use Shaarli\Bookmark\BookmarkFileService; use Shaarli\Bookmark\BookmarkServiceInterface; use Shaarli\Config\ConfigManager; @@ -49,6 +50,9 @@ class ContainerBuilder /** @var LoginManager */ protected $login; + /** @var LoggerInterface */ + protected $logger; + /** @var string|null */ protected $basePath = null; @@ -56,12 +60,14 @@ class ContainerBuilder ConfigManager $conf, SessionManager $session, CookieManager $cookieManager, - LoginManager $login + LoginManager $login, + LoggerInterface $logger ) { $this->conf = $conf; $this->session = $session; $this->login = $login; $this->cookieManager = $cookieManager; + $this->logger = $logger; } public function build(): ShaarliContainer @@ -72,6 +78,7 @@ class ContainerBuilder $container['sessionManager'] = $this->session; $container['cookieManager'] = $this->cookieManager; $container['loginManager'] = $this->login; + $container['logger'] = $this->logger; $container['basePath'] = $this->basePath; $container['plugins'] = function (ShaarliContainer $container): PluginManager { @@ -99,6 +106,7 @@ class ContainerBuilder return new PageBuilder( $container->conf, $container->sessionManager->getSession(), + $container->logger, $container->bookmarkService, $container->sessionManager->generateToken(), $container->loginManager->isLoggedIn() diff --git a/application/container/ShaarliContainer.php b/application/container/ShaarliContainer.php index 3a7c238f..3e5bd252 100644 --- a/application/container/ShaarliContainer.php +++ b/application/container/ShaarliContainer.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Shaarli\Container; +use Psr\Log\LoggerInterface; use Shaarli\Bookmark\BookmarkServiceInterface; use Shaarli\Config\ConfigManager; use Shaarli\Feed\FeedBuilder; @@ -36,6 +37,7 @@ use Slim\Container; * @property History $history * @property HttpAccess $httpAccess * @property LoginManager $loginManager + * @property LoggerInterface $logger * @property MetadataRetriever $metadataRetriever * @property NetscapeBookmarkUtils $netscapeBookmarkUtils * @property callable $notFoundHandler Overrides default Slim exception display diff --git a/application/front/controller/visitor/LoginController.php b/application/front/controller/visitor/LoginController.php index 121ba40b..f5038fe3 100644 --- a/application/front/controller/visitor/LoginController.php +++ b/application/front/controller/visitor/LoginController.php @@ -65,7 +65,6 @@ class LoginController extends ShaarliVisitorController } if (!$this->container->loginManager->checkCredentials( - $this->container->environment['REMOTE_ADDR'], client_ip_id($this->container->environment), $request->getParam('login'), $request->getParam('password') diff --git a/application/render/PageBuilder.php b/application/render/PageBuilder.php index 2d6d2dbe..512bb79e 100644 --- a/application/render/PageBuilder.php +++ b/application/render/PageBuilder.php @@ -3,7 +3,7 @@ namespace Shaarli\Render; use Exception; -use exceptions\MissingBasePathException; +use Psr\Log\LoggerInterface; use RainTPL; use Shaarli\ApplicationUtils; use Shaarli\Bookmark\BookmarkServiceInterface; @@ -35,6 +35,9 @@ class PageBuilder */ protected $session; + /** @var LoggerInterface */ + protected $logger; + /** * @var BookmarkServiceInterface $bookmarkService instance. */ @@ -54,17 +57,25 @@ class PageBuilder * PageBuilder constructor. * $tpl is initialized at false for lazy loading. * - * @param ConfigManager $conf Configuration Manager instance (reference). - * @param array $session $_SESSION array - * @param BookmarkServiceInterface $linkDB instance. - * @param string $token Session token - * @param bool $isLoggedIn + * @param ConfigManager $conf Configuration Manager instance (reference). + * @param array $session $_SESSION array + * @param LoggerInterface $logger + * @param null $linkDB instance. + * @param null $token Session token + * @param bool $isLoggedIn */ - public function __construct(&$conf, $session, $linkDB = null, $token = null, $isLoggedIn = false) - { + public function __construct( + ConfigManager &$conf, + array $session, + LoggerInterface $logger, + $linkDB = null, + $token = null, + $isLoggedIn = false + ) { $this->tpl = false; $this->conf = $conf; $this->session = $session; + $this->logger = $logger; $this->bookmarkService = $linkDB; $this->token = $token; $this->isLoggedIn = $isLoggedIn; @@ -98,7 +109,7 @@ class PageBuilder $this->tpl->assign('newVersion', escape($version)); $this->tpl->assign('versionError', ''); } catch (Exception $exc) { - logm($this->conf->get('resource.log'), $_SERVER['REMOTE_ADDR'], $exc->getMessage()); + $this->logger->error(format_log('Error: ' . $exc->getMessage(), client_ip_id($_SERVER))); $this->tpl->assign('newVersion', ''); $this->tpl->assign('versionError', escape($exc->getMessage())); } diff --git a/application/security/BanManager.php b/application/security/BanManager.php index 68190c54..f72c8b7b 100644 --- a/application/security/BanManager.php +++ b/application/security/BanManager.php @@ -3,6 +3,7 @@ namespace Shaarli\Security; +use Psr\Log\LoggerInterface; use Shaarli\FileUtils; /** @@ -28,8 +29,8 @@ class BanManager /** @var string Path to the file containing IP bans and failures */ protected $banFile; - /** @var string Path to the log file, used to log bans */ - protected $logFile; + /** @var LoggerInterface Path to the log file, used to log bans */ + protected $logger; /** @var array List of IP with their associated number of failed attempts */ protected $failures = []; @@ -40,18 +41,19 @@ class BanManager /** * BanManager constructor. * - * @param array $trustedProxies List of allowed proxies IP - * @param int $nbAttempts Number of allowed failed attempt before the ban - * @param int $banDuration Ban duration in seconds - * @param string $banFile Path to the file containing IP bans and failures - * @param string $logFile Path to the log file, used to log bans + * @param array $trustedProxies List of allowed proxies IP + * @param int $nbAttempts Number of allowed failed attempt before the ban + * @param int $banDuration Ban duration in seconds + * @param string $banFile Path to the file containing IP bans and failures + * @param LoggerInterface $logger PSR-3 logger to save login attempts in log directory */ - public function __construct($trustedProxies, $nbAttempts, $banDuration, $banFile, $logFile) { + public function __construct($trustedProxies, $nbAttempts, $banDuration, $banFile, LoggerInterface $logger) { $this->trustedProxies = $trustedProxies; $this->nbAttempts = $nbAttempts; $this->banDuration = $banDuration; $this->banFile = $banFile; - $this->logFile = $logFile; + $this->logger = $logger; + $this->readBanFile(); } @@ -78,11 +80,7 @@ class BanManager if ($this->failures[$ip] >= $this->nbAttempts) { $this->bans[$ip] = time() + $this->banDuration; - logm( - $this->logFile, - $server['REMOTE_ADDR'], - 'IP address banned from login: '. $ip - ); + $this->logger->info(format_log('IP address banned from login: '. $ip, $ip)); } $this->writeBanFile(); } @@ -138,7 +136,7 @@ class BanManager unset($this->failures[$ip]); } unset($this->bans[$ip]); - logm($this->logFile, $server['REMOTE_ADDR'], 'Ban lifted for: '. $ip); + $this->logger->info(format_log('Ban lifted for: '. $ip, $ip)); $this->writeBanFile(); return false; diff --git a/application/security/LoginManager.php b/application/security/LoginManager.php index 65048f10..426e785e 100644 --- a/application/security/LoginManager.php +++ b/application/security/LoginManager.php @@ -2,6 +2,7 @@ namespace Shaarli\Security; use Exception; +use Psr\Log\LoggerInterface; use Shaarli\Config\ConfigManager; /** @@ -31,26 +32,30 @@ class LoginManager protected $staySignedInToken = ''; /** @var CookieManager */ protected $cookieManager; + /** @var LoggerInterface */ + protected $logger; /** * Constructor * - * @param ConfigManager $configManager Configuration Manager instance + * @param ConfigManager $configManager Configuration Manager instance * @param SessionManager $sessionManager SessionManager instance - * @param CookieManager $cookieManager CookieManager instance + * @param CookieManager $cookieManager CookieManager instance + * @param BanManager $banManager + * @param LoggerInterface $logger Used to log login attempts */ - public function __construct($configManager, $sessionManager, $cookieManager) - { + public function __construct( + ConfigManager $configManager, + SessionManager $sessionManager, + CookieManager $cookieManager, + BanManager $banManager, + LoggerInterface $logger + ) { $this->configManager = $configManager; $this->sessionManager = $sessionManager; $this->cookieManager = $cookieManager; - $this->banManager = new BanManager( - $this->configManager->get('security.trusted_proxies', []), - $this->configManager->get('security.ban_after'), - $this->configManager->get('security.ban_duration'), - $this->configManager->get('resource.ban_file', 'data/ipbans.php'), - $this->configManager->get('resource.log') - ); + $this->banManager = $banManager; + $this->logger = $logger; if ($this->configManager->get('security.open_shaarli') === true) { $this->openShaarli = true; @@ -129,48 +134,34 @@ class LoginManager /** * Check user credentials are valid * - * @param string $remoteIp Remote client IP address * @param string $clientIpId Client IP address identifier * @param string $login Username * @param string $password Password * * @return bool true if the provided credentials are valid, false otherwise */ - public function checkCredentials($remoteIp, $clientIpId, $login, $password) + public function checkCredentials($clientIpId, $login, $password) { - // Check login matches config - if ($login !== $this->configManager->get('credentials.login')) { - return false; - } - // Check credentials try { $useLdapLogin = !empty($this->configManager->get('ldap.host')); - if ((false === $useLdapLogin && $this->checkCredentialsFromLocalConfig($login, $password)) - || (true === $useLdapLogin && $this->checkCredentialsFromLdap($login, $password)) + if ($login === $this->configManager->get('credentials.login') + && ( + (false === $useLdapLogin && $this->checkCredentialsFromLocalConfig($login, $password)) + || (true === $useLdapLogin && $this->checkCredentialsFromLdap($login, $password)) + ) ) { - $this->sessionManager->storeLoginInfo($clientIpId); - logm( - $this->configManager->get('resource.log'), - $remoteIp, - 'Login successful' - ); - return true; + $this->sessionManager->storeLoginInfo($clientIpId); + $this->logger->info(format_log('Login successful', $clientIpId)); + + return true; } - } - catch(Exception $exception) { - logm( - $this->configManager->get('resource.log'), - $remoteIp, - 'Exception while checking credentials: ' . $exception - ); + } catch(Exception $exception) { + $this->logger->info(format_log('Exception while checking credentials: ' . $exception, $clientIpId)); } - logm( - $this->configManager->get('resource.log'), - $remoteIp, - 'Login failed for user ' . $login - ); + $this->logger->info(format_log('Login failed for user ' . $login, $clientIpId)); + return false; } -- cgit v1.2.3 From 5c06c0870f8e425c2d4ed0f7c330c13e1605628e Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Tue, 20 Oct 2020 18:32:46 +0200 Subject: Dislay an error if an exception occurs in the error handler Related to #1598 --- application/Utils.php | 9 +++++++++ application/front/controller/visitor/ErrorController.php | 5 +---- 2 files changed, 10 insertions(+), 4 deletions(-) (limited to 'application') diff --git a/application/Utils.php b/application/Utils.php index bcfda65c..37be9a13 100644 --- a/application/Utils.php +++ b/application/Utils.php @@ -463,3 +463,12 @@ function t($text, $nText = '', $nb = 1, $domain = 'shaarli') { return dn__($domain, $text, $nText, $nb); } + +/** + * Converts an exception into a printable stack trace string. + */ +function exception2text(Throwable $e): string +{ + return $e->getMessage() . PHP_EOL . $e->getFile() . $e->getLine() . PHP_EOL . $e->getTraceAsString(); +} + diff --git a/application/front/controller/visitor/ErrorController.php b/application/front/controller/visitor/ErrorController.php index 10aa84c8..8da11172 100644 --- a/application/front/controller/visitor/ErrorController.php +++ b/application/front/controller/visitor/ErrorController.php @@ -28,10 +28,7 @@ class ErrorController extends ShaarliVisitorController // Internal error (any other Throwable) if ($this->container->conf->get('dev.debug', false)) { $this->assignView('message', $throwable->getMessage()); - $this->assignView( - 'stacktrace', - nl2br(get_class($throwable) .': '. PHP_EOL . $throwable->getTraceAsString()) - ); + $this->assignView('stacktrace', exception2text($throwable)); } else { $this->assignView('message', t('An unexpected error occurred.')); } -- cgit v1.2.3 From 0cf76ccb4736473a958d9fd36ed914e2d25d594a Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Wed, 21 Oct 2020 13:12:15 +0200 Subject: Feature: add a Server administration page It contains mostly read only information about the current Shaarli instance, PHP version, extensions, file and folder permissions, etc. Also action buttons to clear the cache or sync thumbnails. Part of the content of this page is also displayed on the install page, to check server requirement before installing Shaarli config file. Fixes #40 Fixes #185 --- application/ApplicationUtils.php | 93 ++++++++++++++++++---- application/FileUtils.php | 56 +++++++++++++ .../front/controller/admin/ServerController.php | 87 ++++++++++++++++++++ .../controller/visitor/BookmarkListController.php | 28 ++++--- .../front/controller/visitor/InstallController.php | 12 ++- 5 files changed, 251 insertions(+), 25 deletions(-) create mode 100644 application/front/controller/admin/ServerController.php (limited to 'application') diff --git a/application/ApplicationUtils.php b/application/ApplicationUtils.php index 3aa21829..bd1c7cf3 100644 --- a/application/ApplicationUtils.php +++ b/application/ApplicationUtils.php @@ -14,8 +14,9 @@ class ApplicationUtils */ 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 = ''; @@ -125,7 +126,7 @@ class ApplicationUtils // 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) { @@ -171,35 +172,45 @@ class ApplicationUtils /** * 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'); } @@ -208,6 +219,10 @@ class ApplicationUtils } } + if ($minimalMode) { + return $errors; + } + // Check configuration files are readable and writable foreach (array( $conf->getConfigFileExt(), @@ -246,4 +261,54 @@ class ApplicationUtils { 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'); + } } diff --git a/application/FileUtils.php b/application/FileUtils.php index 30560bfc..3f940751 100644 --- a/application/FileUtils.php +++ b/application/FileUtils.php @@ -81,4 +81,60 @@ class FileUtils ) ); } + + /** + * 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(__FILE__)); + + return strpos(realpath($path), $rootDirectory) !== false; + } } diff --git a/application/front/controller/admin/ServerController.php b/application/front/controller/admin/ServerController.php new file mode 100644 index 00000000..85654a43 --- /dev/null +++ b/application/front/controller/admin/ServerController.php @@ -0,0 +1,87 @@ +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.') . ' ' . + '' . t('Please synchronize them.') .'' + ); + } 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'); + } +} diff --git a/application/front/controller/visitor/BookmarkListController.php b/application/front/controller/visitor/BookmarkListController.php index a8019ead..5267c8f5 100644 --- a/application/front/controller/visitor/BookmarkListController.php +++ b/application/front/controller/visitor/BookmarkListController.php @@ -169,16 +169,24 @@ class BookmarkListController extends ShaarliVisitorController */ 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; diff --git a/application/front/controller/visitor/InstallController.php b/application/front/controller/visitor/InstallController.php index 7cb32777..564a5777 100644 --- a/application/front/controller/visitor/InstallController.php +++ b/application/front/controller/visitor/InstallController.php @@ -53,6 +53,16 @@ class InstallController extends ShaarliVisitorController $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')); } @@ -150,7 +160,7 @@ class InstallController extends ShaarliVisitorController 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; } -- cgit v1.2.3 From 42a72c02fa4b6a5eb9d26a7a3a990e497fc10df3 Mon Sep 17 00:00:00 2001 From: Ganesh Kandu Date: Tue, 27 Oct 2020 17:42:35 +0530 Subject: Replaced PHP_EOL to "\n" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit i was getting error ``` An error occurred while parsing JSON configuration file (data/config.json.php): error code #4 âžœ Syntax error Please check your JSON syntax (without PHP comment tags) using a JSON lint tool such as jsonlint.com. ``` after debug i found ```php $data = str_replace(self::getPhpHeaders(), '', $data); $data = str_replace(self::getPhpSuffix(), '', $data); ``` doesn't removing php header and php suffix cause of this issue was PHP_EOL represents the endline character for the current system. if my ```config.json.php``` was encoded with unix ( LF ) and php running on windows windows encoding ( CR LF ) is not same as unix encoding ( LF ) so ```str_replace``` doesn't replace strin then it causes issue. --- application/config/ConfigJson.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'application') diff --git a/application/config/ConfigJson.php b/application/config/ConfigJson.php index c0c0dab9..eaa4ee3f 100644 --- a/application/config/ConfigJson.php +++ b/application/config/ConfigJson.php @@ -73,7 +73,7 @@ class ConfigJson implements ConfigIO */ public static function getPhpHeaders() { - return ''; + return "\n" . '*/ ?>'; } } -- cgit v1.2.3 From e69e3fef7bbdc7299ae01aa0e0258395d2e49818 Mon Sep 17 00:00:00 2001 From: Ganesh Kandu Date: Tue, 27 Oct 2020 18:08:14 +0530 Subject: Removed PHP_EOL just replace "*/ ?>" and "'; + return '*/ ?>'; } } -- cgit v1.2.3 From 9c04921a8c28c18ef757f2d43ba35e7e2a7f1a4b Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Fri, 16 Oct 2020 20:17:08 +0200 Subject: Feature: Share private bookmarks using a URL containing a private key MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add a share link next to « Permalink » in linklist (using share icon from fork awesome) - This link generates a private key associated to the bookmark - Accessing the bookmark while logged out with the proper key will display it Fixes #475 --- application/bookmark/BookmarkFileService.php | 7 ++++-- application/bookmark/BookmarkServiceInterface.php | 5 +++-- .../controller/admin/ManageShaareController.php | 26 ++++++++++++++++++++++ .../controller/visitor/BookmarkListController.php | 4 +++- 4 files changed, 37 insertions(+), 5 deletions(-) (limited to 'application') diff --git a/application/bookmark/BookmarkFileService.php b/application/bookmark/BookmarkFileService.php index eb7899bf..14b3d620 100644 --- a/application/bookmark/BookmarkFileService.php +++ b/application/bookmark/BookmarkFileService.php @@ -97,12 +97,15 @@ class BookmarkFileService implements BookmarkServiceInterface /** * @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()) { + if (!$this->isLoggedIn + && $first->isPrivate() + && (empty($privateKey) || $privateKey !== $first->getAdditionalContentEntry('private_key')) + ) { throw new Exception('Not authorized'); } diff --git a/application/bookmark/BookmarkServiceInterface.php b/application/bookmark/BookmarkServiceInterface.php index 37a54d03..9fa61533 100644 --- a/application/bookmark/BookmarkServiceInterface.php +++ b/application/bookmark/BookmarkServiceInterface.php @@ -20,13 +20,14 @@ interface BookmarkServiceInterface /** * 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 diff --git a/application/front/controller/admin/ManageShaareController.php b/application/front/controller/admin/ManageShaareController.php index 908ebae3..e490f85a 100644 --- a/application/front/controller/admin/ManageShaareController.php +++ b/application/front/controller/admin/ManageShaareController.php @@ -320,6 +320,32 @@ class ManageShaareController extends ShaarliAdminController 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') + ); + } + /** * Helper function used to display the shaare form whether it's a new or existing bookmark. * diff --git a/application/front/controller/visitor/BookmarkListController.php b/application/front/controller/visitor/BookmarkListController.php index 5267c8f5..78c474c9 100644 --- a/application/front/controller/visitor/BookmarkListController.php +++ b/application/front/controller/visitor/BookmarkListController.php @@ -137,8 +137,10 @@ class BookmarkListController extends ShaarliVisitorController */ 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()); -- cgit v1.2.3 From c2cd15dac2bfaebe6d32f7649fbdedc07400fa08 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Fri, 16 Oct 2020 13:34:59 +0200 Subject: Move utils classes to Shaarli\Helper namespace and folder --- application/ApplicationUtils.php | 314 --------------------- application/FileUtils.php | 140 --------- application/History.php | 1 + .../front/controller/visitor/InstallController.php | 2 +- application/helper/ApplicationUtils.php | 314 +++++++++++++++++++++ application/helper/FileUtils.php | 140 +++++++++ application/legacy/LegacyLinkDB.php | 2 +- application/legacy/LegacyUpdater.php | 2 +- application/render/PageBuilder.php | 2 +- application/security/BanManager.php | 2 +- 10 files changed, 460 insertions(+), 459 deletions(-) delete mode 100644 application/ApplicationUtils.php delete mode 100644 application/FileUtils.php create mode 100644 application/helper/ApplicationUtils.php create mode 100644 application/helper/FileUtils.php (limited to 'application') diff --git a/application/ApplicationUtils.php b/application/ApplicationUtils.php deleted file mode 100644 index bd1c7cf3..00000000 --- a/application/ApplicationUtils.php +++ /dev/null @@ -1,314 +0,0 @@ -'; - - /** - * Gets the latest version code from the Git repository - * - * The code is read from the raw content of the version file on the Git server. - * - * @param string $url URL to reach to get the latest version. - * @param int $timeout Timeout to check the URL (in seconds). - * - * @return mixed the version code from the repository if available, else 'false' - */ - public static function getLatestGitVersionCode($url, $timeout = 2) - { - list($headers, $data) = get_http_response($url, $timeout); - - if (strpos($headers[0], '200 OK') === false) { - error_log('Failed to retrieve ' . $url); - return false; - } - - return $data; - } - - /** - * Retrieve the version from a remote URL or a file. - * - * @param string $remote URL or file to fetch. - * @param int $timeout For URLs fetching. - * - * @return bool|string The version or false if it couldn't be retrieved. - */ - public static function getVersion($remote, $timeout = 2) - { - if (startsWith($remote, 'http')) { - if (($data = static::getLatestGitVersionCode($remote, $timeout)) === false) { - return false; - } - } else { - if (!is_file($remote)) { - return false; - } - $data = file_get_contents($remote); - } - - return str_replace( - array(self::$VERSION_START_TAG, self::$VERSION_END_TAG, PHP_EOL), - array('', '', ''), - $data - ); - } - - /** - * Checks if a new Shaarli version has been published on the Git repository - * - * Updates checks are run periodically, according to the following criteria: - * - the update checks are enabled (install, global config); - * - the user is logged in (or this is an open instance); - * - the last check is older than a given interval; - * - the check is non-blocking if the HTTPS connection to Git fails; - * - in case of failure, the update file's modification date is updated, - * to avoid intempestive connection attempts. - * - * @param string $currentVersion the current version code - * @param string $updateFile the file where to store the latest version code - * @param int $checkInterval the minimum interval between update checks (in seconds - * @param bool $enableCheck whether to check for new versions - * @param bool $isLoggedIn whether the user is logged in - * @param string $branch check update for the given branch - * - * @throws Exception an invalid branch has been set for update checks - * - * @return mixed the new version code if available and greater, else 'false' - */ - public static function checkUpdate( - $currentVersion, - $updateFile, - $checkInterval, - $enableCheck, - $isLoggedIn, - $branch = 'stable' - ) { - // Do not check versions for visitors - // Do not check if the user doesn't want to - // Do not check with dev version - if (!$isLoggedIn || empty($enableCheck) || $currentVersion === 'dev') { - return false; - } - - if (is_file($updateFile) && (filemtime($updateFile) > time() - $checkInterval)) { - // Shaarli has checked for updates recently - skip HTTP query - $latestKnownVersion = file_get_contents($updateFile); - - if (version_compare($latestKnownVersion, $currentVersion) == 1) { - return $latestKnownVersion; - } - return false; - } - - if (!in_array($branch, self::$GIT_BRANCHES)) { - throw new Exception( - 'Invalid branch selected for updates: "' . $branch . '"' - ); - } - - // Late Static Binding allows overriding within tests - // See http://php.net/manual/en/language.oop5.late-static-bindings.php - $latestVersion = static::getVersion( - self::$GIT_RAW_URL . '/' . $branch . '/' . self::$VERSION_FILE - ); - - if (!$latestVersion) { - // Only update the file's modification date - file_put_contents($updateFile, $currentVersion); - return false; - } - - // Update the file's content and modification date - file_put_contents($updateFile, $latestVersion); - - if (version_compare($latestVersion, $currentVersion) == 1) { - return $latestVersion; - } - - return false; - } - - /** - * Checks the PHP version to ensure Shaarli can run - * - * @param string $minVersion minimum PHP required version - * @param string $curVersion current PHP version (use PHP_VERSION) - * - * @return bool true on success - * - * @throws Exception the PHP version is not supported - */ - public static function checkPHPVersion($minVersion, $curVersion) - { - if (version_compare($curVersion, $minVersion) < 0) { - $msg = t( - '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.' - ); - throw new Exception(sprintf($msg, $minVersion)); - } - return true; - } - - /** - * Checks Shaarli has the proper access permissions to its resources - * - * @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(ConfigManager $conf, bool $minimalMode = false): array - { - $errors = []; - $rainTplDir = rtrim($conf->get('resource.raintpl_tpl'), '/'); - - // Check script and template directories are readable - foreach ([ - 'application', - 'inc', - 'plugins', - $rainTplDir, - $rainTplDir . '/' . $conf->get('resource.theme'), - ] as $path) { - if (!is_readable(realpath($path))) { - $errors[] = '"' . $path . '" ' . t('directory is not readable'); - } - } - - // Check cache and data directories are readable and writable - 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 (!is_writable(realpath($path))) { - $errors[] = '"' . $path . '" ' . t('directory is not writable'); - } - } - - if ($minimalMode) { - return $errors; - } - - // Check configuration files are readable and writable - foreach (array( - $conf->getConfigFileExt(), - $conf->get('resource.datastore'), - $conf->get('resource.ban_file'), - $conf->get('resource.log'), - $conf->get('resource.update_check'), - ) as $path) { - if (!is_file(realpath($path))) { - # the file may not exist yet - continue; - } - - if (!is_readable(realpath($path))) { - $errors[] = '"' . $path . '" ' . t('file is not readable'); - } - if (!is_writable(realpath($path))) { - $errors[] = '"' . $path . '" ' . t('file is not writable'); - } - } - - return $errors; - } - - /** - * Returns a salted hash representing the current Shaarli version. - * - * Useful for assets browser cache. - * - * @param string $currentVersion of Shaarli - * @param string $salt User personal salt, also used for the authentication - * - * @return string version hash - */ - public static function getVersionHash($currentVersion, $salt) - { - 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'); - } -} diff --git a/application/FileUtils.php b/application/FileUtils.php deleted file mode 100644 index 3f940751..00000000 --- a/application/FileUtils.php +++ /dev/null @@ -1,140 +0,0 @@ -'; - - /** - * Write data into a file (Shaarli database format). - * The data is stored in a PHP file, as a comment, in compressed base64 format. - * - * The file will be created if it doesn't exist. - * - * @param string $file File path. - * @param mixed $content Content to write. - * - * @return int|bool Number of bytes written or false if it fails. - * - * @throws IOException The destination file can't be written. - */ - public static function writeFlatDB($file, $content) - { - if (is_file($file) && !is_writeable($file)) { - // The datastore exists but is not writeable - throw new IOException($file); - } elseif (!is_file($file) && !is_writeable(dirname($file))) { - // The datastore does not exist and its parent directory is not writeable - throw new IOException(dirname($file)); - } - - return file_put_contents( - $file, - self::$phpPrefix . base64_encode(gzdeflate(serialize($content))) . self::$phpSuffix - ); - } - - /** - * Read data from a file containing Shaarli database format content. - * - * If the file isn't readable or doesn't exist, default data will be returned. - * - * @param string $file File path. - * @param mixed $default The default value to return if the file isn't readable. - * - * @return mixed The content unserialized, or default if the file isn't readable, or false if it fails. - */ - public static function readFlatDB($file, $default = null) - { - // Note that gzinflate is faster than gzuncompress. - // See: http://www.php.net/manual/en/function.gzdeflate.php#96439 - if (!is_readable($file)) { - return $default; - } - - $data = file_get_contents($file); - if ($data == '') { - return $default; - } - - return unserialize( - gzinflate( - base64_decode( - substr($data, strlen(self::$phpPrefix), -strlen(self::$phpSuffix)) - ) - ) - ); - } - - /** - * 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(__FILE__)); - - return strpos(realpath($path), $rootDirectory) !== false; - } -} diff --git a/application/History.php b/application/History.php index 4fd2f294..bd5c1bf7 100644 --- a/application/History.php +++ b/application/History.php @@ -4,6 +4,7 @@ namespace Shaarli; use DateTime; use Exception; use Shaarli\Bookmark\Bookmark; +use Shaarli\Helper\FileUtils; /** * Class History diff --git a/application/front/controller/visitor/InstallController.php b/application/front/controller/visitor/InstallController.php index 564a5777..22329294 100644 --- a/application/front/controller/visitor/InstallController.php +++ b/application/front/controller/visitor/InstallController.php @@ -4,10 +4,10 @@ declare(strict_types=1); namespace Shaarli\Front\Controller\Visitor; -use Shaarli\ApplicationUtils; use Shaarli\Container\ShaarliContainer; use Shaarli\Front\Exception\AlreadyInstalledException; use Shaarli\Front\Exception\ResourcePermissionException; +use Shaarli\Helper\ApplicationUtils; use Shaarli\Languages; use Shaarli\Security\SessionManager; use Slim\Http\Request; diff --git a/application/helper/ApplicationUtils.php b/application/helper/ApplicationUtils.php new file mode 100644 index 00000000..4b34e114 --- /dev/null +++ b/application/helper/ApplicationUtils.php @@ -0,0 +1,314 @@ +'; + + /** + * Gets the latest version code from the Git repository + * + * The code is read from the raw content of the version file on the Git server. + * + * @param string $url URL to reach to get the latest version. + * @param int $timeout Timeout to check the URL (in seconds). + * + * @return mixed the version code from the repository if available, else 'false' + */ + public static function getLatestGitVersionCode($url, $timeout = 2) + { + list($headers, $data) = get_http_response($url, $timeout); + + if (strpos($headers[0], '200 OK') === false) { + error_log('Failed to retrieve ' . $url); + return false; + } + + return $data; + } + + /** + * Retrieve the version from a remote URL or a file. + * + * @param string $remote URL or file to fetch. + * @param int $timeout For URLs fetching. + * + * @return bool|string The version or false if it couldn't be retrieved. + */ + public static function getVersion($remote, $timeout = 2) + { + if (startsWith($remote, 'http')) { + if (($data = static::getLatestGitVersionCode($remote, $timeout)) === false) { + return false; + } + } else { + if (!is_file($remote)) { + return false; + } + $data = file_get_contents($remote); + } + + return str_replace( + array(self::$VERSION_START_TAG, self::$VERSION_END_TAG, PHP_EOL), + array('', '', ''), + $data + ); + } + + /** + * Checks if a new Shaarli version has been published on the Git repository + * + * Updates checks are run periodically, according to the following criteria: + * - the update checks are enabled (install, global config); + * - the user is logged in (or this is an open instance); + * - the last check is older than a given interval; + * - the check is non-blocking if the HTTPS connection to Git fails; + * - in case of failure, the update file's modification date is updated, + * to avoid intempestive connection attempts. + * + * @param string $currentVersion the current version code + * @param string $updateFile the file where to store the latest version code + * @param int $checkInterval the minimum interval between update checks (in seconds + * @param bool $enableCheck whether to check for new versions + * @param bool $isLoggedIn whether the user is logged in + * @param string $branch check update for the given branch + * + * @throws Exception an invalid branch has been set for update checks + * + * @return mixed the new version code if available and greater, else 'false' + */ + public static function checkUpdate( + $currentVersion, + $updateFile, + $checkInterval, + $enableCheck, + $isLoggedIn, + $branch = 'stable' + ) { + // Do not check versions for visitors + // Do not check if the user doesn't want to + // Do not check with dev version + if (!$isLoggedIn || empty($enableCheck) || $currentVersion === 'dev') { + return false; + } + + if (is_file($updateFile) && (filemtime($updateFile) > time() - $checkInterval)) { + // Shaarli has checked for updates recently - skip HTTP query + $latestKnownVersion = file_get_contents($updateFile); + + if (version_compare($latestKnownVersion, $currentVersion) == 1) { + return $latestKnownVersion; + } + return false; + } + + if (!in_array($branch, self::$GIT_BRANCHES)) { + throw new Exception( + 'Invalid branch selected for updates: "' . $branch . '"' + ); + } + + // Late Static Binding allows overriding within tests + // See http://php.net/manual/en/language.oop5.late-static-bindings.php + $latestVersion = static::getVersion( + self::$GIT_RAW_URL . '/' . $branch . '/' . self::$VERSION_FILE + ); + + if (!$latestVersion) { + // Only update the file's modification date + file_put_contents($updateFile, $currentVersion); + return false; + } + + // Update the file's content and modification date + file_put_contents($updateFile, $latestVersion); + + if (version_compare($latestVersion, $currentVersion) == 1) { + return $latestVersion; + } + + return false; + } + + /** + * Checks the PHP version to ensure Shaarli can run + * + * @param string $minVersion minimum PHP required version + * @param string $curVersion current PHP version (use PHP_VERSION) + * + * @return bool true on success + * + * @throws Exception the PHP version is not supported + */ + public static function checkPHPVersion($minVersion, $curVersion) + { + if (version_compare($curVersion, $minVersion) < 0) { + $msg = t( + '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.' + ); + throw new Exception(sprintf($msg, $minVersion)); + } + return true; + } + + /** + * Checks Shaarli has the proper access permissions to its resources + * + * @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(ConfigManager $conf, bool $minimalMode = false): array + { + $errors = []; + $rainTplDir = rtrim($conf->get('resource.raintpl_tpl'), '/'); + + // Check script and template directories are readable + foreach ([ + 'application', + 'inc', + 'plugins', + $rainTplDir, + $rainTplDir . '/' . $conf->get('resource.theme'), + ] as $path) { + if (!is_readable(realpath($path))) { + $errors[] = '"' . $path . '" ' . t('directory is not readable'); + } + } + + // Check cache and data directories are readable and writable + 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 (!is_writable(realpath($path))) { + $errors[] = '"' . $path . '" ' . t('directory is not writable'); + } + } + + if ($minimalMode) { + return $errors; + } + + // Check configuration files are readable and writable + foreach (array( + $conf->getConfigFileExt(), + $conf->get('resource.datastore'), + $conf->get('resource.ban_file'), + $conf->get('resource.log'), + $conf->get('resource.update_check'), + ) as $path) { + if (!is_file(realpath($path))) { + # the file may not exist yet + continue; + } + + if (!is_readable(realpath($path))) { + $errors[] = '"' . $path . '" ' . t('file is not readable'); + } + if (!is_writable(realpath($path))) { + $errors[] = '"' . $path . '" ' . t('file is not writable'); + } + } + + return $errors; + } + + /** + * Returns a salted hash representing the current Shaarli version. + * + * Useful for assets browser cache. + * + * @param string $currentVersion of Shaarli + * @param string $salt User personal salt, also used for the authentication + * + * @return string version hash + */ + public static function getVersionHash($currentVersion, $salt) + { + 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'); + } +} diff --git a/application/helper/FileUtils.php b/application/helper/FileUtils.php new file mode 100644 index 00000000..2d50d850 --- /dev/null +++ b/application/helper/FileUtils.php @@ -0,0 +1,140 @@ +'; + + /** + * Write data into a file (Shaarli database format). + * The data is stored in a PHP file, as a comment, in compressed base64 format. + * + * The file will be created if it doesn't exist. + * + * @param string $file File path. + * @param mixed $content Content to write. + * + * @return int|bool Number of bytes written or false if it fails. + * + * @throws IOException The destination file can't be written. + */ + public static function writeFlatDB($file, $content) + { + if (is_file($file) && !is_writeable($file)) { + // The datastore exists but is not writeable + throw new IOException($file); + } elseif (!is_file($file) && !is_writeable(dirname($file))) { + // The datastore does not exist and its parent directory is not writeable + throw new IOException(dirname($file)); + } + + return file_put_contents( + $file, + self::$phpPrefix . base64_encode(gzdeflate(serialize($content))) . self::$phpSuffix + ); + } + + /** + * Read data from a file containing Shaarli database format content. + * + * If the file isn't readable or doesn't exist, default data will be returned. + * + * @param string $file File path. + * @param mixed $default The default value to return if the file isn't readable. + * + * @return mixed The content unserialized, or default if the file isn't readable, or false if it fails. + */ + public static function readFlatDB($file, $default = null) + { + // Note that gzinflate is faster than gzuncompress. + // See: http://www.php.net/manual/en/function.gzdeflate.php#96439 + if (!is_readable($file)) { + return $default; + } + + $data = file_get_contents($file); + if ($data == '') { + return $default; + } + + return unserialize( + gzinflate( + base64_decode( + substr($data, strlen(self::$phpPrefix), -strlen(self::$phpSuffix)) + ) + ) + ); + } + + /** + * 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(__FILE__)); + + return strpos(realpath($path), $rootDirectory) !== false; + } +} diff --git a/application/legacy/LegacyLinkDB.php b/application/legacy/LegacyLinkDB.php index 7bf76fd4..5c02a21b 100644 --- a/application/legacy/LegacyLinkDB.php +++ b/application/legacy/LegacyLinkDB.php @@ -8,7 +8,7 @@ use DateTime; use Iterator; use Shaarli\Bookmark\Exception\BookmarkNotFoundException; use Shaarli\Exceptions\IOException; -use Shaarli\FileUtils; +use Shaarli\Helper\FileUtils; use Shaarli\Render\PageCacheManager; /** diff --git a/application/legacy/LegacyUpdater.php b/application/legacy/LegacyUpdater.php index 0ab3a55b..fe1a286f 100644 --- a/application/legacy/LegacyUpdater.php +++ b/application/legacy/LegacyUpdater.php @@ -7,7 +7,6 @@ use RainTPL; use ReflectionClass; use ReflectionException; use ReflectionMethod; -use Shaarli\ApplicationUtils; use Shaarli\Bookmark\Bookmark; use Shaarli\Bookmark\BookmarkArray; use Shaarli\Bookmark\BookmarkFilter; @@ -17,6 +16,7 @@ use Shaarli\Config\ConfigJson; use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigPhp; use Shaarli\Exceptions\IOException; +use Shaarli\Helper\ApplicationUtils; use Shaarli\Thumbnailer; use Shaarli\Updater\Exception\UpdaterException; diff --git a/application/render/PageBuilder.php b/application/render/PageBuilder.php index 512bb79e..25e0e284 100644 --- a/application/render/PageBuilder.php +++ b/application/render/PageBuilder.php @@ -5,9 +5,9 @@ namespace Shaarli\Render; 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; diff --git a/application/security/BanManager.php b/application/security/BanManager.php index f72c8b7b..288cbde0 100644 --- a/application/security/BanManager.php +++ b/application/security/BanManager.php @@ -4,7 +4,7 @@ namespace Shaarli\Security; use Psr\Log\LoggerInterface; -use Shaarli\FileUtils; +use Shaarli\Helper\FileUtils; /** * Class BanManager -- cgit v1.2.3 From 36e6d88dbfd753665224664d5214f39ccfbbf6a5 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Fri, 16 Oct 2020 11:50:53 +0200 Subject: Feature: add weekly and monthly view/RSS feed for daily page - Heavy refactoring of DailyController - Add a banner like in tag cloud to display monthly and weekly links - Translations: t() now supports variables with optional first letter uppercase Fixes #160 --- application/Utils.php | 33 +++- application/bookmark/BookmarkFileService.php | 38 ++-- application/bookmark/BookmarkServiceInterface.php | 27 ++- .../front/controller/visitor/DailyController.php | 105 ++++++----- application/helper/DailyPageHelper.php | 208 +++++++++++++++++++++ 5 files changed, 338 insertions(+), 73 deletions(-) create mode 100644 application/helper/DailyPageHelper.php (limited to 'application') diff --git a/application/Utils.php b/application/Utils.php index bc1c9f5d..db046893 100644 --- a/application/Utils.php +++ b/application/Utils.php @@ -326,6 +326,23 @@ function format_date($date, $time = true, $intl = true) 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. * @@ -454,16 +471,20 @@ function alphabetical_sort(&$data, $reverse = false, $byKeys = false) * 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)); } /** diff --git a/application/bookmark/BookmarkFileService.php b/application/bookmark/BookmarkFileService.php index 14b3d620..0df2f47f 100644 --- a/application/bookmark/BookmarkFileService.php +++ b/application/bookmark/BookmarkFileService.php @@ -343,26 +343,42 @@ class BookmarkFileService implements BookmarkServiceInterface /** * @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; } /** diff --git a/application/bookmark/BookmarkServiceInterface.php b/application/bookmark/BookmarkServiceInterface.php index 9fa61533..08cdbb4e 100644 --- a/application/bookmark/BookmarkServiceInterface.php +++ b/application/bookmark/BookmarkServiceInterface.php @@ -156,22 +156,29 @@ interface BookmarkServiceInterface 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. diff --git a/application/front/controller/visitor/DailyController.php b/application/front/controller/visitor/DailyController.php index 07617cf1..728bc2d8 100644 --- a/application/front/controller/visitor/DailyController.php +++ b/application/front/controller/visitor/DailyController.php @@ -5,8 +5,8 @@ declare(strict_types=1); 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; @@ -26,32 +26,20 @@ class DailyController extends ShaarliVisitorController */ 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); @@ -63,13 +51,15 @@ class DailyController extends ShaarliVisitorController $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. @@ -82,7 +72,7 @@ class DailyController extends ShaarliVisitorController $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)); @@ -106,11 +96,14 @@ class DailyController extends ShaarliVisitorController } $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; } @@ -127,12 +120,19 @@ class DailyController extends ShaarliVisitorController /** @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' => [], ]; @@ -141,16 +141,20 @@ class DailyController extends ShaarliVisitorController // 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); @@ -189,4 +193,13 @@ class DailyController extends ShaarliVisitorController return $columns; } + + protected function translateType($type): string + { + return [ + t('day') => t('Daily'), + t('week') => t('Weekly'), + t('month') => t('Monthly'), + ][t($type)] ?? t('Daily'); + } } diff --git a/application/helper/DailyPageHelper.php b/application/helper/DailyPageHelper.php new file mode 100644 index 00000000..5fabc907 --- /dev/null +++ b/application/helper/DailyPageHelper.php @@ -0,0 +1,208 @@ +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 () + * - week: 202041 () + * - month: 202010 () + * + * @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'); + } + } +} -- cgit v1.2.3 From 54afb1d6f65f727b20b66582bb63a42c421eea4d Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Tue, 27 Oct 2020 19:55:29 +0100 Subject: Fix rebase issue --- application/front/controller/admin/ServerController.php | 4 ++-- application/helper/FileUtils.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'application') diff --git a/application/front/controller/admin/ServerController.php b/application/front/controller/admin/ServerController.php index 85654a43..bfc99422 100644 --- a/application/front/controller/admin/ServerController.php +++ b/application/front/controller/admin/ServerController.php @@ -4,8 +4,8 @@ declare(strict_types=1); namespace Shaarli\Front\Controller\Admin; -use Shaarli\ApplicationUtils; -use Shaarli\FileUtils; +use Shaarli\Helper\ApplicationUtils; +use Shaarli\Helper\FileUtils; use Slim\Http\Request; use Slim\Http\Response; diff --git a/application/helper/FileUtils.php b/application/helper/FileUtils.php index 2d50d850..2eac0793 100644 --- a/application/helper/FileUtils.php +++ b/application/helper/FileUtils.php @@ -133,7 +133,7 @@ class FileUtils */ public static function isPathInShaarliFolder(string $path): bool { - $rootDirectory = dirname(dirname(__FILE__)); + $rootDirectory = dirname(dirname(dirname(__FILE__))); return strpos(realpath($path), $rootDirectory) !== false; } -- cgit v1.2.3 From 5d8de7587d67b5c3e5d1fed8562d9b87ecde80c1 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sat, 10 Oct 2020 17:40:26 +0200 Subject: Feature: bulk creation of bookmarks This changes creates a new form in addlink page allowing to create multiple bookmarks at once more easily. It focuses on re-using as much existing code and template component as possible. These changes includes: - a new form in addlink (hidden behind a button by default), containing a text area for URL, and tags/private status to apply to created links - this form displays a new template called editlink.batch, itself including editlink template multiple times - User interation in this new templates are handle by a new JS script (shaare-batch.js) making AJAX requests, and therefore does not need page reloading - ManageShaareController has been split into 3 distinct controllers: + ShaareAdd: displays addlink template + ShaareManage: various operation applied on existing shaares (change visibility, pin, deletion, etc.) + ShaarePublish: handles creation/edit forms and saving Shaare's form - Updated translations Fixes #137 --- .../controller/admin/ManageShaareController.php | 386 --------------------- .../front/controller/admin/ShaareAddController.php | 34 ++ .../controller/admin/ShaareManageController.php | 202 +++++++++++ .../controller/admin/ShaarePublishController.php | 222 ++++++++++++ application/render/TemplatePage.php | 1 + 5 files changed, 459 insertions(+), 386 deletions(-) delete mode 100644 application/front/controller/admin/ManageShaareController.php create mode 100644 application/front/controller/admin/ShaareAddController.php create mode 100644 application/front/controller/admin/ShaareManageController.php create mode 100644 application/front/controller/admin/ShaarePublishController.php (limited to 'application') diff --git a/application/front/controller/admin/ManageShaareController.php b/application/front/controller/admin/ManageShaareController.php deleted file mode 100644 index e490f85a..00000000 --- a/application/front/controller/admin/ManageShaareController.php +++ /dev/null @@ -1,386 +0,0 @@ -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(''); - } - - 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(''); - } - - // 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') - ); - } - - /** - * 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)); - } -} diff --git a/application/front/controller/admin/ShaareAddController.php b/application/front/controller/admin/ShaareAddController.php new file mode 100644 index 00000000..8dc386b2 --- /dev/null +++ b/application/front/controller/admin/ShaareAddController.php @@ -0,0 +1,34 @@ +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)); + } +} diff --git a/application/front/controller/admin/ShaareManageController.php b/application/front/controller/admin/ShaareManageController.php new file mode 100644 index 00000000..7ceb8d8a --- /dev/null +++ b/application/front/controller/admin/ShaareManageController.php @@ -0,0 +1,202 @@ +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(''); + } + + // 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') + ); + } +} diff --git a/application/front/controller/admin/ShaarePublishController.php b/application/front/controller/admin/ShaarePublishController.php new file mode 100644 index 00000000..608f79cf --- /dev/null +++ b/application/front/controller/admin/ShaarePublishController.php @@ -0,0 +1,222 @@ +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) { + $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->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(''); + } elseif ($request->getParam('source') === 'batch') { + return $response; + } + + 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() + ); + } + + /** + * 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'); + $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: ')); + } + + return [ + 'title' => $title ?? $metadata['title'] ?? '', + 'url' => $url ?? '', + 'description' => $description ?? $metadata['description'] ?? '', + 'tags' => $tags ?? $metadata['tags'] ?? '', + 'private' => $private, + 'linkIsNew' => true, + ]; + } + + $formatter = $this->container->formatterFactory->getFormatter('raw'); + $link = $formatter->format($bookmark); + $link['linkIsNew'] = false; + + return $link; + } + + protected function buildFormData(array $link, bool $isNew, Request $request): array + { + $tags = $this->container->bookmarkService->bookmarksCountPerTag(); + if ($this->container->conf->get('formatter') === 'markdown') { + $tags[BookmarkMarkdownFormatter::NO_MD_TAG] = 1; + } + + return 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), + ]); + } +} diff --git a/application/render/TemplatePage.php b/application/render/TemplatePage.php index 8af8228a..03b424f3 100644 --- a/application/render/TemplatePage.php +++ b/application/render/TemplatePage.php @@ -14,6 +14,7 @@ interface TemplatePage 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'; -- cgit v1.2.3 From 25e90d8d75382721ff7473fa1686090fcfeb46ff Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sun, 11 Oct 2020 13:34:38 +0200 Subject: Bulk creation: fix private status based on the first form --- application/front/controller/admin/ShaarePublishController.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'application') diff --git a/application/front/controller/admin/ShaarePublishController.php b/application/front/controller/admin/ShaarePublishController.php index 608f79cf..fd680ea0 100644 --- a/application/front/controller/admin/ShaarePublishController.php +++ b/application/front/controller/admin/ShaarePublishController.php @@ -169,7 +169,11 @@ class ShaarePublishController extends ShaarliAdminController $title = $request->getParam('title'); $description = $request->getParam('description'); $tags = $request->getParam('tags'); - $private = filter_var($request->getParam('private'), FILTER_VALIDATE_BOOLEAN); + 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.) -- cgit v1.2.3 From c609944cb906a2f5002cd86a808aa36d8deb2afd Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Fri, 23 Oct 2020 12:29:52 +0200 Subject: Bulk creation: improve performances using memoization Reduced additional processing time per links from ~40ms to ~5ms --- .../controller/admin/ShaarePublishController.php | 52 ++++++++++++++++++---- 1 file changed, 43 insertions(+), 9 deletions(-) (limited to 'application') diff --git a/application/front/controller/admin/ShaarePublishController.php b/application/front/controller/admin/ShaarePublishController.php index fd680ea0..65fdcdee 100644 --- a/application/front/controller/admin/ShaarePublishController.php +++ b/application/front/controller/admin/ShaarePublishController.php @@ -6,6 +6,7 @@ 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; @@ -14,6 +15,16 @@ 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. @@ -72,7 +83,7 @@ class ShaarePublishController extends ShaarliAdminController return $this->redirect($response, '/'); } - $formatter = $this->container->formatterFactory->getFormatter('raw'); + $formatter = $this->getFormatter('raw'); $link = $formatter->format($bookmark); return $this->displayForm($link, false, $request, $response); @@ -110,7 +121,7 @@ class ShaarePublishController extends ShaarliAdminController $this->container->bookmarkService->addOrSet($bookmark, false); // To preserve backward compatibility with 3rd parties, plugins still use arrays - $formatter = $this->container->formatterFactory->getFormatter('raw'); + $formatter = $this->getFormatter('raw'); $data = $formatter->format($bookmark); $this->executePageHooks('save_link', $data); @@ -198,7 +209,7 @@ class ShaarePublishController extends ShaarliAdminController ]; } - $formatter = $this->container->formatterFactory->getFormatter('raw'); + $formatter = $this->getFormatter('raw'); $link = $formatter->format($bookmark); $link['linkIsNew'] = false; @@ -207,20 +218,43 @@ class ShaarePublishController extends ShaarliAdminController protected function buildFormData(array $link, bool $isNew, Request $request): array { - $tags = $this->container->bookmarkService->bookmarksCountPerTag(); - if ($this->container->conf->get('formatter') === 'markdown') { - $tags[BookmarkMarkdownFormatter::NO_MD_TAG] = 1; - } - return escape([ 'link' => $link, 'link_is_new' => $isNew, 'http_referer' => $this->container->environment['HTTP_REFERER'] ?? '', 'source' => $request->getParam('source') ?? '', - 'tags' => $tags, + '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; + } } -- cgit v1.2.3 From 34c8f558e595d4f90e46e3753c8455b0b515771a Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Fri, 23 Oct 2020 13:28:02 +0200 Subject: Bulk creation: ignore blank lines --- application/front/controller/admin/ShaarePublishController.php | 3 +++ 1 file changed, 3 insertions(+) (limited to 'application') diff --git a/application/front/controller/admin/ShaarePublishController.php b/application/front/controller/admin/ShaarePublishController.php index 65fdcdee..ddcffdc7 100644 --- a/application/front/controller/admin/ShaarePublishController.php +++ b/application/front/controller/admin/ShaarePublishController.php @@ -46,6 +46,9 @@ class ShaarePublishController extends ShaarliAdminController $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(); -- cgit v1.2.3 From 156061d445fd23d033a52f84954484a3349c988a Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Wed, 28 Oct 2020 12:54:52 +0100 Subject: Raise 404 error instead of 500 if permalink access is denied --- application/bookmark/BookmarkFileService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'application') diff --git a/application/bookmark/BookmarkFileService.php b/application/bookmark/BookmarkFileService.php index 0df2f47f..3ea98a45 100644 --- a/application/bookmark/BookmarkFileService.php +++ b/application/bookmark/BookmarkFileService.php @@ -106,7 +106,7 @@ class BookmarkFileService implements BookmarkServiceInterface && $first->isPrivate() && (empty($privateKey) || $privateKey !== $first->getAdditionalContentEntry('private_key')) ) { - throw new Exception('Not authorized'); + throw new BookmarkNotFoundException(); } return $first; -- cgit v1.2.3 From d3f6d525253eb7bb041d9436cbf213c10524a85c Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Wed, 28 Oct 2020 14:02:08 +0100 Subject: Fix compatiliby issue on login with PHP 7.1 session_set_cookie_params does not return any value in PHP 7.1 --- application/render/PageBuilder.php | 2 +- application/security/SessionManager.php | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) (limited to 'application') diff --git a/application/render/PageBuilder.php b/application/render/PageBuilder.php index 25e0e284..c2fae705 100644 --- a/application/render/PageBuilder.php +++ b/application/render/PageBuilder.php @@ -160,7 +160,7 @@ class PageBuilder $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); diff --git a/application/security/SessionManager.php b/application/security/SessionManager.php index 36df8c1c..96bf193c 100644 --- a/application/security/SessionManager.php +++ b/application/security/SessionManager.php @@ -293,9 +293,12 @@ class SessionManager 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 -- cgit v1.2.3 From 114a43b20e9a1f83647d4f0f7a001e80a76c75ce Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Wed, 28 Oct 2020 14:13:50 +0100 Subject: Remove unnecessary escape of referer Fixes #1611 --- application/front/controller/admin/ShaarePublishController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'application') diff --git a/application/front/controller/admin/ShaarePublishController.php b/application/front/controller/admin/ShaarePublishController.php index ddcffdc7..18afc2d1 100644 --- a/application/front/controller/admin/ShaarePublishController.php +++ b/application/front/controller/admin/ShaarePublishController.php @@ -139,7 +139,7 @@ class ShaarePublishController extends ShaarliAdminController } if (!empty($request->getParam('returnurl'))) { - $this->container->environment['HTTP_REFERER'] = escape($request->getParam('returnurl')); + $this->container->environment['HTTP_REFERER'] = $request->getParam('returnurl'); } return $this->redirectFromReferer( -- cgit v1.2.3 From b37ca790729125fa0df956220a4062f1d34c57e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Carr?= Date: Wed, 28 Oct 2020 19:57:40 -0700 Subject: postLink: change relative path to absolute path --- application/api/controllers/Links.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'application') diff --git a/application/api/controllers/Links.php b/application/api/controllers/Links.php index 29247950..16fc8688 100644 --- a/application/api/controllers/Links.php +++ b/application/api/controllers/Links.php @@ -130,7 +130,7 @@ class Links extends ApiController $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); } -- cgit v1.2.3 From 740b32b520e6b1723512c6f9b78cef6575b1725b Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Tue, 3 Nov 2020 12:38:38 +0100 Subject: Default formatter: add a setting to disable auto-linkification + update documentation + single parameter for both URL and hashtags Fixes #1094 --- application/bookmark/LinkUtils.php | 11 ++++++++--- application/formatter/BookmarkDefaultFormatter.php | 7 ++++++- 2 files changed, 14 insertions(+), 4 deletions(-) (limited to 'application') diff --git a/application/bookmark/LinkUtils.php b/application/bookmark/LinkUtils.php index faf5dbfd..17c37979 100644 --- a/application/bookmark/LinkUtils.php +++ b/application/bookmark/LinkUtils.php @@ -138,12 +138,17 @@ function space2nbsp($text) * * @param string $description shaare's description. * @param string $indexUrl URL to Shaarli's index. - + * @param bool $autolink Turn on/off automatic linkifications of URLs and hashtags + * * @return string formatted description. */ -function format_description($description, $indexUrl = '') +function format_description($description, $indexUrl = '', $autolink = true) { - return nl2br(space2nbsp(hashtag_autolink(text2clickable($description), $indexUrl))); + if ($autolink) { + $description = hashtag_autolink(text2clickable($description), $indexUrl); + } + + return nl2br(space2nbsp($description)); } /** diff --git a/application/formatter/BookmarkDefaultFormatter.php b/application/formatter/BookmarkDefaultFormatter.php index d58a5e39..149a3eb9 100644 --- a/application/formatter/BookmarkDefaultFormatter.php +++ b/application/formatter/BookmarkDefaultFormatter.php @@ -46,8 +46,13 @@ class BookmarkDefaultFormatter extends BookmarkFormatter $bookmark->getDescription() ?? '', $bookmark->getAdditionalContentEntry('search_highlight')['description'] ?? [] ); + $description = format_description( + escape($description), + $indexUrl, + $this->conf->get('formatter_settings.autolink', true) + ); - return $this->replaceTokens(format_description(escape($description), $indexUrl)); + return $this->replaceTokens($description); } /** -- cgit v1.2.3 From 330ac859fb13a3a15875f185a611bfaa6c5f5587 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Thu, 5 Nov 2020 16:14:22 +0100 Subject: Fix: redirect to referrer after bookmark deletion Except if the referer points to a permalink (which has been deleted). Fixes #1622 --- application/front/controller/admin/ShaareManageController.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'application') diff --git a/application/front/controller/admin/ShaareManageController.php b/application/front/controller/admin/ShaareManageController.php index 7ceb8d8a..2ed298f5 100644 --- a/application/front/controller/admin/ShaareManageController.php +++ b/application/front/controller/admin/ShaareManageController.php @@ -66,8 +66,8 @@ class ShaareManageController extends ShaarliAdminController return $response->write(''); } - // Don't redirect to where we were previously because the datastore has changed. - return $this->redirect($response, '/'); + // Don't redirect to permalink after deletion. + return $this->redirectFromReferer($request, $response, ['shaare/']); } /** -- cgit v1.2.3 From b3bd8c3e8d367975980043e772f7cd78b7f96bc6 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Thu, 22 Oct 2020 16:21:03 +0200 Subject: Feature: support any tag separator So it allows to have multiple words tags. Breaking change: commas ',' are no longer a default separator. Fixes #594 --- application/bookmark/Bookmark.php | 39 +++++++++--------- application/bookmark/BookmarkFileService.php | 2 +- application/bookmark/BookmarkFilter.php | 47 ++++++++++++++-------- application/bookmark/LinkUtils.php | 46 +++++++++++++++++++++ application/config/ConfigManager.php | 1 + application/formatter/BookmarkDefaultFormatter.php | 7 ++-- application/formatter/BookmarkFormatter.php | 3 +- .../front/controller/admin/ManageTagController.php | 33 +++++++++++++++ .../controller/admin/ShaareManageController.php | 4 +- .../controller/admin/ShaarePublishController.php | 12 +++++- .../controller/visitor/BookmarkListController.php | 11 +++-- .../controller/visitor/TagCloudController.php | 10 +++-- .../front/controller/visitor/TagController.php | 10 +++-- application/http/HttpAccess.php | 6 ++- application/http/HttpUtils.php | 12 +++--- application/http/MetadataRetriever.php | 4 +- application/legacy/LegacyUpdater.php | 2 +- application/netscape/NetscapeBookmarkUtils.php | 10 ++--- application/render/PageBuilder.php | 1 + 19 files changed, 191 insertions(+), 69 deletions(-) (limited to 'application') diff --git a/application/bookmark/Bookmark.php b/application/bookmark/Bookmark.php index 4810c5e6..8aaeb9d8 100644 --- a/application/bookmark/Bookmark.php +++ b/application/bookmark/Bookmark.php @@ -60,11 +60,13 @@ class Bookmark /** * Initialize a link from array data. Especially useful to create a Bookmark from former link storage format. * - * @param array $data + * @param array $data + * @param string $tagsSeparator Tags separator loaded from the config file. + * This is a context data, and it should *never* be stored in the Bookmark object. * * @return $this */ - public function fromArray(array $data): Bookmark + public function fromArray(array $data, string $tagsSeparator = ' '): Bookmark { $this->id = $data['id'] ?? null; $this->shortUrl = $data['shorturl'] ?? null; @@ -77,7 +79,7 @@ class Bookmark if (is_array($data['tags'])) { $this->tags = $data['tags']; } else { - $this->tags = preg_split('/\s+/', $data['tags'] ?? '', -1, PREG_SPLIT_NO_EMPTY); + $this->tags = tags_str2array($data['tags'] ?? '', $tagsSeparator); } if (! empty($data['updated'])) { $this->updated = $data['updated']; @@ -348,7 +350,12 @@ class Bookmark */ public function setTags(?array $tags): Bookmark { - $this->setTagsString(implode(' ', $tags ?? [])); + $this->tags = array_map( + function (string $tag): string { + return $tag[0] === '-' ? substr($tag, 1) : $tag; + }, + tags_filter($tags, ' ') + ); return $this; } @@ -420,11 +427,13 @@ class Bookmark } /** - * @return string Bookmark's tags as a string, separated by a space + * @param string $separator Tags separator loaded from the config file. + * + * @return string Bookmark's tags as a string, separated by a separator */ - public function getTagsString(): string + public function getTagsString(string $separator = ' '): string { - return implode(' ', $this->getTags()); + return tags_array2str($this->getTags(), $separator); } /** @@ -444,19 +453,13 @@ class Bookmark * - trailing dash in tags will be removed * * @param string|null $tags + * @param string $separator Tags separator loaded from the config file. * * @return $this */ - public function setTagsString(?string $tags): Bookmark + public function setTagsString(?string $tags, string $separator = ' '): Bookmark { - // Remove first '-' char in tags. - $tags = preg_replace('/(^| )\-/', '$1', $tags ?? ''); - // Explode all tags separted by spaces or commas - $tags = preg_split('/[\s,]+/', $tags); - // Remove eventual empty values - $tags = array_values(array_filter($tags)); - - $this->tags = $tags; + $this->setTags(tags_str2array($tags, $separator)); return $this; } @@ -507,7 +510,7 @@ class Bookmark */ public function renameTag(string $fromTag, string $toTag): void { - if (($pos = array_search($fromTag, $this->tags)) !== false) { + if (($pos = array_search($fromTag, $this->tags ?? [])) !== false) { $this->tags[$pos] = trim($toTag); } } @@ -519,7 +522,7 @@ class Bookmark */ public function deleteTag(string $tag): void { - if (($pos = array_search($tag, $this->tags)) !== false) { + if (($pos = array_search($tag, $this->tags ?? [])) !== false) { unset($this->tags[$pos]); $this->tags = array_values($this->tags); } diff --git a/application/bookmark/BookmarkFileService.php b/application/bookmark/BookmarkFileService.php index 3ea98a45..85efeea6 100644 --- a/application/bookmark/BookmarkFileService.php +++ b/application/bookmark/BookmarkFileService.php @@ -91,7 +91,7 @@ class BookmarkFileService implements BookmarkServiceInterface } } - $this->bookmarkFilter = new BookmarkFilter($this->bookmarks); + $this->bookmarkFilter = new BookmarkFilter($this->bookmarks, $this->conf); } /** diff --git a/application/bookmark/BookmarkFilter.php b/application/bookmark/BookmarkFilter.php index c79386ea..5d8733dc 100644 --- a/application/bookmark/BookmarkFilter.php +++ b/application/bookmark/BookmarkFilter.php @@ -6,6 +6,7 @@ namespace Shaarli\Bookmark; use Exception; use Shaarli\Bookmark\Exception\BookmarkNotFoundException; +use Shaarli\Config\ConfigManager; /** * Class LinkFilter. @@ -58,12 +59,16 @@ class BookmarkFilter */ private $bookmarks; + /** @var ConfigManager */ + protected $conf; + /** * @param Bookmark[] $bookmarks initialization. */ - public function __construct($bookmarks) + public function __construct($bookmarks, ConfigManager $conf) { $this->bookmarks = $bookmarks; + $this->conf = $conf; } /** @@ -107,10 +112,14 @@ class BookmarkFilter $filtered = $this->bookmarks; } if (!empty($request[0])) { - $filtered = (new BookmarkFilter($filtered))->filterTags($request[0], $casesensitive, $visibility); + $filtered = (new BookmarkFilter($filtered, $this->conf)) + ->filterTags($request[0], $casesensitive, $visibility) + ; } if (!empty($request[1])) { - $filtered = (new BookmarkFilter($filtered))->filterFulltext($request[1], $visibility); + $filtered = (new BookmarkFilter($filtered, $this->conf)) + ->filterFulltext($request[1], $visibility) + ; } return $filtered; case self::$FILTER_TEXT: @@ -280,8 +289,9 @@ class BookmarkFilter * * @return string generated regex fragment */ - private static function tag2regex(string $tag): string + protected function tag2regex(string $tag): string { + $tagsSeparator = $this->conf->get('general.tags_separator', ' '); $len = strlen($tag); if (!$len || $tag === "-" || $tag === "*") { // nothing to search, return empty regex @@ -295,12 +305,13 @@ class BookmarkFilter $i = 0; // start at first character $regex = '(?='; // use positive lookahead } - $regex .= '.*(?:^| )'; // before tag may only be a space or the beginning + // before tag may only be the separator or the beginning + $regex .= '.*(?:^|' . $tagsSeparator . ')'; // iterate over string, separating it into placeholder and content for (; $i < $len; $i++) { if ($tag[$i] === '*') { // placeholder found - $regex .= '[^ ]*?'; + $regex .= '[^' . $tagsSeparator . ']*?'; } else { // regular characters $offset = strpos($tag, '*', $i); @@ -316,7 +327,8 @@ class BookmarkFilter $i = $offset; } } - $regex .= '(?:$| ))'; // after the tag may only be a space or the end + // after the tag may only be the separator or the end + $regex .= '(?:$|' . $tagsSeparator . '))'; return $regex; } @@ -334,14 +346,15 @@ class BookmarkFilter */ public function filterTags($tags, bool $casesensitive = false, string $visibility = 'all') { + $tagsSeparator = $this->conf->get('general.tags_separator', ' '); // get single tags (we may get passed an array, even though the docs say different) $inputTags = $tags; if (!is_array($tags)) { // we got an input string, split tags - $inputTags = preg_split('/(?:\s+)|,/', $inputTags, -1, PREG_SPLIT_NO_EMPTY); + $inputTags = tags_str2array($inputTags, $tagsSeparator); } - if (!count($inputTags)) { + if (count($inputTags) === 0) { // no input tags return $this->noFilter($visibility); } @@ -358,7 +371,7 @@ class BookmarkFilter } // build regex from all tags - $re = '/^' . implode(array_map("self::tag2regex", $inputTags)) . '.*$/'; + $re = '/^' . implode(array_map([$this, 'tag2regex'], $inputTags)) . '.*$/'; if (!$casesensitive) { // make regex case insensitive $re .= 'i'; @@ -378,7 +391,8 @@ class BookmarkFilter continue; } } - $search = $link->getTagsString(); // build search string, start with tags of current link + // build search string, start with tags of current link + $search = $link->getTagsString($tagsSeparator); if (strlen(trim($link->getDescription())) && strpos($link->getDescription(), '#') !== false) { // description given and at least one possible tag found $descTags = array(); @@ -390,9 +404,9 @@ class BookmarkFilter ); if (count($descTags[1])) { // there were some tags in the description, add them to the search string - $search .= ' ' . implode(' ', $descTags[1]); + $search .= $tagsSeparator . tags_array2str($descTags[1], $tagsSeparator); } - }; + } // match regular expression with search string if (!preg_match($re, $search)) { // this entry does _not_ match our regex @@ -422,7 +436,7 @@ class BookmarkFilter } } - if (empty(trim($link->getTagsString()))) { + if (empty($link->getTags())) { $filtered[$key] = $link; } } @@ -537,10 +551,11 @@ class BookmarkFilter */ protected function buildFullTextSearchableLink(Bookmark $link, array &$lengths): string { + $tagString = $link->getTagsString($this->conf->get('general.tags_separator', ' ')); $content = mb_convert_case($link->getTitle(), MB_CASE_LOWER, 'UTF-8') .'\\'; $content .= mb_convert_case($link->getDescription(), MB_CASE_LOWER, 'UTF-8') .'\\'; $content .= mb_convert_case($link->getUrl(), MB_CASE_LOWER, 'UTF-8') .'\\'; - $content .= mb_convert_case($link->getTagsString(), MB_CASE_LOWER, 'UTF-8') .'\\'; + $content .= mb_convert_case($tagString, MB_CASE_LOWER, 'UTF-8') .'\\'; $lengths['title'] = ['start' => 0, 'end' => mb_strlen($link->getTitle())]; $nextField = $lengths['title']['end'] + 1; @@ -548,7 +563,7 @@ class BookmarkFilter $nextField = $lengths['description']['end'] + 1; $lengths['url'] = ['start' => $nextField, 'end' => $nextField + mb_strlen($link->getUrl())]; $nextField = $lengths['url']['end'] + 1; - $lengths['tags'] = ['start' => $nextField, 'end' => $nextField + mb_strlen($link->getTagsString())]; + $lengths['tags'] = ['start' => $nextField, 'end' => $nextField + mb_strlen($tagString)]; return $content; } diff --git a/application/bookmark/LinkUtils.php b/application/bookmark/LinkUtils.php index 17c37979..9493b0aa 100644 --- a/application/bookmark/LinkUtils.php +++ b/application/bookmark/LinkUtils.php @@ -176,3 +176,49 @@ function is_note($linkUrl) { return isset($linkUrl[0]) && $linkUrl[0] === '?'; } + +/** + * Extract an array of tags from a given tag string, with provided separator. + * + * @param string|null $tags String containing a list of tags separated by $separator. + * @param string $separator Shaarli's default: ' ' (whitespace) + * + * @return array List of tags + */ +function tags_str2array(?string $tags, string $separator): array +{ + // For whitespaces, we use the special \s regex character + $separator = $separator === ' ' ? '\s' : $separator; + + return preg_split('/\s*' . $separator . '+\s*/', trim($tags) ?? '', -1, PREG_SPLIT_NO_EMPTY); +} + +/** + * Return a tag string with provided separator from a list of tags. + * Note that given array is clean up by tags_filter(). + * + * @param array|null $tags List of tags + * @param string $separator + * + * @return string + */ +function tags_array2str(?array $tags, string $separator): string +{ + return implode($separator, tags_filter($tags, $separator)); +} + +/** + * Clean an array of tags: trim + remove empty entries + * + * @param array|null $tags List of tags + * @param string $separator + * + * @return array + */ +function tags_filter(?array $tags, string $separator): array +{ + $trimDefault = " \t\n\r\0\x0B"; + return array_values(array_filter(array_map(function (string $entry) use ($separator, $trimDefault): string { + return trim($entry, $trimDefault . $separator); + }, $tags ?? []))); +} diff --git a/application/config/ConfigManager.php b/application/config/ConfigManager.php index fb085023..a035baae 100644 --- a/application/config/ConfigManager.php +++ b/application/config/ConfigManager.php @@ -368,6 +368,7 @@ class ConfigManager $this->setEmpty('general.default_note_title', 'Note: '); $this->setEmpty('general.retrieve_description', true); $this->setEmpty('general.enable_async_metadata', true); + $this->setEmpty('general.tags_separator', ' '); $this->setEmpty('updates.check_updates', false); $this->setEmpty('updates.check_updates_branch', 'stable'); diff --git a/application/formatter/BookmarkDefaultFormatter.php b/application/formatter/BookmarkDefaultFormatter.php index 149a3eb9..51bea0f1 100644 --- a/application/formatter/BookmarkDefaultFormatter.php +++ b/application/formatter/BookmarkDefaultFormatter.php @@ -68,15 +68,16 @@ class BookmarkDefaultFormatter extends BookmarkFormatter */ protected function formatTagListHtml($bookmark) { + $tagsSeparator = $this->conf->get('general.tags_separator', ' '); if (empty($bookmark->getAdditionalContentEntry('search_highlight')['tags'])) { return $this->formatTagList($bookmark); } $tags = $this->tokenizeSearchHighlightField( - $bookmark->getTagsString(), + $bookmark->getTagsString($tagsSeparator), $bookmark->getAdditionalContentEntry('search_highlight')['tags'] ); - $tags = $this->filterTagList(explode(' ', $tags)); + $tags = $this->filterTagList(tags_str2array($tags, $tagsSeparator)); $tags = escape($tags); $tags = $this->replaceTokensArray($tags); @@ -88,7 +89,7 @@ class BookmarkDefaultFormatter extends BookmarkFormatter */ protected function formatTagString($bookmark) { - return implode(' ', $this->formatTagList($bookmark)); + return implode($this->conf->get('general.tags_separator'), $this->formatTagList($bookmark)); } /** diff --git a/application/formatter/BookmarkFormatter.php b/application/formatter/BookmarkFormatter.php index e1b7f705..124ce78b 100644 --- a/application/formatter/BookmarkFormatter.php +++ b/application/formatter/BookmarkFormatter.php @@ -267,7 +267,7 @@ abstract class BookmarkFormatter */ protected function formatTagString($bookmark) { - return implode(' ', $this->formatTagList($bookmark)); + return implode($this->conf->get('general.tags_separator', ' '), $this->formatTagList($bookmark)); } /** @@ -351,6 +351,7 @@ abstract class BookmarkFormatter /** * Format tag list, e.g. remove private tags if the user is not logged in. + * TODO: this method is called multiple time to format tags, the result should be cached. * * @param array $tags * diff --git a/application/front/controller/admin/ManageTagController.php b/application/front/controller/admin/ManageTagController.php index 2065c3e2..22fb461c 100644 --- a/application/front/controller/admin/ManageTagController.php +++ b/application/front/controller/admin/ManageTagController.php @@ -24,6 +24,12 @@ class ManageTagController extends ShaarliAdminController $fromTag = $request->getParam('fromtag') ?? ''; $this->assignView('fromtag', escape($fromTag)); + $separator = escape($this->container->conf->get('general.tags_separator', ' ')); + if ($separator === ' ') { + $separator = ' '; + $this->assignView('tags_separator_desc', t('whitespace')); + } + $this->assignView('tags_separator', $separator); $this->assignView( 'pagetitle', t('Manage tags') .' - '. $this->container->conf->get('general.title', 'Shaarli') @@ -85,4 +91,31 @@ class ManageTagController extends ShaarliAdminController return $this->redirect($response, $redirect); } + + /** + * POST /admin/tags/change-separator - Change tag separator + */ + public function changeSeparator(Request $request, Response $response): Response + { + $this->checkToken($request); + + $reservedCharacters = ['-', '.', '*']; + $newSeparator = $request->getParam('separator'); + if ($newSeparator === null || mb_strlen($newSeparator) !== 1) { + $this->saveErrorMessage(t('Tags separator must be a single character.')); + } elseif (in_array($newSeparator, $reservedCharacters, true)) { + $reservedCharacters = implode(' ', array_map(function (string $character) { + return '' . $character . ''; + }, $reservedCharacters)); + $this->saveErrorMessage( + t('These characters are reserved and can\'t be used as tags separator: ') . $reservedCharacters + ); + } else { + $this->container->conf->set('general.tags_separator', $newSeparator, true, true); + + $this->saveSuccessMessage('Your tags separator setting has been updated!'); + } + + return $this->redirect($response, '/admin/tags'); + } } diff --git a/application/front/controller/admin/ShaareManageController.php b/application/front/controller/admin/ShaareManageController.php index 2ed298f5..0b143172 100644 --- a/application/front/controller/admin/ShaareManageController.php +++ b/application/front/controller/admin/ShaareManageController.php @@ -125,7 +125,7 @@ class ShaareManageController extends ShaarliAdminController // To preserve backward compatibility with 3rd parties, plugins still use arrays $data = $formatter->format($bookmark); $this->executePageHooks('save_link', $data); - $bookmark->fromArray($data); + $bookmark->fromArray($data, $this->container->conf->get('general.tags_separator', ' ')); $this->container->bookmarkService->set($bookmark, false); ++$count; @@ -167,7 +167,7 @@ class ShaareManageController extends ShaarliAdminController // To preserve backward compatibility with 3rd parties, plugins still use arrays $data = $formatter->format($bookmark); $this->executePageHooks('save_link', $data); - $bookmark->fromArray($data); + $bookmark->fromArray($data, $this->container->conf->get('general.tags_separator', ' ')); $this->container->bookmarkService->set($bookmark); diff --git a/application/front/controller/admin/ShaarePublishController.php b/application/front/controller/admin/ShaarePublishController.php index 18afc2d1..625a5680 100644 --- a/application/front/controller/admin/ShaarePublishController.php +++ b/application/front/controller/admin/ShaarePublishController.php @@ -113,7 +113,10 @@ class ShaarePublishController extends ShaarliAdminController $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')); + $bookmark->setTagsString( + $request->getParam('lf_tags'), + $this->container->conf->get('general.tags_separator', ' ') + ); if ($this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE && true !== $this->container->conf->get('general.enable_async_metadata', true) @@ -128,7 +131,7 @@ class ShaarePublishController extends ShaarliAdminController $data = $formatter->format($bookmark); $this->executePageHooks('save_link', $data); - $bookmark->fromArray($data); + $bookmark->fromArray($data, $this->container->conf->get('general.tags_separator', ' ')); $this->container->bookmarkService->set($bookmark); // If we are called from the bookmarklet, we must close the popup: @@ -221,6 +224,11 @@ class ShaarePublishController extends ShaarliAdminController protected function buildFormData(array $link, bool $isNew, Request $request): array { + $link['tags'] = strlen($link['tags']) > 0 + ? $link['tags'] . $this->container->conf->get('general.tags_separator', ' ') + : $link['tags'] + ; + return escape([ 'link' => $link, 'link_is_new' => $isNew, diff --git a/application/front/controller/visitor/BookmarkListController.php b/application/front/controller/visitor/BookmarkListController.php index 78c474c9..cc3837ce 100644 --- a/application/front/controller/visitor/BookmarkListController.php +++ b/application/front/controller/visitor/BookmarkListController.php @@ -95,6 +95,10 @@ class BookmarkListController extends ShaarliVisitorController $next_page_url = '?page=' . ($page - 1) . $searchtermUrl . $searchtagsUrl; } + $tagsSeparator = $this->container->conf->get('general.tags_separator', ' '); + $searchTagsUrlEncoded = array_map('urlencode', tags_str2array($searchTags, $tagsSeparator)); + $searchTags = !empty($searchTags) ? trim($searchTags, $tagsSeparator) . $tagsSeparator : ''; + // Fill all template fields. $data = array_merge( $this->initializeTemplateVars(), @@ -106,7 +110,7 @@ class BookmarkListController extends ShaarliVisitorController 'result_count' => count($linksToDisplay), 'search_term' => escape($searchTerm), 'search_tags' => escape($searchTags), - 'search_tags_url' => array_map('urlencode', explode(' ', $searchTags)), + 'search_tags_url' => $searchTagsUrlEncoded, 'visibility' => $visibility, 'links' => $linkDisp, ] @@ -119,8 +123,9 @@ class BookmarkListController extends ShaarliVisitorController return '[' . $tag . ']'; }; $data['pagetitle'] .= ! empty($searchTags) - ? implode(' ', array_map($bracketWrap, preg_split('/\s+/', $searchTags))) . ' ' - : ''; + ? implode(' ', array_map($bracketWrap, tags_str2array($searchTags, $tagsSeparator))) . ' ' + : '' + ; $data['pagetitle'] .= '- '; } diff --git a/application/front/controller/visitor/TagCloudController.php b/application/front/controller/visitor/TagCloudController.php index 76ed7690..560cad08 100644 --- a/application/front/controller/visitor/TagCloudController.php +++ b/application/front/controller/visitor/TagCloudController.php @@ -47,13 +47,14 @@ class TagCloudController extends ShaarliVisitorController */ protected function processRequest(string $type, Request $request, Response $response): Response { + $tagsSeparator = $this->container->conf->get('general.tags_separator', ' '); if ($this->container->loginManager->isLoggedIn() === true) { $visibility = $this->container->sessionManager->getSessionParameter('visibility'); } $sort = $request->getQueryParam('sort'); $searchTags = $request->getQueryParam('searchtags'); - $filteringTags = $searchTags !== null ? explode(' ', $searchTags) : []; + $filteringTags = $searchTags !== null ? explode($tagsSeparator, $searchTags) : []; $tags = $this->container->bookmarkService->bookmarksCountPerTag($filteringTags, $visibility ?? null); @@ -71,8 +72,9 @@ class TagCloudController extends ShaarliVisitorController $tagsUrl[escape($tag)] = urlencode((string) $tag); } - $searchTags = implode(' ', escape($filteringTags)); - $searchTagsUrl = urlencode(implode(' ', $filteringTags)); + $searchTags = tags_array2str($filteringTags, $tagsSeparator); + $searchTags = !empty($searchTags) ? trim($searchTags, $tagsSeparator) . $tagsSeparator : ''; + $searchTagsUrl = urlencode($searchTags); $data = [ 'search_tags' => escape($searchTags), 'search_tags_url' => $searchTagsUrl, @@ -82,7 +84,7 @@ class TagCloudController extends ShaarliVisitorController $this->executePageHooks('render_tag' . $type, $data, 'tag.' . $type); $this->assignAllView($data); - $searchTags = !empty($searchTags) ? $searchTags .' - ' : ''; + $searchTags = !empty($searchTags) ? trim(str_replace($tagsSeparator, ' ', $searchTags)) .' - ' : ''; $this->assignView( 'pagetitle', $searchTags . t('Tag '. $type) .' - '. $this->container->conf->get('general.title', 'Shaarli') diff --git a/application/front/controller/visitor/TagController.php b/application/front/controller/visitor/TagController.php index de4e7ea2..7a3377a7 100644 --- a/application/front/controller/visitor/TagController.php +++ b/application/front/controller/visitor/TagController.php @@ -45,9 +45,10 @@ class TagController extends ShaarliVisitorController unset($params['addtag']); } + $tagsSeparator = $this->container->conf->get('general.tags_separator', ' '); // Check if this tag is already in the search query and ignore it if it is. // Each tag is always separated by a space - $currentTags = isset($params['searchtags']) ? explode(' ', $params['searchtags']) : []; + $currentTags = tags_str2array($params['searchtags'] ?? '', $tagsSeparator); $addtag = true; foreach ($currentTags as $value) { @@ -62,7 +63,7 @@ class TagController extends ShaarliVisitorController $currentTags[] = trim($newTag); } - $params['searchtags'] = trim(implode(' ', $currentTags)); + $params['searchtags'] = tags_array2str($currentTags, $tagsSeparator); // We also remove page (keeping the same page has no sense, since the results are different) unset($params['page']); @@ -98,10 +99,11 @@ class TagController extends ShaarliVisitorController } if (isset($params['searchtags'])) { - $tags = explode(' ', $params['searchtags']); + $tagsSeparator = $this->container->conf->get('general.tags_separator', ' '); + $tags = tags_str2array($params['searchtags'] ?? '', $tagsSeparator); // Remove value from array $tags. $tags = array_diff($tags, [$tagToRemove]); - $params['searchtags'] = implode(' ', $tags); + $params['searchtags'] = tags_array2str($tags, $tagsSeparator); if (empty($params['searchtags'])) { unset($params['searchtags']); diff --git a/application/http/HttpAccess.php b/application/http/HttpAccess.php index 646a5264..e80e0c01 100644 --- a/application/http/HttpAccess.php +++ b/application/http/HttpAccess.php @@ -29,14 +29,16 @@ class HttpAccess &$title, &$description, &$keywords, - $retrieveDescription + $retrieveDescription, + $tagsSeparator ) { return get_curl_download_callback( $charset, $title, $description, $keywords, - $retrieveDescription + $retrieveDescription, + $tagsSeparator ); } diff --git a/application/http/HttpUtils.php b/application/http/HttpUtils.php index 28c12969..ed1002b0 100644 --- a/application/http/HttpUtils.php +++ b/application/http/HttpUtils.php @@ -550,7 +550,8 @@ function get_curl_download_callback( &$title, &$description, &$keywords, - $retrieveDescription + $retrieveDescription, + $tagsSeparator ) { $currentChunk = 0; $foundChunk = null; @@ -568,6 +569,7 @@ function get_curl_download_callback( */ return function ($ch, $data) use ( $retrieveDescription, + $tagsSeparator, &$charset, &$title, &$description, @@ -598,10 +600,10 @@ function get_curl_download_callback( if (! empty($keywords)) { $foundChunk = $currentChunk; // Keywords use the format tag1, tag2 multiple words, tag - // So we format them to match Shaarli's separator and glue multiple words with '-' - $keywords = implode(' ', array_map(function($keyword) { - return implode('-', preg_split('/\s+/', trim($keyword))); - }, explode(',', $keywords))); + // So we split the result with `,`, then if a tag contains the separator we replace it by `-`. + $keywords = tags_array2str(array_map(function(string $keyword) use ($tagsSeparator): string { + return tags_array2str(tags_str2array($keyword, $tagsSeparator), '-'); + }, tags_str2array($keywords, ',')), $tagsSeparator); } } diff --git a/application/http/MetadataRetriever.php b/application/http/MetadataRetriever.php index ba9bd40c..2e1401ec 100644 --- a/application/http/MetadataRetriever.php +++ b/application/http/MetadataRetriever.php @@ -38,7 +38,6 @@ class MetadataRetriever $title = null; $description = null; $tags = null; - $retrieveDescription = $this->conf->get('general.retrieve_description'); // Short timeout to keep the application responsive // The callback will fill $charset and $title with data from the downloaded page. @@ -52,7 +51,8 @@ class MetadataRetriever $title, $description, $tags, - $retrieveDescription + $this->conf->get('general.retrieve_description'), + $this->conf->get('general.tags_separator', ' ') ) ); diff --git a/application/legacy/LegacyUpdater.php b/application/legacy/LegacyUpdater.php index fe1a286f..ed949b1e 100644 --- a/application/legacy/LegacyUpdater.php +++ b/application/legacy/LegacyUpdater.php @@ -585,7 +585,7 @@ class LegacyUpdater $linksArray = new BookmarkArray(); foreach ($this->linkDB as $key => $link) { - $linksArray[$key] = (new Bookmark())->fromArray($link); + $linksArray[$key] = (new Bookmark())->fromArray($link, $this->conf->get('general.tags_separator', ' ')); } $linksIo = new BookmarkIO($this->conf); $linksIo->write($linksArray); diff --git a/application/netscape/NetscapeBookmarkUtils.php b/application/netscape/NetscapeBookmarkUtils.php index b83f16f8..6ca728b7 100644 --- a/application/netscape/NetscapeBookmarkUtils.php +++ b/application/netscape/NetscapeBookmarkUtils.php @@ -101,11 +101,11 @@ class NetscapeBookmarkUtils // Add tags to all imported bookmarks? if (empty($post['default_tags'])) { - $defaultTags = array(); + $defaultTags = []; } else { - $defaultTags = preg_split( - '/[\s,]+/', - escape($post['default_tags']) + $defaultTags = tags_str2array( + escape($post['default_tags']), + $this->conf->get('general.tags_separator', ' ') ); } @@ -171,7 +171,7 @@ class NetscapeBookmarkUtils $link->setUrl($bkm['uri'], $this->conf->get('security.allowed_protocols')); $link->setDescription($bkm['note']); $link->setPrivate($private); - $link->setTagsString($bkm['tags']); + $link->setTags($bkm['tags']); $this->bookmarkService->addOrSet($link, false); $importCount++; diff --git a/application/render/PageBuilder.php b/application/render/PageBuilder.php index c2fae705..bf0ae326 100644 --- a/application/render/PageBuilder.php +++ b/application/render/PageBuilder.php @@ -161,6 +161,7 @@ class PageBuilder $this->tpl->assign('formatter', $this->conf->get('formatter', 'default')); $this->tpl->assign('links_per_page', $this->session['LINKS_PER_PAGE'] ?? 20); + $this->tpl->assign('tags_separator', $this->conf->get('general.tags_separator', ' ')); // To be removed with a proper theme configuration. $this->tpl->assign('conf', $this->conf); -- cgit v1.2.3 From cfdd2094407e61f371c02117c8c66916a6d1d807 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Thu, 5 Nov 2020 19:45:41 +0100 Subject: Display error details even with dev.debug set to false It makes more sense to display the error even if it's unexpected. Only for logged in users. Fixes #1606 --- application/front/controller/visitor/ErrorController.php | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) (limited to 'application') diff --git a/application/front/controller/visitor/ErrorController.php b/application/front/controller/visitor/ErrorController.php index 8da11172..428e8254 100644 --- a/application/front/controller/visitor/ErrorController.php +++ b/application/front/controller/visitor/ErrorController.php @@ -26,8 +26,14 @@ class ErrorController extends ShaarliVisitorController $response = $response->withStatus($throwable->getCode()); } else { // Internal error (any other Throwable) - if ($this->container->conf->get('dev.debug', false)) { - $this->assignView('message', $throwable->getMessage()); + if ($this->container->conf->get('dev.debug', false) || $this->container->loginManager->isLoggedIn()) { + $this->assignView('message', t('Error: ') . $throwable->getMessage()); + $this->assignView( + 'text', + '' + . t('Please report it on Github.') + . '' + ); $this->assignView('stacktrace', exception2text($throwable)); } else { $this->assignView('message', t('An unexpected error occurred.')); @@ -36,7 +42,6 @@ class ErrorController extends ShaarliVisitorController $response = $response->withStatus(500); } - return $response->write($this->render('error')); } } -- cgit v1.2.3 From 9952de2fe0d7e6b2c45d551ae523ddd796653c7d Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sun, 8 Nov 2020 11:58:17 +0100 Subject: Replace vimeo link in demo bookmarks due to IP ban on the demo instance Fixes #1148 --- application/bookmark/BookmarkInitializer.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'application') diff --git a/application/bookmark/BookmarkInitializer.php b/application/bookmark/BookmarkInitializer.php index 04b996f3..98dd3f1c 100644 --- a/application/bookmark/BookmarkInitializer.php +++ b/application/bookmark/BookmarkInitializer.php @@ -36,8 +36,8 @@ class BookmarkInitializer public function initialize(): void { $bookmark = new Bookmark(); - $bookmark->setTitle('quicksilver (loop) on Vimeo ' . t('(private bookmark with thumbnail demo)')); - $bookmark->setUrl('https://vimeo.com/153493904'); + $bookmark->setTitle('Calm Jazz Music - YouTube ' . t('(private bookmark with thumbnail demo)')); + $bookmark->setUrl('https://www.youtube.com/watch?v=DVEUcbPkb-c'); $bookmark->setDescription(t( 'Shaarli will automatically pick up the thumbnail for links to a variety of websites. -- cgit v1.2.3 From 00d3dd91ef42df13eeafbcc54dcebe3238e322c6 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sun, 8 Nov 2020 13:54:39 +0100 Subject: Fix an issue truncating extracted metadata content Previous regex forced the selection to stop at either the first single or double quote found, regardless of the opening quote. Using '\1', we're sure to wait for the proper quote before stopping the capture. --- application/bookmark/LinkUtils.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'application') diff --git a/application/bookmark/LinkUtils.php b/application/bookmark/LinkUtils.php index 17c37979..a74fda57 100644 --- a/application/bookmark/LinkUtils.php +++ b/application/bookmark/LinkUtils.php @@ -68,16 +68,16 @@ function html_extract_tag($tag, $html) $properties = implode('|', $propertiesKey); // We need a OR here to accept either 'property=og:noquote' or 'property="og:unrelated og:my-tag"' $orCondition = '["\']?(?:og:)?'. $tag .'["\']?|["\'][^\'"]*?(?:og:)?' . $tag . '[^\'"]*?[\'"]'; - // Try to retrieve OpenGraph image. - $ogRegex = '#]+(?:'. $properties .')=(?:'. $orCondition .')[^>]*content=["\'](.*?)["\'].*?>#'; + // Try to retrieve OpenGraph tag. + $ogRegex = '#]+(?:'. $properties .')=(?:'. $orCondition .')[^>]*content=(["\'])([^\1]*?)\1.*?>#'; // If the attributes are not in the order property => content (e.g. Github) // New regex to keep this readable... more or less. - $ogRegexReverse = '#]+content=["\'](.*?)["\'][^>]+(?:'. $properties .')=(?:'. $orCondition .').*?>#'; + $ogRegexReverse = '#]+content=(["\'])([^\1]*?)\1[^>]+(?:'. $properties .')=(?:'. $orCondition .').*?>#'; if (preg_match($ogRegex, $html, $matches) > 0 || preg_match($ogRegexReverse, $html, $matches) > 0 ) { - return $matches[1]; + return $matches[2]; } return false; -- cgit v1.2.3 From 53054b2bf6a919fd4ff9b44b6ad1986f21f488b6 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Tue, 22 Sep 2020 20:25:47 +0200 Subject: Apply PHP Code Beautifier on source code for linter automatic fixes --- application/History.php | 1 + application/Languages.php | 13 ++-- application/Thumbnailer.php | 5 +- application/TimeZone.php | 7 ++- application/Utils.php | 14 +++-- application/api/ApiMiddleware.php | 4 +- application/api/ApiUtils.php | 6 +- application/api/controllers/HistoryController.php | 1 - application/api/controllers/Info.php | 4 +- application/api/controllers/Links.php | 6 +- .../api/exceptions/ApiAuthorizationException.php | 2 +- application/api/exceptions/ApiException.php | 2 +- application/bookmark/Bookmark.php | 5 +- application/bookmark/BookmarkArray.php | 6 +- application/bookmark/BookmarkFileService.php | 18 +++--- application/bookmark/BookmarkFilter.php | 12 ++-- application/bookmark/BookmarkIO.php | 4 +- application/bookmark/BookmarkInitializer.php | 6 +- application/bookmark/LinkUtils.php | 11 ++-- .../exception/BookmarkNotFoundException.php | 1 + .../bookmark/exception/EmptyDataStoreException.php | 6 +- .../exception/InvalidBookmarkException.php | 14 ++--- .../exception/NotWritableDataStoreException.php | 4 +- application/config/ConfigIO.php | 1 + application/config/ConfigManager.php | 13 ++-- application/config/ConfigPhp.php | 28 +++++---- application/config/ConfigPlugin.php | 8 +-- .../exception/MissingFieldConfigException.php | 1 - .../exception/UnauthorizedConfigException.php | 1 - application/exceptions/IOException.php | 1 + application/feed/FeedBuilder.php | 9 +-- .../formatter/BookmarkMarkdownFormatter.php | 12 ++-- application/formatter/BookmarkRawFormatter.php | 4 +- application/formatter/FormatterFactory.php | 2 +- application/front/ShaarliMiddleware.php | 6 +- .../front/controller/admin/ConfigureController.php | 7 ++- .../front/controller/admin/ExportController.php | 4 +- .../front/controller/admin/ImportController.php | 4 +- .../front/controller/admin/ManageTagController.php | 4 +- .../front/controller/admin/PasswordController.php | 4 +- .../front/controller/admin/PluginsController.php | 4 +- .../front/controller/admin/ServerController.php | 2 +- .../controller/admin/SessionFilterController.php | 2 - .../front/controller/admin/ShaareAddController.php | 2 +- .../controller/admin/ShaareManageController.php | 2 +- .../controller/admin/ShaarePublishController.php | 13 ++-- .../controller/admin/ThumbnailsController.php | 2 +- .../front/controller/admin/ToolsController.php | 2 +- .../controller/visitor/BookmarkListController.php | 8 ++- .../front/controller/visitor/DailyController.php | 2 +- .../front/controller/visitor/FeedController.php | 2 +- .../front/controller/visitor/InstallController.php | 25 ++++---- .../front/controller/visitor/LoginController.php | 8 ++- .../controller/visitor/PictureWallController.php | 2 +- .../visitor/ShaarliVisitorController.php | 5 +- .../controller/visitor/TagCloudController.php | 4 +- .../front/controller/visitor/TagController.php | 8 +-- application/helper/ApplicationUtils.php | 47 +++++++------- application/helper/FileUtils.php | 4 +- application/http/HttpUtils.php | 73 +++++++++++++--------- application/http/Url.php | 10 +-- application/http/UrlUtils.php | 11 ++-- application/legacy/LegacyController.php | 2 +- application/legacy/LegacyLinkDB.php | 18 +++--- application/legacy/LegacyLinkFilter.php | 18 +++--- application/legacy/LegacyUpdater.php | 12 ++-- application/netscape/NetscapeBookmarkUtils.php | 4 +- application/plugin/PluginManager.php | 13 ++-- .../exception/PluginFileNotFoundException.php | 1 + application/render/ThemeUtils.php | 4 +- application/security/BanManager.php | 8 +-- application/security/LoginManager.php | 16 +++-- application/security/SessionManager.php | 3 +- application/updater/Updater.php | 6 +- application/updater/UpdaterUtils.php | 4 +- 75 files changed, 336 insertions(+), 272 deletions(-) (limited to 'application') diff --git a/application/History.php b/application/History.php index bd5c1bf7..1be955c5 100644 --- a/application/History.php +++ b/application/History.php @@ -1,4 +1,5 @@ language = $confLanguage; } - if (! extension_loaded('gettext') + if ( + ! extension_loaded('gettext') || in_array($this->conf->get('translation.mode', 'auto'), ['auto', 'php']) ) { $this->initPhpTranslator(); @@ -98,7 +99,7 @@ class Languages $this->translator->loadDomain(self::DEFAULT_DOMAIN, 'inc/languages'); // Default extension translation from the current theme - $themeTransFolder = rtrim($this->conf->get('raintpl_tpl'), '/') .'/'. $this->conf->get('theme') .'/language'; + $themeTransFolder = rtrim($this->conf->get('raintpl_tpl'), '/') . '/' . $this->conf->get('theme') . '/language'; if (is_dir($themeTransFolder)) { $this->translator->loadDomain($this->conf->get('theme'), $themeTransFolder, false); } @@ -121,7 +122,7 @@ class Languages $translations = new Translations(); // Core translations try { - $translations = $translations->addFromPoFile('inc/languages/'. $this->language .'/LC_MESSAGES/shaarli.po'); + $translations = $translations->addFromPoFile('inc/languages/' . $this->language . '/LC_MESSAGES/shaarli.po'); $translations->setDomain('shaarli'); $this->translator->loadTranslations($translations); } catch (\InvalidArgumentException $e) { @@ -129,11 +130,11 @@ class Languages // Default extension translation from the current theme $theme = $this->conf->get('theme'); - $themeTransFolder = rtrim($this->conf->get('raintpl_tpl'), '/') .'/'. $theme .'/language'; + $themeTransFolder = rtrim($this->conf->get('raintpl_tpl'), '/') . '/' . $theme . '/language'; if (is_dir($themeTransFolder)) { try { $translations = Translations::fromPoFile( - $themeTransFolder .'/'. $this->language .'/LC_MESSAGES/'. $theme .'.po' + $themeTransFolder . '/' . $this->language . '/LC_MESSAGES/' . $theme . '.po' ); $translations->setDomain($theme); $this->translator->loadTranslations($translations); @@ -149,7 +150,7 @@ class Languages try { $extension = Translations::fromPoFile( - $translationPath . $this->language .'/LC_MESSAGES/'. $domain .'.po' + $translationPath . $this->language . '/LC_MESSAGES/' . $domain . '.po' ); $extension->setDomain($domain); $this->translator->loadTranslations($extension); diff --git a/application/Thumbnailer.php b/application/Thumbnailer.php index 5aec23c8..30354310 100644 --- a/application/Thumbnailer.php +++ b/application/Thumbnailer.php @@ -60,7 +60,7 @@ class Thumbnailer // TODO: create a proper error handling system able to catch exceptions... die(t( 'php-gd extension must be loaded to use thumbnails. ' - .'Thumbnails are now disabled. Please reload the page.' + . 'Thumbnails are now disabled. Please reload the page.' )); } @@ -81,7 +81,8 @@ class Thumbnailer */ public function get($url) { - if ($this->conf->get('thumbnails.mode') === self::MODE_COMMON + if ( + $this->conf->get('thumbnails.mode') === self::MODE_COMMON && ! $this->isCommonMediaOrImage($url) ) { return false; diff --git a/application/TimeZone.php b/application/TimeZone.php index c1869ef8..a420eb96 100644 --- a/application/TimeZone.php +++ b/application/TimeZone.php @@ -1,4 +1,5 @@ $continent, 'city' => $city]; $continents[$continent] = true; } @@ -85,7 +86,7 @@ function generateTimeZoneData($installedTimeZones, $preselectedTimezone = '') function isTimeZoneValid($continent, $city) { return in_array( - $continent.'/'.$city, + $continent . '/' . $city, timezone_identifiers_list() ); } diff --git a/application/Utils.php b/application/Utils.php index db046893..4c2d6701 100644 --- a/application/Utils.php +++ b/application/Utils.php @@ -1,4 +1,5 @@ $value) { $out[escape($key)] = escape($value); } @@ -163,7 +164,7 @@ function checkDateFormat($format, $string) * * @return string $referer - final referer. */ -function generateLocation($referer, $host, $loopTerms = array()) +function generateLocation($referer, $host, $loopTerms = []) { $finalReferer = './?'; @@ -196,7 +197,7 @@ function generateLocation($referer, $host, $loopTerms = array()) function autoLocale($headerLocale) { // Default if browser does not send HTTP_ACCEPT_LANGUAGE - $locales = array('en_US', 'en_US.utf8', 'en_US.UTF-8'); + $locales = ['en_US', 'en_US.utf8', 'en_US.UTF-8']; if (! empty($headerLocale)) { if (preg_match_all('/([a-z]{2,3})[-_]?([a-z]{2})?,?/i', $headerLocale, $matches, PREG_SET_ORDER)) { $attempts = []; @@ -376,7 +377,7 @@ function return_bytes($val) return $val; } $val = trim($val); - $last = strtolower($val[strlen($val)-1]); + $last = strtolower($val[strlen($val) - 1]); $val = intval(substr($val, 0, -1)); switch ($last) { case 'g': @@ -482,7 +483,9 @@ function alphabetical_sort(&$data, $reverse = false, $byKeys = false) */ function t($text, $nText = '', $nb = 1, $domain = 'shaarli', $variables = [], $fixCase = false) { - $postFunction = $fixCase ? 'ucfirst' : function ($input) { return $input; }; + $postFunction = $fixCase ? 'ucfirst' : function ($input) { + return $input; + }; return $postFunction(dn__($domain, $text, $nText, $nb, $variables)); } @@ -494,4 +497,3 @@ function exception2text(Throwable $e): string { return $e->getMessage() . PHP_EOL . $e->getFile() . $e->getLine() . PHP_EOL . $e->getTraceAsString(); } - diff --git a/application/api/ApiMiddleware.php b/application/api/ApiMiddleware.php index adc8b266..9fb88358 100644 --- a/application/api/ApiMiddleware.php +++ b/application/api/ApiMiddleware.php @@ -1,4 +1,5 @@ hasHeader('Authorization') + if ( + !$request->hasHeader('Authorization') && !isset($this->container->environment['REDIRECT_HTTP_AUTHORIZATION']) ) { throw new ApiAuthorizationException('JWT token not provided'); diff --git a/application/api/ApiUtils.php b/application/api/ApiUtils.php index eb1ca9bc..05a2840a 100644 --- a/application/api/ApiUtils.php +++ b/application/api/ApiUtils.php @@ -1,4 +1,5 @@ iat) + if ( + empty($payload->iat) || $payload->iat > time() || time() - $payload->iat > ApiMiddleware::$TOKEN_DURATION ) { diff --git a/application/api/controllers/HistoryController.php b/application/api/controllers/HistoryController.php index 505647a9..d83a3a25 100644 --- a/application/api/controllers/HistoryController.php +++ b/application/api/controllers/HistoryController.php @@ -1,6 +1,5 @@ $this->bookmarkService->count(), 'private_counter' => $this->bookmarkService->count(BookmarkFilter::$PRIVATE), - 'settings' => array( + 'settings' => [ 'title' => $this->conf->get('general.title', 'Shaarli'), 'header_link' => $this->conf->get('general.header_link', '?'), 'timezone' => $this->conf->get('general.timezone', 'UTC'), 'enabled_plugins' => $this->conf->get('general.enabled_plugins', []), 'default_private_links' => $this->conf->get('privacy.default_private_links', false), - ), + ], ]; return $response->withJson($info, 200, $this->jsonStyle); diff --git a/application/api/controllers/Links.php b/application/api/controllers/Links.php index 6bf529e4..c379b962 100644 --- a/application/api/controllers/Links.php +++ b/application/api/controllers/Links.php @@ -119,7 +119,8 @@ class Links extends ApiController $data = (array) ($request->getParsedBody() ?? []); $bookmark = ApiUtils::buildBookmarkFromRequest($data, $this->conf->get('privacy.default_private_links')); // duplicate by URL, return 409 Conflict - if (! empty($bookmark->getUrl()) + if ( + ! empty($bookmark->getUrl()) && ! empty($dup = $this->bookmarkService->findByUrl($bookmark->getUrl())) ) { return $response->withJson( @@ -159,7 +160,8 @@ class Links extends ApiController $requestBookmark = ApiUtils::buildBookmarkFromRequest($data, $this->conf->get('privacy.default_private_links')); // duplicate URL on a different link, return 409 Conflict - if (! empty($requestBookmark->getUrl()) + if ( + ! empty($requestBookmark->getUrl()) && ! empty($dup = $this->bookmarkService->findByUrl($requestBookmark->getUrl())) && $dup->getId() != $id ) { diff --git a/application/api/exceptions/ApiAuthorizationException.php b/application/api/exceptions/ApiAuthorizationException.php index 0e3f4776..c77e9eea 100644 --- a/application/api/exceptions/ApiAuthorizationException.php +++ b/application/api/exceptions/ApiAuthorizationException.php @@ -28,7 +28,7 @@ class ApiAuthorizationException extends ApiException */ public function setMessage($message) { - $original = $this->debug === true ? ': '. $this->getMessage() : ''; + $original = $this->debug === true ? ': ' . $this->getMessage() : ''; $this->message = $message . $original; } } diff --git a/application/api/exceptions/ApiException.php b/application/api/exceptions/ApiException.php index d6b66323..7deafb96 100644 --- a/application/api/exceptions/ApiException.php +++ b/application/api/exceptions/ApiException.php @@ -44,7 +44,7 @@ abstract class ApiException extends \Exception } return [ 'message' => $this->getMessage(), - 'stacktrace' => get_class($this) .': '. $this->getTraceAsString() + 'stacktrace' => get_class($this) . ': ' . $this->getTraceAsString() ]; } diff --git a/application/bookmark/Bookmark.php b/application/bookmark/Bookmark.php index 8aaeb9d8..b592722f 100644 --- a/application/bookmark/Bookmark.php +++ b/application/bookmark/Bookmark.php @@ -106,7 +106,8 @@ class Bookmark */ public function validate(): void { - if ($this->id === null + if ( + $this->id === null || ! is_int($this->id) || empty($this->shortUrl) || empty($this->created) @@ -114,7 +115,7 @@ class Bookmark throw new InvalidBookmarkException($this); } if (empty($this->url)) { - $this->url = '/shaare/'. $this->shortUrl; + $this->url = '/shaare/' . $this->shortUrl; } if (empty($this->title)) { $this->title = $this->url; diff --git a/application/bookmark/BookmarkArray.php b/application/bookmark/BookmarkArray.php index 67bb3b73..b9328116 100644 --- a/application/bookmark/BookmarkArray.php +++ b/application/bookmark/BookmarkArray.php @@ -72,7 +72,8 @@ class BookmarkArray implements \Iterator, \Countable, \ArrayAccess */ public function offsetSet($offset, $value) { - if (! $value instanceof Bookmark + if ( + ! $value instanceof Bookmark || $value->getId() === null || empty($value->getUrl()) || ($offset !== null && ! is_int($offset)) || ! is_int($value->getId()) || $offset !== null && $offset !== $value->getId() @@ -222,7 +223,8 @@ class BookmarkArray implements \Iterator, \Countable, \ArrayAccess */ public function getByUrl(string $url): ?Bookmark { - if (! empty($url) + if ( + ! empty($url) && isset($this->urls[$url]) && isset($this->bookmarks[$this->urls[$url]]) ) { diff --git a/application/bookmark/BookmarkFileService.php b/application/bookmark/BookmarkFileService.php index 85efeea6..66248cc2 100644 --- a/application/bookmark/BookmarkFileService.php +++ b/application/bookmark/BookmarkFileService.php @@ -69,7 +69,7 @@ class BookmarkFileService implements BookmarkServiceInterface } else { try { $this->bookmarks = $this->bookmarksIO->read(); - } catch (EmptyDataStoreException|DatastoreNotInitializedException $e) { + } catch (EmptyDataStoreException | DatastoreNotInitializedException $e) { $this->bookmarks = new BookmarkArray(); if ($this->isLoggedIn) { @@ -85,7 +85,7 @@ class BookmarkFileService implements BookmarkServiceInterface if (! $this->bookmarks instanceof BookmarkArray) { $this->migrate(); exit( - 'Your data store has been migrated, please reload the page.'. PHP_EOL . + 'Your data store has been migrated, please reload the page.' . PHP_EOL . 'If this message keeps showing up, please delete data/updates.txt file.' ); } @@ -102,7 +102,8 @@ class BookmarkFileService implements BookmarkServiceInterface $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 + if ( + !$this->isLoggedIn && $first->isPrivate() && (empty($privateKey) || $privateKey !== $first->getAdditionalContentEntry('private_key')) ) { @@ -165,7 +166,8 @@ class BookmarkFileService implements BookmarkServiceInterface } $bookmark = $this->bookmarks[$id]; - if (($bookmark->isPrivate() && $visibility != 'all' && $visibility != 'private') + if ( + ($bookmark->isPrivate() && $visibility != 'all' && $visibility != 'private') || (! $bookmark->isPrivate() && $visibility != 'all' && $visibility != 'public') ) { throw new Exception('Unauthorized'); @@ -265,7 +267,8 @@ class BookmarkFileService implements BookmarkServiceInterface } $bookmark = $this->bookmarks[$id]; - if (($bookmark->isPrivate() && $visibility != 'all' && $visibility != 'private') + if ( + ($bookmark->isPrivate() && $visibility != 'all' && $visibility != 'private') || (! $bookmark->isPrivate() && $visibility != 'all' && $visibility != 'public') ) { return false; @@ -307,7 +310,8 @@ class BookmarkFileService implements BookmarkServiceInterface $caseMapping = []; foreach ($bookmarks as $bookmark) { foreach ($bookmark->getTags() as $tag) { - if (empty($tag) + if ( + empty($tag) || (! $this->isLoggedIn && startsWith($tag, '.')) || $tag === BookmarkMarkdownFormatter::NO_MD_TAG || in_array($tag, $filteringTags, true) @@ -356,7 +360,7 @@ class BookmarkFileService implements BookmarkServiceInterface foreach ($this->search([], null, false, false, true) as $bookmark) { if ($to < $bookmark->getCreated()) { $next = $bookmark->getCreated(); - } else if ($from < $bookmark->getCreated() && $to > $bookmark->getCreated()) { + } elseif ($from < $bookmark->getCreated() && $to > $bookmark->getCreated()) { $out[] = $bookmark; } else { if ($previous !== null) { diff --git a/application/bookmark/BookmarkFilter.php b/application/bookmark/BookmarkFilter.php index 5d8733dc..db83c51c 100644 --- a/application/bookmark/BookmarkFilter.php +++ b/application/bookmark/BookmarkFilter.php @@ -150,7 +150,7 @@ class BookmarkFilter return $this->bookmarks; } - $out = array(); + $out = []; foreach ($this->bookmarks as $key => $value) { if ($value->isPrivate() && $visibility === 'private') { $out[$key] = $value; @@ -395,7 +395,7 @@ class BookmarkFilter $search = $link->getTagsString($tagsSeparator); if (strlen(trim($link->getDescription())) && strpos($link->getDescription(), '#') !== false) { // description given and at least one possible tag found - $descTags = array(); + $descTags = []; // find all tags in the form of #tag in the description preg_match_all( '/(?getTagsString($this->conf->get('general.tags_separator', ' ')); - $content = mb_convert_case($link->getTitle(), MB_CASE_LOWER, 'UTF-8') .'\\'; - $content .= mb_convert_case($link->getDescription(), MB_CASE_LOWER, 'UTF-8') .'\\'; - $content .= mb_convert_case($link->getUrl(), MB_CASE_LOWER, 'UTF-8') .'\\'; - $content .= mb_convert_case($tagString, MB_CASE_LOWER, 'UTF-8') .'\\'; + $content = mb_convert_case($link->getTitle(), MB_CASE_LOWER, 'UTF-8') . '\\'; + $content .= mb_convert_case($link->getDescription(), MB_CASE_LOWER, 'UTF-8') . '\\'; + $content .= mb_convert_case($link->getUrl(), MB_CASE_LOWER, 'UTF-8') . '\\'; + $content .= mb_convert_case($tagString, MB_CASE_LOWER, 'UTF-8') . '\\'; $lengths['title'] = ['start' => 0, 'end' => mb_strlen($link->getTitle())]; $nextField = $lengths['title']['end'] + 1; diff --git a/application/bookmark/BookmarkIO.php b/application/bookmark/BookmarkIO.php index f40fa476..c78dbe41 100644 --- a/application/bookmark/BookmarkIO.php +++ b/application/bookmark/BookmarkIO.php @@ -112,12 +112,12 @@ class BookmarkIO if (is_file($this->datastore) && !is_writeable($this->datastore)) { // The datastore exists but is not writeable throw new NotWritableDataStoreException($this->datastore); - } else if (!is_file($this->datastore) && !is_writeable(dirname($this->datastore))) { + } elseif (!is_file($this->datastore) && !is_writeable(dirname($this->datastore))) { // The datastore does not exist and its parent directory is not writeable throw new NotWritableDataStoreException(dirname($this->datastore)); } - $data = self::$phpPrefix.base64_encode(gzdeflate(serialize($links))).self::$phpSuffix; + $data = self::$phpPrefix . base64_encode(gzdeflate(serialize($links))) . self::$phpSuffix; $this->mutex->synchronized(function () use ($data) { file_put_contents( diff --git a/application/bookmark/BookmarkInitializer.php b/application/bookmark/BookmarkInitializer.php index 98dd3f1c..2240f58c 100644 --- a/application/bookmark/BookmarkInitializer.php +++ b/application/bookmark/BookmarkInitializer.php @@ -39,7 +39,7 @@ class BookmarkInitializer $bookmark->setTitle('Calm Jazz Music - YouTube ' . t('(private bookmark with thumbnail demo)')); $bookmark->setUrl('https://www.youtube.com/watch?v=DVEUcbPkb-c'); $bookmark->setDescription(t( -'Shaarli will automatically pick up the thumbnail for links to a variety of websites. + 'Shaarli will automatically pick up the thumbnail for links to a variety of websites. Explore your new Shaarli instance by trying out controls and menus. Visit the project on [Github](https://github.com/shaarli/Shaarli) or [the documentation](https://shaarli.readthedocs.io/en/master/) to learn more about Shaarli. @@ -54,7 +54,7 @@ Now you can edit or delete the default shaares. $bookmark = new Bookmark(); $bookmark->setTitle(t('Note: Shaare descriptions')); $bookmark->setDescription(t( -'Adding a shaare without entering a URL creates a text-only "note" post such as this one. + 'Adding a shaare without entering a URL creates a text-only "note" post such as this one. This note is private, so you are the only one able to see it while logged in. You can use this to keep notes, post articles, code snippets, and much more. @@ -91,7 +91,7 @@ Markdown also supports tables: 'Shaarli - ' . t('The personal, minimalist, super-fast, database free, bookmarking service') ); $bookmark->setDescription(t( -'Welcome to Shaarli! + 'Welcome to Shaarli! Shaarli allows you to bookmark your favorite pages, and share them with others or store them privately. You can add a description to your bookmarks, such as this one, and tag them. diff --git a/application/bookmark/LinkUtils.php b/application/bookmark/LinkUtils.php index cf97e3b0..d65e97ed 100644 --- a/application/bookmark/LinkUtils.php +++ b/application/bookmark/LinkUtils.php @@ -67,14 +67,15 @@ function html_extract_tag($tag, $html) $propertiesKey = ['property', 'name', 'itemprop']; $properties = implode('|', $propertiesKey); // We need a OR here to accept either 'property=og:noquote' or 'property="og:unrelated og:my-tag"' - $orCondition = '["\']?(?:og:)?'. $tag .'["\']?|["\'][^\'"]*?(?:og:)?' . $tag . '[^\'"]*?[\'"]'; + $orCondition = '["\']?(?:og:)?' . $tag . '["\']?|["\'][^\'"]*?(?:og:)?' . $tag . '[^\'"]*?[\'"]'; // Try to retrieve OpenGraph tag. - $ogRegex = '#]+(?:'. $properties .')=(?:'. $orCondition .')[^>]*content=(["\'])([^\1]*?)\1.*?>#'; + $ogRegex = '#]+(?:' . $properties . ')=(?:' . $orCondition . ')[^>]*content=(["\'])([^\1]*?)\1.*?>#'; // If the attributes are not in the order property => content (e.g. Github) // New regex to keep this readable... more or less. - $ogRegexReverse = '#]+content=(["\'])([^\1]*?)\1[^>]+(?:'. $properties .')=(?:'. $orCondition .').*?>#'; + $ogRegexReverse = '#]+content=(["\'])([^\1]*?)\1[^>]+(?:' . $properties . ')=(?:' . $orCondition . ').*?>#'; - if (preg_match($ogRegex, $html, $matches) > 0 + if ( + preg_match($ogRegex, $html, $matches) > 0 || preg_match($ogRegexReverse, $html, $matches) > 0 ) { return $matches[2]; @@ -116,7 +117,7 @@ function hashtag_autolink($description, $indexUrl = '') * \p{Mn} - any non marking space (accents, umlauts, etc) */ $regex = '/(^|\s)#([\p{Pc}\p{N}\p{L}\p{Mn}]+)/mui'; - $replacement = '$1#$2'; + $replacement = '$1#$2'; return preg_replace($regex, $replacement, $description); } diff --git a/application/bookmark/exception/BookmarkNotFoundException.php b/application/bookmark/exception/BookmarkNotFoundException.php index 827a3d35..a91d1efa 100644 --- a/application/bookmark/exception/BookmarkNotFoundException.php +++ b/application/bookmark/exception/BookmarkNotFoundException.php @@ -1,4 +1,5 @@ message = 'This bookmark is not valid'. PHP_EOL; - $this->message .= ' - ID: '. $bookmark->getId() . PHP_EOL; - $this->message .= ' - Title: '. $bookmark->getTitle() . PHP_EOL; - $this->message .= ' - Url: '. $bookmark->getUrl() . PHP_EOL; - $this->message .= ' - ShortUrl: '. $bookmark->getShortUrl() . PHP_EOL; - $this->message .= ' - Created: '. $created . PHP_EOL; + $this->message = 'This bookmark is not valid' . PHP_EOL; + $this->message .= ' - ID: ' . $bookmark->getId() . PHP_EOL; + $this->message .= ' - Title: ' . $bookmark->getTitle() . PHP_EOL; + $this->message .= ' - Url: ' . $bookmark->getUrl() . PHP_EOL; + $this->message .= ' - ShortUrl: ' . $bookmark->getShortUrl() . PHP_EOL; + $this->message .= ' - Created: ' . $created . PHP_EOL; } else { - $this->message = 'The provided data is not a bookmark'. PHP_EOL; + $this->message = 'The provided data is not a bookmark' . PHP_EOL; $this->message .= var_export($bookmark, true); } } diff --git a/application/bookmark/exception/NotWritableDataStoreException.php b/application/bookmark/exception/NotWritableDataStoreException.php index 95f34b50..df91f3bc 100644 --- a/application/bookmark/exception/NotWritableDataStoreException.php +++ b/application/bookmark/exception/NotWritableDataStoreException.php @@ -1,9 +1,7 @@ message = 'Couldn\'t load data from the data store file "'. $dataStore .'". '. + $this->message = 'Couldn\'t load data from the data store file "' . $dataStore . '". ' . 'Your data might be corrupted, or your file isn\'t readable.'; } } diff --git a/application/config/ConfigIO.php b/application/config/ConfigIO.php index 3efe5b6f..a623bc8b 100644 --- a/application/config/ConfigIO.php +++ b/application/config/ConfigIO.php @@ -1,4 +1,5 @@ getConfigFileExt()) && !$isLoggedIn) { @@ -392,7 +393,7 @@ class ConfigManager $this->setEmpty('translation.mode', 'php'); $this->setEmpty('translation.extensions', []); - $this->setEmpty('plugins', array()); + $this->setEmpty('plugins', []); $this->setEmpty('formatter', 'markdown'); } diff --git a/application/config/ConfigPhp.php b/application/config/ConfigPhp.php index cad34594..53d6a7a3 100644 --- a/application/config/ConfigPhp.php +++ b/application/config/ConfigPhp.php @@ -1,4 +1,5 @@ legacy key. */ - public static $LEGACY_KEYS_MAPPING = array( + public static $LEGACY_KEYS_MAPPING = [ 'credentials.login' => 'login', 'credentials.hash' => 'hash', 'credentials.salt' => 'salt', @@ -68,7 +69,7 @@ class ConfigPhp implements ConfigIO 'privacy.hide_public_links' => 'config.HIDE_PUBLIC_LINKS', 'privacy.hide_timestamps' => 'config.HIDE_TIMESTAMPS', 'security.open_shaarli' => 'config.OPEN_SHAARLI', - ); + ]; /** * @inheritdoc @@ -76,12 +77,12 @@ class ConfigPhp implements ConfigIO public function read($filepath) { if (! file_exists($filepath) || ! is_readable($filepath)) { - return array(); + return []; } include $filepath; - $out = array(); + $out = []; foreach (self::$ROOT_KEYS as $key) { $out[$key] = isset($GLOBALS[$key]) ? $GLOBALS[$key] : ''; } @@ -95,7 +96,7 @@ class ConfigPhp implements ConfigIO */ public function write($filepath, $conf) { - $configStr = ' $value) { $configStr .= '$GLOBALS[\'config\'][\'' . $key - .'\'] = ' - .var_export($conf['config'][$key], true).';' + . '\'] = ' + . var_export($conf['config'][$key], true) . ';' . PHP_EOL; } @@ -115,18 +116,19 @@ class ConfigPhp implements ConfigIO foreach ($conf['plugins'] as $key => $value) { $configStr .= '$GLOBALS[\'plugins\'][\'' . $key - .'\'] = ' - .var_export($conf['plugins'][$key], true).';' + . '\'] = ' + . var_export($conf['plugins'][$key], true) . ';' . PHP_EOL; } } - if (!file_put_contents($filepath, $configStr) + if ( + !file_put_contents($filepath, $configStr) || strcmp(file_get_contents($filepath), $configStr) != 0 ) { throw new \Shaarli\Exceptions\IOException( $filepath, - t('Shaarli could not create the config file. '. + t('Shaarli could not create the config file. ' . 'Please make sure Shaarli has the right to write in the folder is it installed in.') ); } diff --git a/application/config/ConfigPlugin.php b/application/config/ConfigPlugin.php index ea8dfbda..6cadef12 100644 --- a/application/config/ConfigPlugin.php +++ b/application/config/ConfigPlugin.php @@ -39,8 +39,8 @@ function save_plugin_config($formData) throw new PluginConfigOrderException(); } - $plugins = array(); - $newEnabledPlugins = array(); + $plugins = []; + $newEnabledPlugins = []; foreach ($formData as $key => $data) { if (startsWith($key, 'order')) { continue; @@ -62,7 +62,7 @@ function save_plugin_config($formData) throw new PluginConfigOrderException(); } - $finalPlugins = array(); + $finalPlugins = []; // Make plugins order continuous. foreach ($plugins as $plugin) { $finalPlugins[] = $plugin; @@ -81,7 +81,7 @@ function save_plugin_config($formData) */ function validate_plugin_order($formData) { - $orders = array(); + $orders = []; foreach ($formData as $key => $value) { // No duplicate order allowed. if (in_array($value, $orders, true)) { diff --git a/application/config/exception/MissingFieldConfigException.php b/application/config/exception/MissingFieldConfigException.php index 9e0a9359..a5f4356a 100644 --- a/application/config/exception/MissingFieldConfigException.php +++ b/application/config/exception/MissingFieldConfigException.php @@ -1,6 +1,5 @@ getNbLinks(count($linksToDisplay), $userInput); // Can't use array_keys() because $link is a LinkDB instance and not a real array. - $keys = array(); + $keys = []; foreach ($linksToDisplay as $key => $value) { $keys[] = $key; } $pageaddr = escape(index_url($this->serverInfo)); $this->formatter->addContextData('index_url', $pageaddr); - $linkDisplayed = array(); + $linkDisplayed = []; for ($i = 0; $i < $nblinksToDisplay && $i < count($keys); $i++) { $linkDisplayed[$keys[$i]] = $this->buildItem($feedType, $linksToDisplay[$keys[$i]], $pageaddr); } @@ -176,9 +177,9 @@ class FeedBuilder $data = $this->formatter->format($link); $data['guid'] = rtrim($pageaddr, '/') . '/shaare/' . $data['shorturl']; if ($this->usePermalinks === true) { - $permalink = ''. t('Direct link') .''; + $permalink = '' . t('Direct link') . ''; } else { - $permalink = ''. t('Permalink') .''; + $permalink = '' . t('Permalink') . ''; } $data['description'] .= PHP_EOL . PHP_EOL . '
— ' . $permalink; diff --git a/application/formatter/BookmarkMarkdownFormatter.php b/application/formatter/BookmarkMarkdownFormatter.php index f7714be9..052333ca 100644 --- a/application/formatter/BookmarkMarkdownFormatter.php +++ b/application/formatter/BookmarkMarkdownFormatter.php @@ -71,7 +71,7 @@ class BookmarkMarkdownFormatter extends BookmarkDefaultFormatter $processedDescription = $this->replaceTokens($processedDescription); if (!empty($processedDescription)) { - $processedDescription = '
'. $processedDescription . '
'; + $processedDescription = '
' . $processedDescription . '
'; } return $processedDescription; @@ -110,7 +110,7 @@ class BookmarkMarkdownFormatter extends BookmarkDefaultFormatter function ($match) use ($allowedProtocols, $indexUrl) { $link = startsWith($match[1], '?') || startsWith($match[1], '/') ? $indexUrl : ''; $link .= whitelist_protocols($match[1], $allowedProtocols); - return ']('. $link.')'; + return '](' . $link . ')'; }, $description ); @@ -137,7 +137,7 @@ class BookmarkMarkdownFormatter extends BookmarkDefaultFormatter * \p{Mn} - any non marking space (accents, umlauts, etc) */ $regex = '/(^|\s)#([\p{Pc}\p{N}\p{L}\p{Mn}]+)/mui'; - $replacement = '$1[#$2]('. $indexUrl .'./add-tag/$2)'; + $replacement = '$1[#$2](' . $indexUrl . './add-tag/$2)'; $descriptionLines = explode(PHP_EOL, $description); $descriptionOut = ''; @@ -178,17 +178,17 @@ class BookmarkMarkdownFormatter extends BookmarkDefaultFormatter */ protected function sanitizeHtml($description) { - $escapeTags = array( + $escapeTags = [ 'script', 'style', 'link', 'iframe', 'frameset', 'frame', - ); + ]; foreach ($escapeTags as $tag) { $description = preg_replace_callback( - '#<\s*'. $tag .'[^>]*>(.*]*>)?#is', + '#<\s*' . $tag . '[^>]*>(.*]*>)?#is', function ($match) { return escape($match[0]); }, diff --git a/application/formatter/BookmarkRawFormatter.php b/application/formatter/BookmarkRawFormatter.php index bc372273..4ff07cdf 100644 --- a/application/formatter/BookmarkRawFormatter.php +++ b/application/formatter/BookmarkRawFormatter.php @@ -10,4 +10,6 @@ namespace Shaarli\Formatter; * * @package Shaarli\Formatter */ -class BookmarkRawFormatter extends BookmarkFormatter {} +class BookmarkRawFormatter extends BookmarkFormatter +{ +} diff --git a/application/formatter/FormatterFactory.php b/application/formatter/FormatterFactory.php index a029579f..bb865aed 100644 --- a/application/formatter/FormatterFactory.php +++ b/application/formatter/FormatterFactory.php @@ -41,7 +41,7 @@ class FormatterFactory public function getFormatter(string $type = null): BookmarkFormatter { $type = $type ? $type : $this->conf->get('formatter', 'default'); - $className = '\\Shaarli\\Formatter\\Bookmark'. ucfirst($type) .'Formatter'; + $className = '\\Shaarli\\Formatter\\Bookmark' . ucfirst($type) . 'Formatter'; if (!class_exists($className)) { $className = '\\Shaarli\\Formatter\\BookmarkDefaultFormatter'; } diff --git a/application/front/ShaarliMiddleware.php b/application/front/ShaarliMiddleware.php index d1aa1399..164217f4 100644 --- a/application/front/ShaarliMiddleware.php +++ b/application/front/ShaarliMiddleware.php @@ -42,7 +42,8 @@ class ShaarliMiddleware $this->initBasePath($request); try { - if (!is_file($this->container->conf->getConfigFileExt()) + if ( + !is_file($this->container->conf->getConfigFileExt()) && !in_array($next->getName(), ['displayInstall', 'saveInstall'], true) ) { return $response->withRedirect($this->container->basePath . '/install'); @@ -86,7 +87,8 @@ class ShaarliMiddleware */ protected function checkOpenShaarli(Request $request, Response $response, callable $next): bool { - if (// if the user isn't logged in + if ( +// if the user isn't logged in !$this->container->loginManager->isLoggedIn() // and Shaarli doesn't have public content... && $this->container->conf->get('privacy.hide_public_links') diff --git a/application/front/controller/admin/ConfigureController.php b/application/front/controller/admin/ConfigureController.php index 0ed7ad81..eb26ef21 100644 --- a/application/front/controller/admin/ConfigureController.php +++ b/application/front/controller/admin/ConfigureController.php @@ -51,7 +51,7 @@ class ConfigureController extends ShaarliAdminController $this->assignView('languages', Languages::getAvailableLanguages()); $this->assignView('gd_enabled', extension_loaded('gd')); $this->assignView('thumbnails_mode', $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE)); - $this->assignView('pagetitle', t('Configure') .' - '. $this->container->conf->get('general.title', 'Shaarli')); + $this->assignView('pagetitle', t('Configure') . ' - ' . $this->container->conf->get('general.title', 'Shaarli')); return $response->write($this->render(TemplatePage::CONFIGURE)); } @@ -95,12 +95,13 @@ class ConfigureController extends ShaarliAdminController } $thumbnailsMode = extension_loaded('gd') ? $request->getParam('enableThumbnails') : Thumbnailer::MODE_NONE; - if ($thumbnailsMode !== Thumbnailer::MODE_NONE + if ( + $thumbnailsMode !== Thumbnailer::MODE_NONE && $thumbnailsMode !== $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) ) { $this->saveWarningMessage( t('You have enabled or changed thumbnails mode.') . - '' . t('Please synchronize them.') .'' + '' . t('Please synchronize them.') . '' ); } $this->container->conf->set('thumbnails.mode', $thumbnailsMode); diff --git a/application/front/controller/admin/ExportController.php b/application/front/controller/admin/ExportController.php index 2be957fa..f01d7e9b 100644 --- a/application/front/controller/admin/ExportController.php +++ b/application/front/controller/admin/ExportController.php @@ -23,7 +23,7 @@ class ExportController extends ShaarliAdminController */ public function index(Request $request, Response $response): Response { - $this->assignView('pagetitle', t('Export') .' - '. $this->container->conf->get('general.title', 'Shaarli')); + $this->assignView('pagetitle', t('Export') . ' - ' . $this->container->conf->get('general.title', 'Shaarli')); return $response->write($this->render(TemplatePage::EXPORT)); } @@ -68,7 +68,7 @@ class ExportController extends ShaarliAdminController $response = $response->withHeader('Content-Type', 'text/html; charset=utf-8'); $response = $response->withHeader( 'Content-disposition', - 'attachment; filename=bookmarks_'.$selection.'_'.$now->format(Bookmark::LINK_DATE_FORMAT).'.html' + 'attachment; filename=bookmarks_' . $selection . '_' . $now->format(Bookmark::LINK_DATE_FORMAT) . '.html' ); $this->assignView('date', $now->format(DateTime::RFC822)); diff --git a/application/front/controller/admin/ImportController.php b/application/front/controller/admin/ImportController.php index 758d5ef9..c2ad6a09 100644 --- a/application/front/controller/admin/ImportController.php +++ b/application/front/controller/admin/ImportController.php @@ -38,7 +38,7 @@ class ImportController extends ShaarliAdminController true ) ); - $this->assignView('pagetitle', t('Import') .' - '. $this->container->conf->get('general.title', 'Shaarli')); + $this->assignView('pagetitle', t('Import') . ' - ' . $this->container->conf->get('general.title', 'Shaarli')); return $response->write($this->render(TemplatePage::IMPORT)); } @@ -64,7 +64,7 @@ class ImportController extends ShaarliAdminController $msg = sprintf( t( 'The file you are trying to upload is probably bigger than what this webserver can accept' - .' (%s). Please upload in smaller chunks.' + . ' (%s). Please upload in smaller chunks.' ), get_max_upload_size(ini_get('post_max_size'), ini_get('upload_max_filesize')) ); diff --git a/application/front/controller/admin/ManageTagController.php b/application/front/controller/admin/ManageTagController.php index 22fb461c..8675a0c5 100644 --- a/application/front/controller/admin/ManageTagController.php +++ b/application/front/controller/admin/ManageTagController.php @@ -32,7 +32,7 @@ class ManageTagController extends ShaarliAdminController $this->assignView('tags_separator', $separator); $this->assignView( 'pagetitle', - t('Manage tags') .' - '. $this->container->conf->get('general.title', 'Shaarli') + t('Manage tags') . ' - ' . $this->container->conf->get('general.title', 'Shaarli') ); return $response->write($this->render(TemplatePage::CHANGE_TAG)); @@ -87,7 +87,7 @@ class ManageTagController extends ShaarliAdminController $this->saveSuccessMessage($alert); - $redirect = true === $isDelete ? '/admin/tags' : '/?searchtags='. urlencode($toTag); + $redirect = true === $isDelete ? '/admin/tags' : '/?searchtags=' . urlencode($toTag); return $this->redirect($response, $redirect); } diff --git a/application/front/controller/admin/PasswordController.php b/application/front/controller/admin/PasswordController.php index 5ec0d24b..4aaf1f82 100644 --- a/application/front/controller/admin/PasswordController.php +++ b/application/front/controller/admin/PasswordController.php @@ -25,7 +25,7 @@ class PasswordController extends ShaarliAdminController $this->assignView( 'pagetitle', - t('Change password') .' - '. $this->container->conf->get('general.title', 'Shaarli') + t('Change password') . ' - ' . $this->container->conf->get('general.title', 'Shaarli') ); } @@ -78,7 +78,7 @@ class PasswordController extends ShaarliAdminController // Save new password // Salt renders rainbow-tables attacks useless. - $this->container->conf->set('credentials.salt', sha1(uniqid('', true) .'_'. mt_rand())); + $this->container->conf->set('credentials.salt', sha1(uniqid('', true) . '_' . mt_rand())); $this->container->conf->set( 'credentials.hash', sha1( diff --git a/application/front/controller/admin/PluginsController.php b/application/front/controller/admin/PluginsController.php index 8e059681..ae47c1af 100644 --- a/application/front/controller/admin/PluginsController.php +++ b/application/front/controller/admin/PluginsController.php @@ -42,7 +42,7 @@ class PluginsController extends ShaarliAdminController $this->assignView('disabledPlugins', $disabledPlugins); $this->assignView( 'pagetitle', - t('Plugin Administration') .' - '. $this->container->conf->get('general.title', 'Shaarli') + t('Plugin Administration') . ' - ' . $this->container->conf->get('general.title', 'Shaarli') ); return $response->write($this->render(TemplatePage::PLUGINS_ADMIN)); @@ -64,7 +64,7 @@ class PluginsController extends ShaarliAdminController unset($parameters['parameters_form']); unset($parameters['token']); foreach ($parameters as $param => $value) { - $this->container->conf->set('plugins.'. $param, escape($value)); + $this->container->conf->set('plugins.' . $param, escape($value)); } } else { $this->container->conf->set('general.enabled_plugins', save_plugin_config($parameters)); diff --git a/application/front/controller/admin/ServerController.php b/application/front/controller/admin/ServerController.php index bfc99422..80997940 100644 --- a/application/front/controller/admin/ServerController.php +++ b/application/front/controller/admin/ServerController.php @@ -65,7 +65,7 @@ class ServerController extends ShaarliAdminController $this->saveWarningMessage( t('Thumbnails cache has been cleared.') . ' ' . - '' . t('Please synchronize them.') .'' + '' . t('Please synchronize them.') . '' ); } else { $folders = [ diff --git a/application/front/controller/admin/SessionFilterController.php b/application/front/controller/admin/SessionFilterController.php index d9a7a2e0..0917b6d2 100644 --- a/application/front/controller/admin/SessionFilterController.php +++ b/application/front/controller/admin/SessionFilterController.php @@ -45,6 +45,4 @@ class SessionFilterController extends ShaarliAdminController return $this->redirectFromReferer($request, $response, ['visibility']); } - - } diff --git a/application/front/controller/admin/ShaareAddController.php b/application/front/controller/admin/ShaareAddController.php index 8dc386b2..ab8e7f40 100644 --- a/application/front/controller/admin/ShaareAddController.php +++ b/application/front/controller/admin/ShaareAddController.php @@ -23,7 +23,7 @@ class ShaareAddController extends ShaarliAdminController $this->assignView( 'pagetitle', - t('Shaare a new link') .' - '. $this->container->conf->get('general.title', 'Shaarli') + 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)); diff --git a/application/front/controller/admin/ShaareManageController.php b/application/front/controller/admin/ShaareManageController.php index 0b143172..35837baa 100644 --- a/application/front/controller/admin/ShaareManageController.php +++ b/application/front/controller/admin/ShaareManageController.php @@ -54,7 +54,7 @@ class ShaareManageController extends ShaarliAdminController $data = $formatter->format($bookmark); $this->executePageHooks('delete_link', $data); $this->container->bookmarkService->remove($bookmark, false); - ++ $count; + ++$count; } if ($count > 0) { diff --git a/application/front/controller/admin/ShaarePublishController.php b/application/front/controller/admin/ShaarePublishController.php index 625a5680..4cbfcdc5 100644 --- a/application/front/controller/admin/ShaarePublishController.php +++ b/application/front/controller/admin/ShaarePublishController.php @@ -118,7 +118,8 @@ class ShaarePublishController extends ShaarliAdminController $this->container->conf->get('general.tags_separator', ' ') ); - if ($this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE + 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() ) { @@ -148,7 +149,8 @@ class ShaarePublishController extends ShaarliAdminController return $this->redirectFromReferer( $request, $response, - ['/admin/add-shaare', '/admin/shaare'], ['addlink', 'post', 'edit_link'], + ['/admin/add-shaare', '/admin/shaare'], + ['addlink', 'post', 'edit_link'], $bookmark->getShortUrl() ); } @@ -168,10 +170,10 @@ class ShaarePublishController extends ShaarliAdminController $this->assignView($key, $value); } - $editLabel = false === $isNew ? t('Edit') .' ' : ''; + $editLabel = false === $isNew ? t('Edit') . ' ' : ''; $this->assignView( 'pagetitle', - $editLabel . t('Shaare') .' - '. $this->container->conf->get('general.title', 'Shaarli') + $editLabel . t('Shaare') . ' - ' . $this->container->conf->get('general.title', 'Shaarli') ); return $response->write($this->render(TemplatePage::EDIT_LINK)); @@ -194,7 +196,8 @@ class ShaarePublishController extends ShaarliAdminController // 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) + if ( + true !== $this->container->conf->get('general.enable_async_metadata', true) && empty($title) && strpos(get_url_scheme($url) ?: '', 'http') !== false ) { diff --git a/application/front/controller/admin/ThumbnailsController.php b/application/front/controller/admin/ThumbnailsController.php index 4dc09d38..94d97d4b 100644 --- a/application/front/controller/admin/ThumbnailsController.php +++ b/application/front/controller/admin/ThumbnailsController.php @@ -34,7 +34,7 @@ class ThumbnailsController extends ShaarliAdminController $this->assignView('ids', $ids); $this->assignView( 'pagetitle', - t('Thumbnails update') .' - '. $this->container->conf->get('general.title', 'Shaarli') + t('Thumbnails update') . ' - ' . $this->container->conf->get('general.title', 'Shaarli') ); return $response->write($this->render(TemplatePage::THUMBNAILS)); diff --git a/application/front/controller/admin/ToolsController.php b/application/front/controller/admin/ToolsController.php index a87f20d2..560e5e3e 100644 --- a/application/front/controller/admin/ToolsController.php +++ b/application/front/controller/admin/ToolsController.php @@ -28,7 +28,7 @@ class ToolsController extends ShaarliAdminController $this->assignView($key, $value); } - $this->assignView('pagetitle', t('Tools') .' - '. $this->container->conf->get('general.title', 'Shaarli')); + $this->assignView('pagetitle', t('Tools') . ' - ' . $this->container->conf->get('general.title', 'Shaarli')); return $response->write($this->render(TemplatePage::TOOLS)); } diff --git a/application/front/controller/visitor/BookmarkListController.php b/application/front/controller/visitor/BookmarkListController.php index cc3837ce..fe8231be 100644 --- a/application/front/controller/visitor/BookmarkListController.php +++ b/application/front/controller/visitor/BookmarkListController.php @@ -35,7 +35,8 @@ class BookmarkListController extends ShaarliVisitorController $formatter->addContextData('base_path', $this->container->basePath); $searchTags = normalize_spaces($request->getParam('searchtags') ?? ''); - $searchTerm = escape(normalize_spaces($request->getParam('searchterm') ?? ''));; + $searchTerm = escape(normalize_spaces($request->getParam('searchterm') ?? '')); + ; // Filter bookmarks according search parameters. $visibility = $this->container->sessionManager->getSessionParameter('visibility'); @@ -160,7 +161,7 @@ class BookmarkListController extends ShaarliVisitorController $data = array_merge( $this->initializeTemplateVars(), [ - 'pagetitle' => $bookmark->getTitle() .' - '. $this->container->conf->get('general.title', 'Shaarli'), + 'pagetitle' => $bookmark->getTitle() . ' - ' . $this->container->conf->get('general.title', 'Shaarli'), 'links' => [$formatter->format($bookmark)], ] ); @@ -185,7 +186,8 @@ class BookmarkListController extends ShaarliVisitorController $bookmark->setThumbnail(null); // Requires an update, not async retrieval, thumbnails enabled - if ($bookmark->shouldUpdateThumbnail() + 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 ) { diff --git a/application/front/controller/visitor/DailyController.php b/application/front/controller/visitor/DailyController.php index 728bc2d8..846cfe22 100644 --- a/application/front/controller/visitor/DailyController.php +++ b/application/front/controller/visitor/DailyController.php @@ -132,7 +132,7 @@ class DailyController extends ShaarliVisitorController 'date' => $endDateTime, 'date_rss' => $endDateTime->format(DateTime::RSS), 'date_human' => DailyPageHelper::getDescriptionByType($type, $dayDateTime), - 'absolute_url' => $indexUrl . 'daily?'. $type .'=' . $day, + 'absolute_url' => $indexUrl . 'daily?' . $type . '=' . $day, 'links' => [], ]; diff --git a/application/front/controller/visitor/FeedController.php b/application/front/controller/visitor/FeedController.php index 8d8b546a..edc7ef43 100644 --- a/application/front/controller/visitor/FeedController.php +++ b/application/front/controller/visitor/FeedController.php @@ -27,7 +27,7 @@ class FeedController extends ShaarliVisitorController protected function processRequest(string $feedType, Request $request, Response $response): Response { - $response = $response->withHeader('Content-Type', 'application/'. $feedType .'+xml; charset=utf-8'); + $response = $response->withHeader('Content-Type', 'application/' . $feedType . '+xml; charset=utf-8'); $pageUrl = page_url($this->container->environment); $cache = $this->container->pageCacheManager->getCachePage($pageUrl); diff --git a/application/front/controller/visitor/InstallController.php b/application/front/controller/visitor/InstallController.php index 22329294..bf965929 100644 --- a/application/front/controller/visitor/InstallController.php +++ b/application/front/controller/visitor/InstallController.php @@ -39,7 +39,8 @@ class InstallController extends ShaarliVisitorController // Before installation, we'll make sure that permissions are set properly, and sessions are working. $this->checkPermissions(); - if (static::SESSION_TEST_VALUE + if ( + static::SESSION_TEST_VALUE !== $this->container->sessionManager->getSessionParameter(static::SESSION_TEST_KEY) ) { $this->container->sessionManager->setSessionParameter(static::SESSION_TEST_KEY, static::SESSION_TEST_VALUE); @@ -75,17 +76,18 @@ class InstallController extends ShaarliVisitorController // This part makes sure sessions works correctly. // (Because on some hosts, session.save_path may not be set correctly, // or we may not have write access to it.) - if (static::SESSION_TEST_VALUE + if ( + static::SESSION_TEST_VALUE !== $this->container->sessionManager->getSessionParameter(static::SESSION_TEST_KEY) ) { // Step 2: Check if data in session is correct. $msg = t( - '
Sessions do not seem to work correctly on your server.
'. - 'Make sure the variable "session.save_path" is set correctly in your PHP config, '. - 'and that you have write access to it.
'. - 'It currently points to %s.
'. - 'On some browsers, accessing your server via a hostname like \'localhost\' '. - 'or any custom hostname without a dot causes cookie storage to fail. '. + '
Sessions do not seem to work correctly on your server.
' . + 'Make sure the variable "session.save_path" is set correctly in your PHP config, ' . + 'and that you have write access to it.
' . + 'It currently points to %s.
' . + 'On some browsers, accessing your server via a hostname like \'localhost\' ' . + 'or any custom hostname without a dot causes cookie storage to fail. ' . 'We recommend accessing your server via it\'s IP address or Fully Qualified Domain Name.
' ); $msg = sprintf($msg, $this->container->sessionManager->getSavePath()); @@ -104,7 +106,8 @@ class InstallController extends ShaarliVisitorController public function save(Request $request, Response $response): Response { $timezone = 'UTC'; - if (!empty($request->getParam('continent')) + if ( + !empty($request->getParam('continent')) && !empty($request->getParam('city')) && isTimeZoneValid($request->getParam('continent'), $request->getParam('city')) ) { @@ -114,7 +117,7 @@ class InstallController extends ShaarliVisitorController $login = $request->getParam('setlogin'); $this->container->conf->set('credentials.login', $login); - $salt = sha1(uniqid('', true) .'_'. mt_rand()); + $salt = sha1(uniqid('', true) . '_' . mt_rand()); $this->container->conf->set('credentials.salt', $salt); $this->container->conf->set('credentials.hash', sha1($request->getParam('setpassword') . $login . $salt)); @@ -123,7 +126,7 @@ class InstallController extends ShaarliVisitorController } else { $this->container->conf->set( 'general.title', - 'Shared bookmarks on '.escape(index_url($this->container->environment)) + 'Shared bookmarks on ' . escape(index_url($this->container->environment)) ); } diff --git a/application/front/controller/visitor/LoginController.php b/application/front/controller/visitor/LoginController.php index f5038fe3..4b881535 100644 --- a/application/front/controller/visitor/LoginController.php +++ b/application/front/controller/visitor/LoginController.php @@ -43,7 +43,7 @@ class LoginController extends ShaarliVisitorController $this ->assignView('returnurl', escape($returnUrl)) ->assignView('remember_user_default', $this->container->conf->get('privacy.remember_user_default', true)) - ->assignView('pagetitle', t('Login') .' - '. $this->container->conf->get('general.title', 'Shaarli')) + ->assignView('pagetitle', t('Login') . ' - ' . $this->container->conf->get('general.title', 'Shaarli')) ; return $response->write($this->render(TemplatePage::LOGIN)); @@ -64,7 +64,8 @@ class LoginController extends ShaarliVisitorController return $this->redirect($response, '/'); } - if (!$this->container->loginManager->checkCredentials( + if ( + !$this->container->loginManager->checkCredentials( client_ip_id($this->container->environment), $request->getParam('login'), $request->getParam('password') @@ -101,7 +102,8 @@ class LoginController extends ShaarliVisitorController */ protected function checkLoginState(): bool { - if ($this->container->loginManager->isLoggedIn() + if ( + $this->container->loginManager->isLoggedIn() || $this->container->conf->get('security.open_shaarli', false) ) { throw new CantLoginException(); diff --git a/application/front/controller/visitor/PictureWallController.php b/application/front/controller/visitor/PictureWallController.php index 3c57f8dd..23553ee6 100644 --- a/application/front/controller/visitor/PictureWallController.php +++ b/application/front/controller/visitor/PictureWallController.php @@ -26,7 +26,7 @@ class PictureWallController extends ShaarliVisitorController $this->assignView( 'pagetitle', - t('Picture wall') .' - '. $this->container->conf->get('general.title', 'Shaarli') + t('Picture wall') . ' - ' . $this->container->conf->get('general.title', 'Shaarli') ); // Optionally filter the results: diff --git a/application/front/controller/visitor/ShaarliVisitorController.php b/application/front/controller/visitor/ShaarliVisitorController.php index 54f9fe03..ae946c59 100644 --- a/application/front/controller/visitor/ShaarliVisitorController.php +++ b/application/front/controller/visitor/ShaarliVisitorController.php @@ -144,7 +144,8 @@ abstract class ShaarliVisitorController if (null !== $referer) { $currentUrl = parse_url($referer); // If the referer is not related to Shaarli instance, redirect to default - if (isset($currentUrl['host']) + if ( + isset($currentUrl['host']) && strpos(index_url($this->container->environment), $currentUrl['host']) === false ) { return $response->withRedirect($defaultPath); @@ -173,7 +174,7 @@ abstract class ShaarliVisitorController } } - $queryString = count($params) > 0 ? '?'. http_build_query($params) : ''; + $queryString = count($params) > 0 ? '?' . http_build_query($params) : ''; $anchor = $anchor ? '#' . $anchor : ''; return $response->withRedirect($path . $queryString . $anchor); diff --git a/application/front/controller/visitor/TagCloudController.php b/application/front/controller/visitor/TagCloudController.php index 560cad08..46d62779 100644 --- a/application/front/controller/visitor/TagCloudController.php +++ b/application/front/controller/visitor/TagCloudController.php @@ -84,10 +84,10 @@ class TagCloudController extends ShaarliVisitorController $this->executePageHooks('render_tag' . $type, $data, 'tag.' . $type); $this->assignAllView($data); - $searchTags = !empty($searchTags) ? trim(str_replace($tagsSeparator, ' ', $searchTags)) .' - ' : ''; + $searchTags = !empty($searchTags) ? trim(str_replace($tagsSeparator, ' ', $searchTags)) . ' - ' : ''; $this->assignView( 'pagetitle', - $searchTags . t('Tag '. $type) .' - '. $this->container->conf->get('general.title', 'Shaarli') + $searchTags . t('Tag ' . $type) . ' - ' . $this->container->conf->get('general.title', 'Shaarli') ); return $response->write($this->render('tag.' . $type)); diff --git a/application/front/controller/visitor/TagController.php b/application/front/controller/visitor/TagController.php index 7a3377a7..3aa58542 100644 --- a/application/front/controller/visitor/TagController.php +++ b/application/front/controller/visitor/TagController.php @@ -27,7 +27,7 @@ class TagController extends ShaarliVisitorController // In case browser does not send HTTP_REFERER, we search a single tag if (null === $referer) { if (null !== $newTag) { - return $this->redirect($response, '/?searchtags='. urlencode($newTag)); + return $this->redirect($response, '/?searchtags=' . urlencode($newTag)); } return $this->redirect($response, '/'); @@ -37,7 +37,7 @@ class TagController extends ShaarliVisitorController parse_str($currentUrl['query'] ?? '', $params); if (null === $newTag) { - return $response->withRedirect(($currentUrl['path'] ?? './') .'?'. http_build_query($params)); + return $response->withRedirect(($currentUrl['path'] ?? './') . '?' . http_build_query($params)); } // Prevent redirection loop @@ -68,7 +68,7 @@ class TagController extends ShaarliVisitorController // We also remove page (keeping the same page has no sense, since the results are different) unset($params['page']); - return $response->withRedirect(($currentUrl['path'] ?? './') .'?'. http_build_query($params)); + return $response->withRedirect(($currentUrl['path'] ?? './') . '?' . http_build_query($params)); } /** @@ -90,7 +90,7 @@ class TagController extends ShaarliVisitorController parse_str($currentUrl['query'] ?? '', $params); if (null === $tagToRemove) { - return $response->withRedirect(($currentUrl['path'] ?? './') .'?'. http_build_query($params)); + return $response->withRedirect(($currentUrl['path'] ?? './') . '?' . http_build_query($params)); } // Prevent redirection loop diff --git a/application/helper/ApplicationUtils.php b/application/helper/ApplicationUtils.php index 4b34e114..212dd8e2 100644 --- a/application/helper/ApplicationUtils.php +++ b/application/helper/ApplicationUtils.php @@ -1,4 +1,5 @@ '; @@ -64,8 +65,8 @@ class ApplicationUtils } return str_replace( - array(self::$VERSION_START_TAG, self::$VERSION_END_TAG, PHP_EOL), - array('', '', ''), + [self::$VERSION_START_TAG, self::$VERSION_END_TAG, PHP_EOL], + ['', '', ''], $data ); } @@ -184,13 +185,15 @@ class ApplicationUtils $rainTplDir = rtrim($conf->get('resource.raintpl_tpl'), '/'); // Check script and template directories are readable - foreach ([ - 'application', - 'inc', - 'plugins', - $rainTplDir, - $rainTplDir . '/' . $conf->get('resource.theme'), - ] as $path) { + foreach ( + [ + 'application', + 'inc', + 'plugins', + $rainTplDir, + $rainTplDir . '/' . $conf->get('resource.theme'), + ] as $path + ) { if (!is_readable(realpath($path))) { $errors[] = '"' . $path . '" ' . t('directory is not readable'); } @@ -203,10 +206,10 @@ class ApplicationUtils ]; } else { $folders = [ - $conf->get('resource.thumbnails_cache'), - $conf->get('resource.data_dir'), - $conf->get('resource.page_cache'), - $conf->get('resource.raintpl_tmp'), + $conf->get('resource.thumbnails_cache'), + $conf->get('resource.data_dir'), + $conf->get('resource.page_cache'), + $conf->get('resource.raintpl_tmp'), ]; } @@ -224,13 +227,15 @@ class ApplicationUtils } // Check configuration files are readable and writable - foreach (array( - $conf->getConfigFileExt(), - $conf->get('resource.datastore'), - $conf->get('resource.ban_file'), - $conf->get('resource.log'), - $conf->get('resource.update_check'), - ) as $path) { + foreach ( + [ + $conf->getConfigFileExt(), + $conf->get('resource.datastore'), + $conf->get('resource.ban_file'), + $conf->get('resource.log'), + $conf->get('resource.update_check'), + ] as $path + ) { if (!is_file(realpath($path))) { # the file may not exist yet continue; diff --git a/application/helper/FileUtils.php b/application/helper/FileUtils.php index 2eac0793..e8a2168c 100644 --- a/application/helper/FileUtils.php +++ b/application/helper/FileUtils.php @@ -105,7 +105,7 @@ class FileUtils } foreach (new \DirectoryIterator($path) as $file) { - if($file->isDot()) { + if ($file->isDot()) { continue; } @@ -116,7 +116,7 @@ class FileUtils if ($file->isFile()) { unlink($file->getPathname()); - } elseif($file->isDir()) { + } elseif ($file->isDir()) { $skipped = static::clearFolder($file->getRealPath(), true, $exclude) || $skipped; } } diff --git a/application/http/HttpUtils.php b/application/http/HttpUtils.php index ed1002b0..4bde1d5b 100644 --- a/application/http/HttpUtils.php +++ b/application/http/HttpUtils.php @@ -48,7 +48,7 @@ function get_http_response( $cleanUrl = $urlObj->idnToAscii(); if (!filter_var($cleanUrl, FILTER_VALIDATE_URL) || !$urlObj->isHttp()) { - return array(array(0 => 'Invalid HTTP UrlUtils'), false); + return [[0 => 'Invalid HTTP UrlUtils'], false]; } $userAgent = @@ -71,7 +71,7 @@ function get_http_response( $ch = curl_init($cleanUrl); if ($ch === false) { - return array(array(0 => 'curl_init() error'), false); + return [[0 => 'curl_init() error'], false]; } // General cURL settings @@ -82,7 +82,7 @@ function get_http_response( curl_setopt( $ch, CURLOPT_HTTPHEADER, - array('Accept-Language: ' . $acceptLanguage) + ['Accept-Language: ' . $acceptLanguage] ); curl_setopt($ch, CURLOPT_MAXREDIRS, $maxRedirs); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); @@ -90,7 +90,7 @@ function get_http_response( curl_setopt($ch, CURLOPT_USERAGENT, $userAgent); // Max download size management - curl_setopt($ch, CURLOPT_BUFFERSIZE, 1024*16); + curl_setopt($ch, CURLOPT_BUFFERSIZE, 1024 * 16); curl_setopt($ch, CURLOPT_NOPROGRESS, false); if (is_callable($curlHeaderFunction)) { curl_setopt($ch, CURLOPT_HEADERFUNCTION, $curlHeaderFunction); @@ -122,9 +122,9 @@ function get_http_response( * Removing this would require updating * GetHttpUrlTest::testGetInvalidRemoteUrl() */ - return array(false, false); + return [false, false]; } - return array(array(0 => 'curl_exec() error: ' . $errorStr), false); + return [[0 => 'curl_exec() error: ' . $errorStr], false]; } // Formatting output like the fallback method @@ -135,7 +135,7 @@ function get_http_response( $rawHeadersLastRedir = end($rawHeadersArrayRedirs); $content = substr($response, $headSize); - $headers = array(); + $headers = []; foreach (preg_split('~[\r\n]+~', $rawHeadersLastRedir) as $line) { if (empty($line) || ctype_space($line)) { continue; @@ -146,7 +146,7 @@ function get_http_response( $value = $splitLine[1]; if (array_key_exists($key, $headers)) { if (!is_array($headers[$key])) { - $headers[$key] = array(0 => $headers[$key]); + $headers[$key] = [0 => $headers[$key]]; } $headers[$key][] = $value; } else { @@ -157,7 +157,7 @@ function get_http_response( } } - return array($headers, $content); + return [$headers, $content]; } /** @@ -188,15 +188,15 @@ function get_http_response_fallback( $acceptLanguage, $maxRedr ) { - $options = array( - 'http' => array( + $options = [ + 'http' => [ 'method' => 'GET', 'timeout' => $timeout, 'user_agent' => $userAgent, 'header' => "Accept: */*\r\n" . 'Accept-Language: ' . $acceptLanguage - ) - ); + ] + ]; stream_context_set_default($options); list($headers, $finalUrl) = get_redirected_headers($cleanUrl, $maxRedr); @@ -207,7 +207,7 @@ function get_http_response_fallback( } if (! $headers) { - return array($headers, false); + return [$headers, false]; } try { @@ -215,10 +215,10 @@ function get_http_response_fallback( $context = stream_context_create($options); $content = file_get_contents($finalUrl, false, $context, -1, $maxBytes); } catch (Exception $exc) { - return array(array(0 => 'HTTP Error'), $exc->getMessage()); + return [[0 => 'HTTP Error'], $exc->getMessage()]; } - return array($headers, $content); + return [$headers, $content]; } /** @@ -237,10 +237,12 @@ function get_redirected_headers($url, $redirectionLimit = 3) } // Headers found, redirection found, and limit not reached. - if ($redirectionLimit-- > 0 + if ( + $redirectionLimit-- > 0 && !empty($headers) && (strpos($headers[0], '301') !== false || strpos($headers[0], '302') !== false) - && !empty($headers['Location'])) { + && !empty($headers['Location']) + ) { $redirection = is_array($headers['Location']) ? end($headers['Location']) : $headers['Location']; if ($redirection != $url) { $redirection = getAbsoluteUrl($url, $redirection); @@ -248,7 +250,7 @@ function get_redirected_headers($url, $redirectionLimit = 3) } } - return array($headers, $url); + return [$headers, $url]; } /** @@ -270,7 +272,7 @@ function getAbsoluteUrl($originalUrl, $newUrl) } $parts = parse_url($originalUrl); - $final = $parts['scheme'] .'://'. $parts['host']; + $final = $parts['scheme'] . '://' . $parts['host']; $final .= (!empty($parts['port'])) ? $parts['port'] : ''; $final .= '/'; if ($newUrl[0] != '/') { @@ -323,7 +325,8 @@ function server_url($server) $scheme = 'https'; } - if (($scheme == 'http' && $port != '80') + if ( + ($scheme == 'http' && $port != '80') || ($scheme == 'https' && $port != '443') ) { $port = ':' . $port; @@ -344,22 +347,26 @@ function server_url($server) $host = $server['SERVER_NAME']; } - return $scheme.'://'.$host.$port; + return $scheme . '://' . $host . $port; } // SSL detection - if ((! empty($server['HTTPS']) && strtolower($server['HTTPS']) == 'on') - || (isset($server['SERVER_PORT']) && $server['SERVER_PORT'] == '443')) { + if ( + (! empty($server['HTTPS']) && strtolower($server['HTTPS']) == 'on') + || (isset($server['SERVER_PORT']) && $server['SERVER_PORT'] == '443') + ) { $scheme = 'https'; } // Do not append standard port values - if (($scheme == 'http' && $server['SERVER_PORT'] != '80') - || ($scheme == 'https' && $server['SERVER_PORT'] != '443')) { - $port = ':'.$server['SERVER_PORT']; + if ( + ($scheme == 'http' && $server['SERVER_PORT'] != '80') + || ($scheme == 'https' && $server['SERVER_PORT'] != '443') + ) { + $port = ':' . $server['SERVER_PORT']; } - return $scheme.'://'.$server['SERVER_NAME'].$port; + return $scheme . '://' . $server['SERVER_NAME'] . $port; } /** @@ -567,7 +574,10 @@ function get_curl_download_callback( * * @return int|bool length of $data or false if we need to stop the download */ - return function ($ch, $data) use ( + return function ( + $ch, + $data + ) use ( $retrieveDescription, $tagsSeparator, &$charset, @@ -601,7 +611,7 @@ function get_curl_download_callback( $foundChunk = $currentChunk; // Keywords use the format tag1, tag2 multiple words, tag // So we split the result with `,`, then if a tag contains the separator we replace it by `-`. - $keywords = tags_array2str(array_map(function(string $keyword) use ($tagsSeparator): string { + $keywords = tags_array2str(array_map(function (string $keyword) use ($tagsSeparator): string { return tags_array2str(tags_str2array($keyword, $tagsSeparator), '-'); }, tags_str2array($keywords, ',')), $tagsSeparator); } @@ -611,7 +621,8 @@ function get_curl_download_callback( // If we already found either the title, description or keywords, // it's highly unlikely that we'll found the other metas further than // in the same chunk of data or the next one. So we also stop the download after that. - if ((!empty($responseCode) && !empty($contentType) && !empty($charset)) && $foundChunk !== null + if ( + (!empty($responseCode) && !empty($contentType) && !empty($charset)) && $foundChunk !== null && (! $retrieveDescription || $foundChunk < $currentChunk || (!empty($title) && !empty($description) && !empty($keywords)) diff --git a/application/http/Url.php b/application/http/Url.php index 90444a2f..fe87088f 100644 --- a/application/http/Url.php +++ b/application/http/Url.php @@ -17,7 +17,7 @@ namespace Shaarli\Http; */ class Url { - private static $annoyingQueryParams = array( + private static $annoyingQueryParams = [ // Facebook 'action_object_map=', 'action_ref_map=', @@ -37,15 +37,15 @@ class Url // Other 'campaign_' - ); + ]; - private static $annoyingFragments = array( + private static $annoyingFragments = [ // ATInternet 'xtor=RSS-', // Misc. 'tk.rss_all' - ); + ]; /* * URL parts represented as an array @@ -120,7 +120,7 @@ class Url foreach (self::$annoyingQueryParams as $annoying) { foreach ($queryParams as $param) { if (startsWith($param, $annoying)) { - $queryParams = array_diff($queryParams, array($param)); + $queryParams = array_diff($queryParams, [$param]); continue; } } diff --git a/application/http/UrlUtils.php b/application/http/UrlUtils.php index e8d1a283..de5b7db1 100644 --- a/application/http/UrlUtils.php +++ b/application/http/UrlUtils.php @@ -1,4 +1,5 @@ container->loginManager->isLoggedIn()) { $parameters = $buildParameters($request->getQueryParams(), true); - return $this->redirect($response, '/login?returnurl='. $this->getBasePath() . $route . $parameters); + return $this->redirect($response, '/login?returnurl=' . $this->getBasePath() . $route . $parameters); } $parameters = $buildParameters($request->getQueryParams(), false); diff --git a/application/legacy/LegacyLinkDB.php b/application/legacy/LegacyLinkDB.php index 5c02a21b..442b833c 100644 --- a/application/legacy/LegacyLinkDB.php +++ b/application/legacy/LegacyLinkDB.php @@ -240,8 +240,8 @@ class LegacyLinkDB implements Iterator, Countable, ArrayAccess } // Create a dummy database for example - $this->links = array(); - $link = array( + $this->links = []; + $link = [ 'id' => 1, 'title' => t('The personal, minimalist, super-fast, database free, bookmarking service'), 'url' => 'https://shaarli.readthedocs.io', @@ -257,11 +257,11 @@ You use the community supported version of the original Shaarli project, by Seba 'created' => new DateTime(), 'tags' => 'opensource software', 'sticky' => false, - ); + ]; $link['shorturl'] = link_small_hash($link['created'], $link['id']); $this->links[1] = $link; - $link = array( + $link = [ 'id' => 0, 'title' => t('My secret stuff... - Pastebin.com'), 'url' => 'http://sebsauvage.net/paste/?8434b27936c09649#bR7XsXhoTiLcqCpQbmOpBi3rq2zzQUC5hBI7ZT1O3x8=', @@ -270,7 +270,7 @@ You use the community supported version of the original Shaarli project, by Seba 'created' => new DateTime('1 minute ago'), 'tags' => 'secretstuff', 'sticky' => false, - ); + ]; $link['shorturl'] = link_small_hash($link['created'], $link['id']); $this->links[0] = $link; @@ -285,7 +285,7 @@ You use the community supported version of the original Shaarli project, by Seba { // Public bookmarks are hidden and user not logged in => nothing to show if ($this->hidePublicLinks && !$this->loggedIn) { - $this->links = array(); + $this->links = []; return; } @@ -293,7 +293,7 @@ You use the community supported version of the original Shaarli project, by Seba $this->ids = []; $this->links = FileUtils::readFlatDB($this->datastore, []); - $toremove = array(); + $toremove = []; foreach ($this->links as $key => &$link) { if (!$this->loggedIn && $link['private'] != 0) { // Transition for not upgraded databases. @@ -414,7 +414,7 @@ You use the community supported version of the original Shaarli project, by Seba * @return array filtered bookmarks, all bookmarks if no suitable filter was provided. */ public function filterSearch( - $filterRequest = array(), + $filterRequest = [], $casesensitive = false, $visibility = 'all', $untaggedonly = false @@ -512,7 +512,7 @@ You use the community supported version of the original Shaarli project, by Seba */ public function days() { - $linkDays = array(); + $linkDays = []; foreach ($this->links as $link) { $linkDays[$link['created']->format('Ymd')] = 0; } diff --git a/application/legacy/LegacyLinkFilter.php b/application/legacy/LegacyLinkFilter.php index 7cf93d60..e6d186c4 100644 --- a/application/legacy/LegacyLinkFilter.php +++ b/application/legacy/LegacyLinkFilter.php @@ -120,7 +120,7 @@ class LegacyLinkFilter return $this->links; } - $out = array(); + $out = []; foreach ($this->links as $key => $value) { if ($value['private'] && $visibility === 'private') { $out[$key] = $value; @@ -143,7 +143,7 @@ class LegacyLinkFilter */ private function filterSmallHash($smallHash) { - $filtered = array(); + $filtered = []; foreach ($this->links as $key => $l) { if ($smallHash == $l['shorturl']) { // Yes, this is ugly and slow @@ -186,7 +186,7 @@ class LegacyLinkFilter return $this->noFilter($visibility); } - $filtered = array(); + $filtered = []; $search = mb_convert_case(html_entity_decode($searchterms), MB_CASE_LOWER, 'UTF-8'); $exactRegex = '/"([^"]+)"/'; // Retrieve exact search terms. @@ -198,8 +198,8 @@ class LegacyLinkFilter $explodedSearchAnd = array_values(array_filter($explodedSearchAnd)); // Filter excluding terms and update andSearch. - $excludeSearch = array(); - $andSearch = array(); + $excludeSearch = []; + $andSearch = []; foreach ($explodedSearchAnd as $needle) { if ($needle[0] == '-' && strlen($needle) > 1) { $excludeSearch[] = substr($needle, 1); @@ -208,7 +208,7 @@ class LegacyLinkFilter } } - $keys = array('title', 'description', 'url', 'tags'); + $keys = ['title', 'description', 'url', 'tags']; // Iterate over every stored link. foreach ($this->links as $id => $link) { @@ -336,7 +336,7 @@ class LegacyLinkFilter } // create resulting array - $filtered = array(); + $filtered = []; // iterate over each link foreach ($this->links as $key => $link) { @@ -352,7 +352,7 @@ class LegacyLinkFilter $search = $link['tags']; // build search string, start with tags of current link if (strlen(trim($link['description'])) && strpos($link['description'], '#') !== false) { // description given and at least one possible tag found - $descTags = array(); + $descTags = []; // find all tags in the form of #tag in the description preg_match_all( '/(?links as $key => $l) { if ($l['created']->format('Ymd') == $day) { $filtered[$key] = $l; diff --git a/application/legacy/LegacyUpdater.php b/application/legacy/LegacyUpdater.php index ed949b1e..9bda54b8 100644 --- a/application/legacy/LegacyUpdater.php +++ b/application/legacy/LegacyUpdater.php @@ -93,7 +93,7 @@ class LegacyUpdater */ public function update() { - $updatesRan = array(); + $updatesRan = []; // If the user isn't logged in, exit without updating. if ($this->isLoggedIn !== true) { @@ -106,7 +106,8 @@ class LegacyUpdater foreach ($this->methods as $method) { // Not an update method or already done, pass. - if (!startsWith($method->getName(), 'updateMethod') + if ( + !startsWith($method->getName(), 'updateMethod') || in_array($method->getName(), $this->doneUpdates) ) { continue; @@ -189,7 +190,7 @@ class LegacyUpdater } // Set sub config keys (config and plugins) - $subConfig = array('config', 'plugins'); + $subConfig = ['config', 'plugins']; foreach ($subConfig as $sub) { foreach ($oldConfig[$sub] as $key => $value) { if (isset($legacyMap[$sub . '.' . $key])) { @@ -259,7 +260,7 @@ class LegacyUpdater $save = $this->conf->get('resource.data_dir') . '/datastore.' . date('YmdHis') . '.php'; copy($this->conf->get('resource.datastore'), $save); - $links = array(); + $links = []; foreach ($this->linkDB as $offset => $value) { $links[] = $value; unset($this->linkDB[$offset]); @@ -498,7 +499,8 @@ class LegacyUpdater */ public function updateMethodDownloadSizeAndTimeoutConf() { - if ($this->conf->exists('general.download_max_size') + if ( + $this->conf->exists('general.download_max_size') && $this->conf->exists('general.download_timeout') ) { return true; diff --git a/application/netscape/NetscapeBookmarkUtils.php b/application/netscape/NetscapeBookmarkUtils.php index 6ca728b7..2d97b4c8 100644 --- a/application/netscape/NetscapeBookmarkUtils.php +++ b/application/netscape/NetscapeBookmarkUtils.php @@ -59,11 +59,11 @@ class NetscapeBookmarkUtils $indexUrl ) { // see tpl/export.html for possible values - if (!in_array($selection, array('all', 'public', 'private'))) { + if (!in_array($selection, ['all', 'public', 'private'])) { throw new Exception(t('Invalid export selection:') . ' "' . $selection . '"'); } - $bookmarkLinks = array(); + $bookmarkLinks = []; foreach ($this->bookmarkService->search([], $selection) as $bookmark) { $link = $formatter->format($bookmark); $link['taglist'] = implode(',', $bookmark->getTags()); diff --git a/application/plugin/PluginManager.php b/application/plugin/PluginManager.php index da66dea3..3ea55728 100644 --- a/application/plugin/PluginManager.php +++ b/application/plugin/PluginManager.php @@ -1,4 +1,5 @@ conf = $conf; - $this->errors = array(); + $this->errors = []; } /** @@ -98,7 +99,7 @@ class PluginManager * * @return void */ - public function executeHooks($hook, &$data, $params = array()) + public function executeHooks($hook, &$data, $params = []) { $metadataParameters = [ 'target' => '_PAGE_', @@ -196,7 +197,7 @@ class PluginManager */ public function getPluginsMeta() { - $metaData = array(); + $metaData = []; $dirs = glob(self::$PLUGINS_PATH . '/*', GLOB_ONLYDIR | GLOB_MARK); // Browse all plugin directories. @@ -217,9 +218,9 @@ class PluginManager if (isset($metaData[$plugin]['parameters'])) { $params = explode(';', $metaData[$plugin]['parameters']); } else { - $params = array(); + $params = []; } - $metaData[$plugin]['parameters'] = array(); + $metaData[$plugin]['parameters'] = []; foreach ($params as $param) { if (empty($param)) { continue; diff --git a/application/plugin/exception/PluginFileNotFoundException.php b/application/plugin/exception/PluginFileNotFoundException.php index e5386f02..21ac6604 100644 --- a/application/plugin/exception/PluginFileNotFoundException.php +++ b/application/plugin/exception/PluginFileNotFoundException.php @@ -1,4 +1,5 @@ trustedProxies = $trustedProxies; $this->nbAttempts = $nbAttempts; $this->banDuration = $banDuration; @@ -80,7 +80,7 @@ class BanManager if ($this->failures[$ip] >= $this->nbAttempts) { $this->bans[$ip] = time() + $this->banDuration; - $this->logger->info(format_log('IP address banned from login: '. $ip, $ip)); + $this->logger->info(format_log('IP address banned from login: ' . $ip, $ip)); } $this->writeBanFile(); } @@ -136,7 +136,7 @@ class BanManager unset($this->failures[$ip]); } unset($this->bans[$ip]); - $this->logger->info(format_log('Ban lifted for: '. $ip, $ip)); + $this->logger->info(format_log('Ban lifted for: ' . $ip, $ip)); $this->writeBanFile(); return false; diff --git a/application/security/LoginManager.php b/application/security/LoginManager.php index 426e785e..b795b80e 100644 --- a/application/security/LoginManager.php +++ b/application/security/LoginManager.php @@ -1,4 +1,5 @@ sessionManager->storeLoginInfo($clientIpId); - } elseif ($this->sessionManager->hasSessionExpired() + } elseif ( + $this->sessionManager->hasSessionExpired() || $this->sessionManager->hasClientIpChanged($clientIpId) ) { $this->sessionManager->logout(); @@ -145,7 +147,8 @@ class LoginManager // Check credentials try { $useLdapLogin = !empty($this->configManager->get('ldap.host')); - if ($login === $this->configManager->get('credentials.login') + if ( + $login === $this->configManager->get('credentials.login') && ( (false === $useLdapLogin && $this->checkCredentialsFromLocalConfig($login, $password)) || (true === $useLdapLogin && $this->checkCredentialsFromLdap($login, $password)) @@ -156,7 +159,7 @@ class LoginManager return true; } - } catch(Exception $exception) { + } catch (Exception $exception) { $this->logger->info(format_log('Exception while checking credentials: ' . $exception, $clientIpId)); } @@ -174,7 +177,8 @@ class LoginManager * * @return bool true if the provided credentials are valid, false otherwise */ - public function checkCredentialsFromLocalConfig($login, $password) { + public function checkCredentialsFromLocalConfig($login, $password) + { $hash = sha1($password . $login . $this->configManager->get('credentials.salt')); return $login == $this->configManager->get('credentials.login') @@ -193,14 +197,14 @@ class LoginManager */ public function checkCredentialsFromLdap($login, $password, $connect = null, $bind = null) { - $connect = $connect ?? function($host) { + $connect = $connect ?? function ($host) { $resource = ldap_connect($host); ldap_set_option($resource, LDAP_OPT_PROTOCOL_VERSION, 3); return $resource; }; - $bind = $bind ?? function($handle, $dn, $password) { + $bind = $bind ?? function ($handle, $dn, $password) { return ldap_bind($handle, $dn, $password); }; diff --git a/application/security/SessionManager.php b/application/security/SessionManager.php index 96bf193c..f957b91a 100644 --- a/application/security/SessionManager.php +++ b/application/security/SessionManager.php @@ -1,4 +1,5 @@ conf->get('credentials.salt')); + $token = sha1(uniqid('', true) . '_' . mt_rand() . $this->conf->get('credentials.salt')); $this->session['tokens'][$token] = 1; return $token; } diff --git a/application/updater/Updater.php b/application/updater/Updater.php index 88a7bc7b..3451cf36 100644 --- a/application/updater/Updater.php +++ b/application/updater/Updater.php @@ -88,7 +88,8 @@ class Updater foreach ($this->methods as $method) { // Not an update method or already done, pass. - if (! startsWith($method->getName(), 'updateMethod') + if ( + ! startsWith($method->getName(), 'updateMethod') || in_array($method->getName(), $this->doneUpdates) ) { continue; @@ -152,7 +153,8 @@ class Updater $updated = false; foreach ($this->bookmarkService->search() as $bookmark) { - if ($bookmark->isNote() + if ( + $bookmark->isNote() && startsWith($bookmark->getUrl(), '?') && 1 === preg_match('/^\?([a-zA-Z0-9-_@]{6})($|&|#)/', $bookmark->getUrl(), $match) ) { diff --git a/application/updater/UpdaterUtils.php b/application/updater/UpdaterUtils.php index 828a49fc..908bdc39 100644 --- a/application/updater/UpdaterUtils.php +++ b/application/updater/UpdaterUtils.php @@ -19,7 +19,7 @@ class UpdaterUtils return explode(';', $content); } } - return array(); + return []; } /** @@ -38,7 +38,7 @@ class UpdaterUtils $res = file_put_contents($updatesFilepath, implode(';', $updates)); if ($res === false) { - throw new \Exception('Unable to write updates in '. $updatesFilepath . '.'); + throw new \Exception('Unable to write updates in ' . $updatesFilepath . '.'); } } } -- cgit v1.2.3 From b99e00f7cd5f7e2090f44cd97bfb426db55340c2 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sun, 8 Nov 2020 15:02:45 +0100 Subject: Manually fix remaining PHPCS errors --- application/History.php | 10 +++++----- application/Languages.php | 6 ++++-- application/Thumbnailer.php | 8 ++++---- application/Utils.php | 2 ++ application/bookmark/Bookmark.php | 2 +- application/bookmark/BookmarkFileService.php | 4 ++-- application/bookmark/BookmarkInitializer.php | 3 +++ application/container/ContainerBuilder.php | 2 +- application/formatter/BookmarkDefaultFormatter.php | 4 ++-- application/formatter/BookmarkMarkdownFormatter.php | 2 +- application/front/controller/admin/ConfigureController.php | 9 +++++++-- application/front/controller/admin/ServerController.php | 4 +++- application/legacy/LegacyLinkDB.php | 2 +- application/updater/Updater.php | 4 ++-- application/updater/UpdaterUtils.php | 4 ++-- 15 files changed, 40 insertions(+), 26 deletions(-) (limited to 'application') diff --git a/application/History.php b/application/History.php index 1be955c5..d230f39d 100644 --- a/application/History.php +++ b/application/History.php @@ -32,27 +32,27 @@ class History /** * @var string Action key: a new link has been created. */ - const CREATED = 'CREATED'; + public const CREATED = 'CREATED'; /** * @var string Action key: a link has been updated. */ - const UPDATED = 'UPDATED'; + public const UPDATED = 'UPDATED'; /** * @var string Action key: a link has been deleted. */ - const DELETED = 'DELETED'; + public const DELETED = 'DELETED'; /** * @var string Action key: settings have been updated. */ - const SETTINGS = 'SETTINGS'; + public const SETTINGS = 'SETTINGS'; /** * @var string Action key: a bulk import has been processed. */ - const IMPORT = 'IMPORT'; + public const IMPORT = 'IMPORT'; /** * @var string History file path. diff --git a/application/Languages.php b/application/Languages.php index 8d0e13c8..60e91631 100644 --- a/application/Languages.php +++ b/application/Languages.php @@ -41,7 +41,7 @@ class Languages /** * Core translations domain */ - const DEFAULT_DOMAIN = 'shaarli'; + public const DEFAULT_DOMAIN = 'shaarli'; /** * @var TranslatorInterface @@ -122,7 +122,9 @@ class Languages $translations = new Translations(); // Core translations try { - $translations = $translations->addFromPoFile('inc/languages/' . $this->language . '/LC_MESSAGES/shaarli.po'); + $translations = $translations->addFromPoFile( + 'inc/languages/' . $this->language . '/LC_MESSAGES/shaarli.po' + ); $translations->setDomain('shaarli'); $this->translator->loadTranslations($translations); } catch (\InvalidArgumentException $e) { diff --git a/application/Thumbnailer.php b/application/Thumbnailer.php index 30354310..c4ff8d7a 100644 --- a/application/Thumbnailer.php +++ b/application/Thumbnailer.php @@ -13,7 +13,7 @@ use WebThumbnailer\WebThumbnailer; */ class Thumbnailer { - const COMMON_MEDIA_DOMAINS = [ + protected const COMMON_MEDIA_DOMAINS = [ 'imgur.com', 'flickr.com', 'youtube.com', @@ -31,9 +31,9 @@ class Thumbnailer 'deviantart.com', ]; - const MODE_ALL = 'all'; - const MODE_COMMON = 'common'; - const MODE_NONE = 'none'; + public const MODE_ALL = 'all'; + public const MODE_COMMON = 'common'; + public const MODE_NONE = 'none'; /** * @var WebThumbnailer instance. diff --git a/application/Utils.php b/application/Utils.php index 4c2d6701..952378ab 100644 --- a/application/Utils.php +++ b/application/Utils.php @@ -382,8 +382,10 @@ function return_bytes($val) switch ($last) { case 'g': $val *= 1024; + // do no break in order 1024^2 for each unit case 'm': $val *= 1024; + // do no break in order 1024^2 for each unit case 'k': $val *= 1024; } diff --git a/application/bookmark/Bookmark.php b/application/bookmark/Bookmark.php index b592722f..4238ef25 100644 --- a/application/bookmark/Bookmark.php +++ b/application/bookmark/Bookmark.php @@ -19,7 +19,7 @@ use Shaarli\Bookmark\Exception\InvalidBookmarkException; class Bookmark { /** @var string Date format used in string (former ID format) */ - const LINK_DATE_FORMAT = 'Ymd_His'; + public const LINK_DATE_FORMAT = 'Ymd_His'; /** @var int Bookmark ID */ protected $id; diff --git a/application/bookmark/BookmarkFileService.php b/application/bookmark/BookmarkFileService.php index 66248cc2..6666a251 100644 --- a/application/bookmark/BookmarkFileService.php +++ b/application/bookmark/BookmarkFileService.php @@ -409,14 +409,14 @@ class BookmarkFileService implements BookmarkServiceInterface false ); $updater = new LegacyUpdater( - UpdaterUtils::read_updates_file($this->conf->get('resource.updates')), + UpdaterUtils::readUpdatesFile($this->conf->get('resource.updates')), $bookmarkDb, $this->conf, true ); $newUpdates = $updater->update(); if (! empty($newUpdates)) { - UpdaterUtils::write_updates_file( + UpdaterUtils::writeUpdatesFile( $this->conf->get('resource.updates'), $updater->getDoneUpdates() ); diff --git a/application/bookmark/BookmarkInitializer.php b/application/bookmark/BookmarkInitializer.php index 2240f58c..8ab5c441 100644 --- a/application/bookmark/BookmarkInitializer.php +++ b/application/bookmark/BookmarkInitializer.php @@ -13,6 +13,9 @@ namespace Shaarli\Bookmark; * To prevent data corruption, it does not overwrite existing bookmarks, * even though there should not be any. * + * We disable this because otherwise it creates indentation issues, and heredoc is not supported by PHP gettext. + * @phpcs:disable Generic.Files.LineLength.TooLong + * * @package Shaarli\Bookmark */ class BookmarkInitializer diff --git a/application/container/ContainerBuilder.php b/application/container/ContainerBuilder.php index d84418ad..f0234eca 100644 --- a/application/container/ContainerBuilder.php +++ b/application/container/ContainerBuilder.php @@ -158,7 +158,7 @@ class ContainerBuilder $container['updater'] = function (ShaarliContainer $container): Updater { return new Updater( - UpdaterUtils::read_updates_file($container->conf->get('resource.updates')), + UpdaterUtils::readUpdatesFile($container->conf->get('resource.updates')), $container->bookmarkService, $container->conf, $container->loginManager->isLoggedIn() diff --git a/application/formatter/BookmarkDefaultFormatter.php b/application/formatter/BookmarkDefaultFormatter.php index 51bea0f1..7e0afafc 100644 --- a/application/formatter/BookmarkDefaultFormatter.php +++ b/application/formatter/BookmarkDefaultFormatter.php @@ -12,8 +12,8 @@ namespace Shaarli\Formatter; */ class BookmarkDefaultFormatter extends BookmarkFormatter { - const SEARCH_HIGHLIGHT_OPEN = '|@@HIGHLIGHT'; - const SEARCH_HIGHLIGHT_CLOSE = 'HIGHLIGHT@@|'; + protected const SEARCH_HIGHLIGHT_OPEN = '|@@HIGHLIGHT'; + protected const SEARCH_HIGHLIGHT_CLOSE = 'HIGHLIGHT@@|'; /** * @inheritdoc diff --git a/application/formatter/BookmarkMarkdownFormatter.php b/application/formatter/BookmarkMarkdownFormatter.php index 052333ca..ee4e8dca 100644 --- a/application/formatter/BookmarkMarkdownFormatter.php +++ b/application/formatter/BookmarkMarkdownFormatter.php @@ -16,7 +16,7 @@ class BookmarkMarkdownFormatter extends BookmarkDefaultFormatter /** * When this tag is present in a bookmark, its description should not be processed with Markdown */ - const NO_MD_TAG = 'nomarkdown'; + public const NO_MD_TAG = 'nomarkdown'; /** @var \Parsedown instance */ protected $parsedown; diff --git a/application/front/controller/admin/ConfigureController.php b/application/front/controller/admin/ConfigureController.php index eb26ef21..dc421661 100644 --- a/application/front/controller/admin/ConfigureController.php +++ b/application/front/controller/admin/ConfigureController.php @@ -51,7 +51,10 @@ class ConfigureController extends ShaarliAdminController $this->assignView('languages', Languages::getAvailableLanguages()); $this->assignView('gd_enabled', extension_loaded('gd')); $this->assignView('thumbnails_mode', $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE)); - $this->assignView('pagetitle', t('Configure') . ' - ' . $this->container->conf->get('general.title', 'Shaarli')); + $this->assignView( + 'pagetitle', + t('Configure') . ' - ' . $this->container->conf->get('general.title', 'Shaarli') + ); return $response->write($this->render(TemplatePage::CONFIGURE)); } @@ -101,7 +104,9 @@ class ConfigureController extends ShaarliAdminController ) { $this->saveWarningMessage( t('You have enabled or changed thumbnails mode.') . - '' . t('Please synchronize them.') . '' + '' . + t('Please synchronize them.') . + '' ); } $this->container->conf->set('thumbnails.mode', $thumbnailsMode); diff --git a/application/front/controller/admin/ServerController.php b/application/front/controller/admin/ServerController.php index 80997940..575a2f9d 100644 --- a/application/front/controller/admin/ServerController.php +++ b/application/front/controller/admin/ServerController.php @@ -65,7 +65,9 @@ class ServerController extends ShaarliAdminController $this->saveWarningMessage( t('Thumbnails cache has been cleared.') . ' ' . - '' . t('Please synchronize them.') . '' + '' . + t('Please synchronize them.') . + '' ); } else { $folders = [ diff --git a/application/legacy/LegacyLinkDB.php b/application/legacy/LegacyLinkDB.php index 442b833c..d3beafe0 100644 --- a/application/legacy/LegacyLinkDB.php +++ b/application/legacy/LegacyLinkDB.php @@ -62,7 +62,7 @@ class LegacyLinkDB implements Iterator, Countable, ArrayAccess private $datastore; // Link date storage format - const LINK_DATE_FORMAT = 'Ymd_His'; + public const LINK_DATE_FORMAT = 'Ymd_His'; // List of bookmarks (associative array) // - key: link date (e.g. "20110823_124546"), diff --git a/application/updater/Updater.php b/application/updater/Updater.php index 3451cf36..4f557d0f 100644 --- a/application/updater/Updater.php +++ b/application/updater/Updater.php @@ -122,12 +122,12 @@ class Updater public function readUpdates(string $updatesFilepath): array { - return UpdaterUtils::read_updates_file($updatesFilepath); + return UpdaterUtils::readUpdatesFile($updatesFilepath); } public function writeUpdates(string $updatesFilepath, array $updates): void { - UpdaterUtils::write_updates_file($updatesFilepath, $updates); + UpdaterUtils::writeUpdatesFile($updatesFilepath, $updates); } /** diff --git a/application/updater/UpdaterUtils.php b/application/updater/UpdaterUtils.php index 908bdc39..206f826e 100644 --- a/application/updater/UpdaterUtils.php +++ b/application/updater/UpdaterUtils.php @@ -11,7 +11,7 @@ class UpdaterUtils * * @return array Already done update methods. */ - public static function read_updates_file($updatesFilepath) + public static function readUpdatesFile($updatesFilepath) { if (! empty($updatesFilepath) && is_file($updatesFilepath)) { $content = file_get_contents($updatesFilepath); @@ -30,7 +30,7 @@ class UpdaterUtils * * @throws \Exception Couldn't write version number. */ - public static function write_updates_file($updatesFilepath, $updates) + public static function writeUpdatesFile($updatesFilepath, $updates) { if (empty($updatesFilepath)) { throw new \Exception('Updates file path is not set, can\'t write updates.'); -- cgit v1.2.3 From 80c8889bfe5151a23066188e6c74c3c1e8575e61 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Mon, 9 Nov 2020 14:37:45 +0100 Subject: Server admin: do not retrieve latest version without update_check If the setting 'updates.check_updates' is disabled, do not retrieve the latest version on server administration page. Additionally, updated default values for - updates.check_updates from false to true - updates.check_updates_branch from stable to latest --- application/config/ConfigManager.php | 4 ++-- application/front/controller/admin/ServerController.php | 15 +++++++++++---- 2 files changed, 13 insertions(+), 6 deletions(-) (limited to 'application') diff --git a/application/config/ConfigManager.php b/application/config/ConfigManager.php index a035baae..3260d7c0 100644 --- a/application/config/ConfigManager.php +++ b/application/config/ConfigManager.php @@ -370,8 +370,8 @@ class ConfigManager $this->setEmpty('general.enable_async_metadata', true); $this->setEmpty('general.tags_separator', ' '); - $this->setEmpty('updates.check_updates', false); - $this->setEmpty('updates.check_updates_branch', 'stable'); + $this->setEmpty('updates.check_updates', true); + $this->setEmpty('updates.check_updates_branch', 'latest'); $this->setEmpty('updates.check_updates_interval', 86400); $this->setEmpty('feed.rss_permalinks', true); diff --git a/application/front/controller/admin/ServerController.php b/application/front/controller/admin/ServerController.php index bfc99422..780151dd 100644 --- a/application/front/controller/admin/ServerController.php +++ b/application/front/controller/admin/ServerController.php @@ -25,9 +25,16 @@ class ServerController extends ShaarliAdminController */ public function index(Request $request, Response $response): Response { - $latestVersion = 'v' . ApplicationUtils::getVersion( - ApplicationUtils::$GIT_RAW_URL . '/latest/' . ApplicationUtils::$VERSION_FILE - ); + $releaseUrl = ApplicationUtils::$GITHUB_URL . '/releases/'; + if ($this->container->conf->get('updates.check_updates', true)) { + $latestVersion = 'v' . ApplicationUtils::getVersion( + ApplicationUtils::$GIT_RAW_URL . '/latest/' . ApplicationUtils::$VERSION_FILE + ); + $releaseUrl .= 'tag/' . $latestVersion; + } else { + $latestVersion = t('Check disabled'); + } + $currentVersion = ApplicationUtils::getVersion('./shaarli_version.php'); $currentVersion = $currentVersion === 'dev' ? $currentVersion : 'v' . $currentVersion; $phpEol = new \DateTimeImmutable(ApplicationUtils::getPhpEol(PHP_VERSION)); @@ -37,7 +44,7 @@ class ServerController extends ShaarliAdminController $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('release_url', $releaseUrl); $this->assignView('latest_version', $latestVersion); $this->assignView('current_version', $currentVersion); $this->assignView('thumbnails_mode', $this->container->conf->get('thumbnails.mode')); -- cgit v1.2.3