]> git.immae.eu Git - github/shaarli/Shaarli.git/blame - application/bookmark/Bookmark.php
Merge pull request #1666 from ArthurHoaro/feature/daily-rss-cache
[github/shaarli/Shaarli.git] / application / bookmark / Bookmark.php
CommitLineData
336a28fa
A
1<?php
2
efb7d21b
A
3declare(strict_types=1);
4
336a28fa
A
5namespace Shaarli\Bookmark;
6
7use DateTime;
c4d5be53 8use DateTimeInterface;
336a28fa
A
9use Shaarli\Bookmark\Exception\InvalidBookmarkException;
10
11/**
12 * Class Bookmark
13 *
14 * This class represent a single Bookmark with all its attributes.
15 * Every bookmark should manipulated using this, before being formatted.
16 *
17 * @package Shaarli\Bookmark
18 */
19class Bookmark
20{
21 /** @var string Date format used in string (former ID format) */
b99e00f7 22 public const LINK_DATE_FORMAT = 'Ymd_His';
336a28fa
A
23
24 /** @var int Bookmark ID */
25 protected $id;
26
27 /** @var string Permalink identifier */
28 protected $shortUrl;
29
30 /** @var string Bookmark's URL - $shortUrl prefixed with `?` for notes */
31 protected $url;
32
33 /** @var string Bookmark's title */
34 protected $title;
35
36 /** @var string Raw bookmark's description */
37 protected $description;
38
39 /** @var array List of bookmark's tags */
40 protected $tags;
41
1a68ae5a 42 /** @var string|bool|null Thumbnail's URL - initialized at null, false if no thumbnail could be found */
336a28fa
A
43 protected $thumbnail;
44
45 /** @var bool Set to true if the bookmark is set as sticky */
46 protected $sticky;
47
c4d5be53 48 /** @var DateTimeInterface Creation datetime */
336a28fa
A
49 protected $created;
50
c4d5be53 51 /** @var DateTimeInterface datetime */
336a28fa
A
52 protected $updated;
53
54 /** @var bool True if the bookmark can only be seen while logged in */
55 protected $private;
56
4e3875c0
A
57 /** @var mixed[] Available to store any additional content for a bookmark. Currently used for search highlight. */
58 protected $additionalContent = [];
59
336a28fa
A
60 /**
61 * Initialize a link from array data. Especially useful to create a Bookmark from former link storage format.
62 *
b3bd8c3e
A
63 * @param array $data
64 * @param string $tagsSeparator Tags separator loaded from the config file.
65 * This is a context data, and it should *never* be stored in the Bookmark object.
336a28fa
A
66 *
67 * @return $this
68 */
b3bd8c3e 69 public function fromArray(array $data, string $tagsSeparator = ' '): Bookmark
336a28fa 70 {
efb7d21b
A
71 $this->id = $data['id'] ?? null;
72 $this->shortUrl = $data['shorturl'] ?? null;
73 $this->url = $data['url'] ?? null;
74 $this->title = $data['title'] ?? null;
75 $this->description = $data['description'] ?? null;
76 $this->thumbnail = $data['thumbnail'] ?? null;
77 $this->sticky = $data['sticky'] ?? false;
78 $this->created = $data['created'] ?? null;
336a28fa
A
79 if (is_array($data['tags'])) {
80 $this->tags = $data['tags'];
81 } else {
b3bd8c3e 82 $this->tags = tags_str2array($data['tags'] ?? '', $tagsSeparator);
336a28fa
A
83 }
84 if (! empty($data['updated'])) {
85 $this->updated = $data['updated'];
86 }
efb7d21b 87 $this->private = ($data['private'] ?? false) ? true : false;
336a28fa
A
88
89 return $this;
90 }
91
92 /**
93 * Make sure that the current instance of Bookmark is valid and can be saved into the data store.
94 * A valid link requires:
95 * - an integer ID
96 * - a short URL (for permalinks)
97 * - a creation date
98 *
99 * This function also initialize optional empty fields:
100 * - the URL with the permalink
101 * - the title with the URL
102 *
4e3875c0
A
103 * Also make sure that we do not save search highlights in the datastore.
104 *
336a28fa
A
105 * @throws InvalidBookmarkException
106 */
efb7d21b 107 public function validate(): void
336a28fa 108 {
53054b2b
A
109 if (
110 $this->id === null
336a28fa
A
111 || ! is_int($this->id)
112 || empty($this->shortUrl)
113 || empty($this->created)
336a28fa
A
114 ) {
115 throw new InvalidBookmarkException($this);
116 }
117 if (empty($this->url)) {
53054b2b 118 $this->url = '/shaare/' . $this->shortUrl;
336a28fa
A
119 }
120 if (empty($this->title)) {
121 $this->title = $this->url;
122 }
4e3875c0
A
123 if (array_key_exists('search_highlight', $this->additionalContent)) {
124 unset($this->additionalContent['search_highlight']);
125 }
336a28fa
A
126 }
127
128 /**
129 * Set the Id.
130 * If they're not already initialized, this function also set:
131 * - created: with the current datetime
132 * - shortUrl: with a generated small hash from the date and the given ID
133 *
efb7d21b 134 * @param int|null $id
336a28fa
A
135 *
136 * @return Bookmark
137 */
efb7d21b 138 public function setId(?int $id): Bookmark
336a28fa
A
139 {
140 $this->id = $id;
141 if (empty($this->created)) {
142 $this->created = new DateTime();
143 }
144 if (empty($this->shortUrl)) {
145 $this->shortUrl = link_small_hash($this->created, $this->id);
146 }
147
148 return $this;
149 }
150
151 /**
152 * Get the Id.
153 *
efb7d21b 154 * @return int|null
336a28fa 155 */
efb7d21b 156 public function getId(): ?int
336a28fa
A
157 {
158 return $this->id;
159 }
160
161 /**
162 * Get the ShortUrl.
163 *
efb7d21b 164 * @return string|null
336a28fa 165 */
efb7d21b 166 public function getShortUrl(): ?string
336a28fa
A
167 {
168 return $this->shortUrl;
169 }
170
171 /**
172 * Get the Url.
173 *
efb7d21b 174 * @return string|null
336a28fa 175 */
efb7d21b 176 public function getUrl(): ?string
336a28fa
A
177 {
178 return $this->url;
179 }
180
181 /**
182 * Get the Title.
183 *
184 * @return string
185 */
efb7d21b 186 public function getTitle(): ?string
336a28fa
A
187 {
188 return $this->title;
189 }
190
191 /**
192 * Get the Description.
193 *
194 * @return string
195 */
efb7d21b 196 public function getDescription(): string
336a28fa
A
197 {
198 return ! empty($this->description) ? $this->description : '';
199 }
200
201 /**
202 * Get the Created.
203 *
c4d5be53 204 * @return DateTimeInterface
336a28fa 205 */
efb7d21b 206 public function getCreated(): ?DateTimeInterface
336a28fa
A
207 {
208 return $this->created;
209 }
210
211 /**
212 * Get the Updated.
213 *
c4d5be53 214 * @return DateTimeInterface
336a28fa 215 */
efb7d21b 216 public function getUpdated(): ?DateTimeInterface
336a28fa
A
217 {
218 return $this->updated;
219 }
220
221 /**
222 * Set the ShortUrl.
223 *
efb7d21b 224 * @param string|null $shortUrl
336a28fa
A
225 *
226 * @return Bookmark
227 */
efb7d21b 228 public function setShortUrl(?string $shortUrl): Bookmark
336a28fa
A
229 {
230 $this->shortUrl = $shortUrl;
231
232 return $this;
233 }
234
235 /**
236 * Set the Url.
237 *
efb7d21b
A
238 * @param string|null $url
239 * @param string[] $allowedProtocols
336a28fa
A
240 *
241 * @return Bookmark
242 */
efb7d21b 243 public function setUrl(?string $url, array $allowedProtocols = []): Bookmark
336a28fa 244 {
efb7d21b 245 $url = $url !== null ? trim($url) : '';
336a28fa
A
246 if (! empty($url)) {
247 $url = whitelist_protocols($url, $allowedProtocols);
248 }
249 $this->url = $url;
250
251 return $this;
252 }
253
254 /**
255 * Set the Title.
256 *
efb7d21b 257 * @param string|null $title
336a28fa
A
258 *
259 * @return Bookmark
260 */
efb7d21b 261 public function setTitle(?string $title): Bookmark
336a28fa 262 {
efb7d21b 263 $this->title = $title !== null ? trim($title) : '';
336a28fa
A
264
265 return $this;
266 }
267
268 /**
269 * Set the Description.
270 *
efb7d21b 271 * @param string|null $description
336a28fa
A
272 *
273 * @return Bookmark
274 */
efb7d21b 275 public function setDescription(?string $description): Bookmark
336a28fa
A
276 {
277 $this->description = $description;
278
279 return $this;
280 }
281
282 /**
283 * Set the Created.
284 * Note: you shouldn't set this manually except for special cases (like bookmark import)
285 *
efb7d21b 286 * @param DateTimeInterface|null $created
336a28fa
A
287 *
288 * @return Bookmark
289 */
efb7d21b 290 public function setCreated(?DateTimeInterface $created): Bookmark
336a28fa
A
291 {
292 $this->created = $created;
293
294 return $this;
295 }
296
297 /**
298 * Set the Updated.
299 *
efb7d21b 300 * @param DateTimeInterface|null $updated
336a28fa
A
301 *
302 * @return Bookmark
303 */
efb7d21b 304 public function setUpdated(?DateTimeInterface $updated): Bookmark
336a28fa
A
305 {
306 $this->updated = $updated;
307
308 return $this;
309 }
310
311 /**
312 * Get the Private.
313 *
314 * @return bool
315 */
efb7d21b 316 public function isPrivate(): bool
336a28fa
A
317 {
318 return $this->private ? true : false;
319 }
320
321 /**
322 * Set the Private.
323 *
efb7d21b 324 * @param bool|null $private
336a28fa
A
325 *
326 * @return Bookmark
327 */
efb7d21b 328 public function setPrivate(?bool $private): Bookmark
336a28fa
A
329 {
330 $this->private = $private ? true : false;
331
332 return $this;
333 }
334
335 /**
336 * Get the Tags.
337 *
efb7d21b 338 * @return string[]
336a28fa 339 */
efb7d21b 340 public function getTags(): array
336a28fa
A
341 {
342 return is_array($this->tags) ? $this->tags : [];
343 }
344
345 /**
346 * Set the Tags.
347 *
efb7d21b 348 * @param string[]|null $tags
336a28fa
A
349 *
350 * @return Bookmark
351 */
efb7d21b 352 public function setTags(?array $tags): Bookmark
336a28fa 353 {
b3bd8c3e
A
354 $this->tags = array_map(
355 function (string $tag): string {
356 return $tag[0] === '-' ? substr($tag, 1) : $tag;
357 },
358 tags_filter($tags, ' ')
359 );
336a28fa
A
360
361 return $this;
362 }
363
364 /**
365 * Get the Thumbnail.
366 *
1a68ae5a 367 * @return string|bool|null Thumbnail's URL - initialized at null, false if no thumbnail could be found
336a28fa
A
368 */
369 public function getThumbnail()
370 {
371 return !$this->isNote() ? $this->thumbnail : false;
372 }
373
374 /**
375 * Set the Thumbnail.
376 *
efb7d21b 377 * @param string|bool|null $thumbnail Thumbnail's URL - false if no thumbnail could be found
336a28fa
A
378 *
379 * @return Bookmark
380 */
efb7d21b 381 public function setThumbnail($thumbnail): Bookmark
336a28fa
A
382 {
383 $this->thumbnail = $thumbnail;
384
385 return $this;
386 }
387
21e72da9
A
388 /**
389 * Return true if:
390 * - the bookmark's thumbnail is not already set to false (= not found)
391 * - it's not a note
392 * - it's an HTTP(S) link
393 * - the thumbnail has not yet be retrieved (null) or its associated cache file doesn't exist anymore
394 *
395 * @return bool True if the bookmark's thumbnail needs to be retrieved.
396 */
397 public function shouldUpdateThumbnail(): bool
398 {
399 return $this->thumbnail !== false
400 && !$this->isNote()
401 && startsWith(strtolower($this->url), 'http')
402 && (null === $this->thumbnail || !is_file($this->thumbnail))
403 ;
404 }
405
336a28fa
A
406 /**
407 * Get the Sticky.
408 *
409 * @return bool
410 */
efb7d21b 411 public function isSticky(): bool
336a28fa
A
412 {
413 return $this->sticky ? true : false;
414 }
415
416 /**
417 * Set the Sticky.
418 *
efb7d21b 419 * @param bool|null $sticky
336a28fa
A
420 *
421 * @return Bookmark
422 */
efb7d21b 423 public function setSticky(?bool $sticky): Bookmark
336a28fa
A
424 {
425 $this->sticky = $sticky ? true : false;
426
427 return $this;
428 }
429
430 /**
b3bd8c3e
A
431 * @param string $separator Tags separator loaded from the config file.
432 *
433 * @return string Bookmark's tags as a string, separated by a separator
336a28fa 434 */
b3bd8c3e 435 public function getTagsString(string $separator = ' '): string
336a28fa 436 {
b3bd8c3e 437 return tags_array2str($this->getTags(), $separator);
336a28fa
A
438 }
439
440 /**
441 * @return bool
442 */
efb7d21b 443 public function isNote(): bool
336a28fa
A
444 {
445 // We check empty value to get a valid result if the link has not been saved yet
301c7ab1 446 return empty($this->url) || startsWith($this->url, '/shaare/') || $this->url[0] === '?';
336a28fa
A
447 }
448
449 /**
450 * Set tags from a string.
451 * Note:
452 * - tags must be separated whether by a space or a comma
453 * - multiple spaces will be removed
454 * - trailing dash in tags will be removed
455 *
efb7d21b 456 * @param string|null $tags
b3bd8c3e 457 * @param string $separator Tags separator loaded from the config file.
336a28fa
A
458 *
459 * @return $this
460 */
b3bd8c3e 461 public function setTagsString(?string $tags, string $separator = ' '): Bookmark
336a28fa 462 {
b3bd8c3e 463 $this->setTags(tags_str2array($tags, $separator));
336a28fa
A
464
465 return $this;
466 }
467
4e3875c0
A
468 /**
469 * Get entire additionalContent array.
470 *
471 * @return mixed[]
472 */
473 public function getAdditionalContent(): array
474 {
475 return $this->additionalContent;
476 }
477
478 /**
479 * Set a single entry in additionalContent, by key.
480 *
481 * @param string $key
482 * @param mixed|null $value Any type of value can be set.
483 *
484 * @return $this
485 */
486 public function addAdditionalContentEntry(string $key, $value): self
487 {
488 $this->additionalContent[$key] = $value;
489
490 return $this;
491 }
492
493 /**
494 * Get a single entry in additionalContent, by key.
495 *
496 * @param string $key
497 * @param mixed|null $default
498 *
499 * @return mixed|null can be any type or even null.
500 */
501 public function getAdditionalContentEntry(string $key, $default = null)
502 {
503 return array_key_exists($key, $this->additionalContent) ? $this->additionalContent[$key] : $default;
504 }
505
336a28fa
A
506 /**
507 * Rename a tag in tags list.
508 *
509 * @param string $fromTag
510 * @param string $toTag
511 */
efb7d21b 512 public function renameTag(string $fromTag, string $toTag): void
336a28fa 513 {
b3bd8c3e 514 if (($pos = array_search($fromTag, $this->tags ?? [])) !== false) {
336a28fa
A
515 $this->tags[$pos] = trim($toTag);
516 }
517 }
518
519 /**
520 * Delete a tag from tags list.
521 *
522 * @param string $tag
523 */
efb7d21b 524 public function deleteTag(string $tag): void
336a28fa 525 {
b3bd8c3e 526 if (($pos = array_search($tag, $this->tags ?? [])) !== false) {
336a28fa
A
527 unset($this->tags[$pos]);
528 $this->tags = array_values($this->tags);
529 }
530 }
531}