]> git.immae.eu Git - github/shaarli/Shaarli.git/blob - application/bookmark/Bookmark.php
Feature: highlight fulltext search results
[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 * Get the Sticky.
382 *
383 * @return bool
384 */
385 public function isSticky(): bool
386 {
387 return $this->sticky ? true : false;
388 }
389
390 /**
391 * Set the Sticky.
392 *
393 * @param bool|null $sticky
394 *
395 * @return Bookmark
396 */
397 public function setSticky(?bool $sticky): Bookmark
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 */
407 public function getTagsString(): string
408 {
409 return implode(' ', $this->getTags());
410 }
411
412 /**
413 * @return bool
414 */
415 public function isNote(): bool
416 {
417 // We check empty value to get a valid result if the link has not been saved yet
418 return empty($this->url) || startsWith($this->url, '/shaare/') || $this->url[0] === '?';
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 *
428 * @param string|null $tags
429 *
430 * @return $this
431 */
432 public function setTagsString(?string $tags): Bookmark
433 {
434 // Remove first '-' char in tags.
435 $tags = preg_replace('/(^| )\-/', '$1', $tags ?? '');
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
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
484 /**
485 * Rename a tag in tags list.
486 *
487 * @param string $fromTag
488 * @param string $toTag
489 */
490 public function renameTag(string $fromTag, string $toTag): void
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 */
502 public function deleteTag(string $tag): void
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 }