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