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