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/bookmark') 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 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/bookmark/BookmarkFileService.php | 9 ++++-- application/bookmark/BookmarkIO.php | 35 +++++++++++++++++------ application/bookmark/BookmarkServiceInterface.php | 11 ------- 3 files changed, 34 insertions(+), 21 deletions(-) (limited to 'application/bookmark') 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 * -- 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/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 +++++++------ 7 files changed, 170 insertions(+), 162 deletions(-) (limited to 'application/bookmark') 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; } -- 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/bookmark') 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 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 ++++++++++++++++++++++++++------ 2 files changed, 139 insertions(+), 18 deletions(-) (limited to 'application/bookmark') 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; + } } -- 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 ++++++++++++++++++ 1 file changed, 18 insertions(+) (limited to 'application/bookmark') 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. * -- 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 +++-- 2 files changed, 8 insertions(+), 4 deletions(-) (limited to 'application/bookmark') 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 -- 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/bookmark/BookmarkFileService.php | 38 ++++++++++++++++------- application/bookmark/BookmarkServiceInterface.php | 27 ++++++++++------ 2 files changed, 44 insertions(+), 21 deletions(-) (limited to 'application/bookmark') 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. -- 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/bookmark') 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 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 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) (limited to 'application/bookmark') 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)); } /** -- 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 +++++++++++++++++++++++++++ 4 files changed, 99 insertions(+), 35 deletions(-) (limited to 'application/bookmark') 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 ?? []))); +} -- 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/bookmark') 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/bookmark') 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/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 ++++++----- .../bookmark/exception/BookmarkNotFoundException.php | 1 + .../bookmark/exception/EmptyDataStoreException.php | 6 +++--- .../bookmark/exception/InvalidBookmarkException.php | 14 +++++++------- .../exception/NotWritableDataStoreException.php | 4 +--- 11 files changed, 47 insertions(+), 40 deletions(-) (limited to 'application/bookmark') 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.'; } } -- 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/bookmark/Bookmark.php | 2 +- application/bookmark/BookmarkFileService.php | 4 ++-- application/bookmark/BookmarkInitializer.php | 3 +++ 3 files changed, 6 insertions(+), 3 deletions(-) (limited to 'application/bookmark') 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 -- cgit v1.2.3