]> git.immae.eu Git - github/shaarli/Shaarli.git/blame - application/bookmark/Bookmark.php
Merge pull request #1567 from ArthurHoaro/feature/async-title-retrieval
[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) */
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
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 *
63 * @param array $data
64 *
65 * @return $this
66 */
efb7d21b 67 public function fromArray(array $data): Bookmark
336a28fa 68 {
efb7d21b
A
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;
336a28fa
A
77 if (is_array($data['tags'])) {
78 $this->tags = $data['tags'];
79 } else {
efb7d21b 80 $this->tags = preg_split('/\s+/', $data['tags'] ?? '', -1, PREG_SPLIT_NO_EMPTY);
336a28fa
A
81 }
82 if (! empty($data['updated'])) {
83 $this->updated = $data['updated'];
84 }
efb7d21b 85 $this->private = ($data['private'] ?? false) ? true : false;
336a28fa
A
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 *
4e3875c0
A
101 * Also make sure that we do not save search highlights in the datastore.
102 *
336a28fa
A
103 * @throws InvalidBookmarkException
104 */
efb7d21b 105 public function validate(): void
336a28fa
A
106 {
107 if ($this->id === null
108 || ! is_int($this->id)
109 || empty($this->shortUrl)
110 || empty($this->created)
336a28fa
A
111 ) {
112 throw new InvalidBookmarkException($this);
113 }
114 if (empty($this->url)) {
301c7ab1 115 $this->url = '/shaare/'. $this->shortUrl;
336a28fa
A
116 }
117 if (empty($this->title)) {
118 $this->title = $this->url;
119 }
4e3875c0
A
120 if (array_key_exists('search_highlight', $this->additionalContent)) {
121 unset($this->additionalContent['search_highlight']);
122 }
336a28fa
A
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 *
efb7d21b 131 * @param int|null $id
336a28fa
A
132 *
133 * @return Bookmark
134 */
efb7d21b 135 public function setId(?int $id): Bookmark
336a28fa
A
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 *
efb7d21b 151 * @return int|null
336a28fa 152 */
efb7d21b 153 public function getId(): ?int
336a28fa
A
154 {
155 return $this->id;
156 }
157
158 /**
159 * Get the ShortUrl.
160 *
efb7d21b 161 * @return string|null
336a28fa 162 */
efb7d21b 163 public function getShortUrl(): ?string
336a28fa
A
164 {
165 return $this->shortUrl;
166 }
167
168 /**
169 * Get the Url.
170 *
efb7d21b 171 * @return string|null
336a28fa 172 */
efb7d21b 173 public function getUrl(): ?string
336a28fa
A
174 {
175 return $this->url;
176 }
177
178 /**
179 * Get the Title.
180 *
181 * @return string
182 */
efb7d21b 183 public function getTitle(): ?string
336a28fa
A
184 {
185 return $this->title;
186 }
187
188 /**
189 * Get the Description.
190 *
191 * @return string
192 */
efb7d21b 193 public function getDescription(): string
336a28fa
A
194 {
195 return ! empty($this->description) ? $this->description : '';
196 }
197
198 /**
199 * Get the Created.
200 *
c4d5be53 201 * @return DateTimeInterface
336a28fa 202 */
efb7d21b 203 public function getCreated(): ?DateTimeInterface
336a28fa
A
204 {
205 return $this->created;
206 }
207
208 /**
209 * Get the Updated.
210 *
c4d5be53 211 * @return DateTimeInterface
336a28fa 212 */
efb7d21b 213 public function getUpdated(): ?DateTimeInterface
336a28fa
A
214 {
215 return $this->updated;
216 }
217
218 /**
219 * Set the ShortUrl.
220 *
efb7d21b 221 * @param string|null $shortUrl
336a28fa
A
222 *
223 * @return Bookmark
224 */
efb7d21b 225 public function setShortUrl(?string $shortUrl): Bookmark
336a28fa
A
226 {
227 $this->shortUrl = $shortUrl;
228
229 return $this;
230 }
231
232 /**
233 * Set the Url.
234 *
efb7d21b
A
235 * @param string|null $url
236 * @param string[] $allowedProtocols
336a28fa
A
237 *
238 * @return Bookmark
239 */
efb7d21b 240 public function setUrl(?string $url, array $allowedProtocols = []): Bookmark
336a28fa 241 {
efb7d21b 242 $url = $url !== null ? trim($url) : '';
336a28fa
A
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 *
efb7d21b 254 * @param string|null $title
336a28fa
A
255 *
256 * @return Bookmark
257 */
efb7d21b 258 public function setTitle(?string $title): Bookmark
336a28fa 259 {
efb7d21b 260 $this->title = $title !== null ? trim($title) : '';
336a28fa
A
261
262 return $this;
263 }
264
265 /**
266 * Set the Description.
267 *
efb7d21b 268 * @param string|null $description
336a28fa
A
269 *
270 * @return Bookmark
271 */
efb7d21b 272 public function setDescription(?string $description): Bookmark
336a28fa
A
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 *
efb7d21b 283 * @param DateTimeInterface|null $created
336a28fa
A
284 *
285 * @return Bookmark
286 */
efb7d21b 287 public function setCreated(?DateTimeInterface $created): Bookmark
336a28fa
A
288 {
289 $this->created = $created;
290
291 return $this;
292 }
293
294 /**
295 * Set the Updated.
296 *
efb7d21b 297 * @param DateTimeInterface|null $updated
336a28fa
A
298 *
299 * @return Bookmark
300 */
efb7d21b 301 public function setUpdated(?DateTimeInterface $updated): Bookmark
336a28fa
A
302 {
303 $this->updated = $updated;
304
305 return $this;
306 }
307
308 /**
309 * Get the Private.
310 *
311 * @return bool
312 */
efb7d21b 313 public function isPrivate(): bool
336a28fa
A
314 {
315 return $this->private ? true : false;
316 }
317
318 /**
319 * Set the Private.
320 *
efb7d21b 321 * @param bool|null $private
336a28fa
A
322 *
323 * @return Bookmark
324 */
efb7d21b 325 public function setPrivate(?bool $private): Bookmark
336a28fa
A
326 {
327 $this->private = $private ? true : false;
328
329 return $this;
330 }
331
332 /**
333 * Get the Tags.
334 *
efb7d21b 335 * @return string[]
336a28fa 336 */
efb7d21b 337 public function getTags(): array
336a28fa
A
338 {
339 return is_array($this->tags) ? $this->tags : [];
340 }
341
342 /**
343 * Set the Tags.
344 *
efb7d21b 345 * @param string[]|null $tags
336a28fa
A
346 *
347 * @return Bookmark
348 */
efb7d21b 349 public function setTags(?array $tags): Bookmark
336a28fa 350 {
efb7d21b 351 $this->setTagsString(implode(' ', $tags ?? []));
336a28fa
A
352
353 return $this;
354 }
355
356 /**
357 * Get the Thumbnail.
358 *
1a68ae5a 359 * @return string|bool|null Thumbnail's URL - initialized at null, false if no thumbnail could be found
336a28fa
A
360 */
361 public function getThumbnail()
362 {
363 return !$this->isNote() ? $this->thumbnail : false;
364 }
365
366 /**
367 * Set the Thumbnail.
368 *
efb7d21b 369 * @param string|bool|null $thumbnail Thumbnail's URL - false if no thumbnail could be found
336a28fa
A
370 *
371 * @return Bookmark
372 */
efb7d21b 373 public function setThumbnail($thumbnail): Bookmark
336a28fa
A
374 {
375 $this->thumbnail = $thumbnail;
376
377 return $this;
378 }
379
380 /**
381 * Get the Sticky.
382 *
383 * @return bool
384 */
efb7d21b 385 public function isSticky(): bool
336a28fa
A
386 {
387 return $this->sticky ? true : false;
388 }
389
390 /**
391 * Set the Sticky.
392 *
efb7d21b 393 * @param bool|null $sticky
336a28fa
A
394 *
395 * @return Bookmark
396 */
efb7d21b 397 public function setSticky(?bool $sticky): Bookmark
336a28fa
A
398 {
399 $this->sticky = $sticky ? true : false;
400
401 return $this;
402 }
403
404 /**
405 * @return string Bookmark's tags as a string, separated by a space
406 */
efb7d21b 407 public function getTagsString(): string
336a28fa
A
408 {
409 return implode(' ', $this->getTags());
410 }
411
412 /**
413 * @return bool
414 */
efb7d21b 415 public function isNote(): bool
336a28fa
A
416 {
417 // We check empty value to get a valid result if the link has not been saved yet
301c7ab1 418 return empty($this->url) || startsWith($this->url, '/shaare/') || $this->url[0] === '?';
336a28fa
A
419 }
420
421 /**
422 * Set tags from a string.
423 * Note:
424 * - tags must be separated whether by a space or a comma
425 * - multiple spaces will be removed
426 * - trailing dash in tags will be removed
427 *
efb7d21b 428 * @param string|null $tags
336a28fa
A
429 *
430 * @return $this
431 */
efb7d21b 432 public function setTagsString(?string $tags): Bookmark
336a28fa
A
433 {
434 // Remove first '-' char in tags.
efb7d21b 435 $tags = preg_replace('/(^| )\-/', '$1', $tags ?? '');
336a28fa
A
436 // Explode all tags separted by spaces or commas
437 $tags = preg_split('/[\s,]+/', $tags);
438 // Remove eventual empty values
439 $tags = array_values(array_filter($tags));
440
441 $this->tags = $tags;
442
443 return $this;
444 }
445
4e3875c0
A
446 /**
447 * Get entire additionalContent array.
448 *
449 * @return mixed[]
450 */
451 public function getAdditionalContent(): array
452 {
453 return $this->additionalContent;
454 }
455
456 /**
457 * Set a single entry in additionalContent, by key.
458 *
459 * @param string $key
460 * @param mixed|null $value Any type of value can be set.
461 *
462 * @return $this
463 */
464 public function addAdditionalContentEntry(string $key, $value): self
465 {
466 $this->additionalContent[$key] = $value;
467
468 return $this;
469 }
470
471 /**
472 * Get a single entry in additionalContent, by key.
473 *
474 * @param string $key
475 * @param mixed|null $default
476 *
477 * @return mixed|null can be any type or even null.
478 */
479 public function getAdditionalContentEntry(string $key, $default = null)
480 {
481 return array_key_exists($key, $this->additionalContent) ? $this->additionalContent[$key] : $default;
482 }
483
336a28fa
A
484 /**
485 * Rename a tag in tags list.
486 *
487 * @param string $fromTag
488 * @param string $toTag
489 */
efb7d21b 490 public function renameTag(string $fromTag, string $toTag): void
336a28fa
A
491 {
492 if (($pos = array_search($fromTag, $this->tags)) !== false) {
493 $this->tags[$pos] = trim($toTag);
494 }
495 }
496
497 /**
498 * Delete a tag from tags list.
499 *
500 * @param string $tag
501 */
efb7d21b 502 public function deleteTag(string $tag): void
336a28fa
A
503 {
504 if (($pos = array_search($tag, $this->tags)) !== false) {
505 unset($this->tags[$pos]);
506 $this->tags = array_values($this->tags);
507 }
508 }
509}