<?php
+declare(strict_types=1);
+
namespace Shaarli\Bookmark;
use DateTime;
+use DateTimeInterface;
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;
/** @var array List of bookmark's tags */
protected $tags;
- /** @var string Thumbnail's URL - false if no thumbnail could be found */
+ /** @var string|bool|null Thumbnail's URL - initialized at null, false if no thumbnail could be found */
protected $thumbnail;
/** @var bool Set to true if the bookmark is set as sticky */
protected $sticky;
- /** @var DateTime Creation datetime */
+ /** @var DateTimeInterface Creation datetime */
protected $created;
- /** @var DateTime Update datetime */
+ /** @var DateTimeInterface datetime */
protected $updated;
/** @var bool True if the bookmark can only be seen while logged in */
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.
*
- * @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($data)
+ public function fromArray(array $data, string $tagsSeparator = ' '): Bookmark
{
- $this->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 = tags_str2array($data['tags'] ?? '', $tagsSeparator);
}
if (! empty($data['updated'])) {
$this->updated = $data['updated'];
}
- $this->private = $data['private'] ? true : false;
+ $this->private = ($data['private'] ?? false) ? true : false;
return $this;
}
* - 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()
+ public function validate(): void
{
- if ($this->id === null
+ if (
+ $this->id === null
|| ! is_int($this->id)
|| empty($this->shortUrl)
|| empty($this->created)
- || ! $this->created instanceof DateTime
) {
throw new InvalidBookmarkException($this);
}
if (empty($this->url)) {
- $this->url = '?'. $this->shortUrl;
+ $this->url = '/shaare/' . $this->shortUrl;
}
if (empty($this->title)) {
$this->title = $this->url;
}
+ if (array_key_exists('search_highlight', $this->additionalContent)) {
+ unset($this->additionalContent['search_highlight']);
+ }
}
/**
* - 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)) {
/**
* Get the Id.
*
- * @return int
+ * @return int|null
*/
- public function getId()
+ public function getId(): ?int
{
return $this->id;
}
/**
* Get the ShortUrl.
*
- * @return string
+ * @return string|null
*/
- public function getShortUrl()
+ public function getShortUrl(): ?string
{
return $this->shortUrl;
}
/**
* Get the Url.
*
- * @return string
+ * @return string|null
*/
- public function getUrl()
+ public function getUrl(): ?string
{
return $this->url;
}
*
* @return string
*/
- public function getTitle()
+ public function getTitle(): ?string
{
return $this->title;
}
*
* @return string
*/
- public function getDescription()
+ public function getDescription(): string
{
return ! empty($this->description) ? $this->description : '';
}
/**
* Get the Created.
*
- * @return DateTime
+ * @return DateTimeInterface
*/
- public function getCreated()
+ public function getCreated(): ?DateTimeInterface
{
return $this->created;
}
/**
* Get the Updated.
*
- * @return DateTime
+ * @return DateTimeInterface
*/
- public function getUpdated()
+ public function getUpdated(): ?DateTimeInterface
{
return $this->updated;
}
/**
* 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;
/**
* 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);
}
/**
* 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;
}
/**
* 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;
* Set the Created.
* Note: you shouldn't set this manually except for special cases (like bookmark import)
*
- * @param DateTime $created
+ * @param DateTimeInterface|null $created
*
* @return Bookmark
*/
- public function setCreated($created)
+ public function setCreated(?DateTimeInterface $created): Bookmark
{
$this->created = $created;
/**
* Set the Updated.
*
- * @param DateTime $updated
+ * @param DateTimeInterface|null $updated
*
* @return Bookmark
*/
- public function setUpdated($updated)
+ public function setUpdated(?DateTimeInterface $updated): Bookmark
{
$this->updated = $updated;
*
* @return bool
*/
- public function isPrivate()
+ public function isPrivate(): bool
{
return $this->private ? true : false;
}
/**
* 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;
/**
* Get the Tags.
*
- * @return array
+ * @return string[]
*/
- public function getTags()
+ public function getTags(): array
{
return is_array($this->tags) ? $this->tags : [];
}
/**
* 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->tags = array_map(
+ function (string $tag): string {
+ return $tag[0] === '-' ? substr($tag, 1) : $tag;
+ },
+ tags_filter($tags, ' ')
+ );
return $this;
}
/**
* Get the Thumbnail.
*
- * @return string|bool
+ * @return string|bool|null Thumbnail's URL - initialized at null, false if no thumbnail could be found
*/
public function getThumbnail()
{
/**
* Set the Thumbnail.
*
- * @param string|bool $thumbnail
+ * @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;
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.
*
* @return bool
*/
- public function isSticky()
+ public function isSticky(): bool
{
return $this->sticky ? true : false;
}
/**
* 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;
}
/**
- * @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()
+ public function getTagsString(string $separator = ' '): string
{
- return implode(' ', $this->getTags());
+ return tags_array2str($this->getTags(), $separator);
}
/**
* @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) || $this->url[0] === '?';
+ return empty($this->url) || startsWith($this->url, '/shaare/') || $this->url[0] === '?';
}
/**
* - multiple spaces will be removed
* - trailing dash in tags will be removed
*
- * @param string $tags
+ * @param string|null $tags
+ * @param string $separator Tags separator loaded from the config file.
*
* @return $this
*/
- public function setTagsString($tags)
+ 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->setTags(tags_str2array($tags, $separator));
- $this->tags = $tags;
+ 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.
*
* @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) {
+ if (($pos = array_search($fromTag, $this->tags ?? [])) !== false) {
$this->tags[$pos] = trim($toTag);
}
}
*
* @param string $tag
*/
- public function deleteTag($tag)
+ 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);
}