diff options
Diffstat (limited to 'application/bookmark')
-rw-r--r-- | application/bookmark/Bookmark.php | 202 | ||||
-rw-r--r-- | application/bookmark/BookmarkArray.php | 23 | ||||
-rw-r--r-- | application/bookmark/BookmarkFileService.php | 157 | ||||
-rw-r--r-- | application/bookmark/BookmarkFilter.php | 175 | ||||
-rw-r--r-- | application/bookmark/BookmarkIO.php | 49 | ||||
-rw-r--r-- | application/bookmark/BookmarkInitializer.php | 87 | ||||
-rw-r--r-- | application/bookmark/BookmarkServiceInterface.php | 109 | ||||
-rw-r--r-- | application/bookmark/LinkUtils.php | 116 | ||||
-rw-r--r-- | application/bookmark/exception/DatastoreNotInitializedException.php | 10 |
9 files changed, 557 insertions, 371 deletions
diff --git a/application/bookmark/Bookmark.php b/application/bookmark/Bookmark.php index f9b21d3d..4810c5e6 100644 --- a/application/bookmark/Bookmark.php +++ b/application/bookmark/Bookmark.php | |||
@@ -1,8 +1,11 @@ | |||
1 | <?php | 1 | <?php |
2 | 2 | ||
3 | declare(strict_types=1); | ||
4 | |||
3 | namespace Shaarli\Bookmark; | 5 | namespace Shaarli\Bookmark; |
4 | 6 | ||
5 | use DateTime; | 7 | use DateTime; |
8 | use DateTimeInterface; | ||
6 | use Shaarli\Bookmark\Exception\InvalidBookmarkException; | 9 | use Shaarli\Bookmark\Exception\InvalidBookmarkException; |
7 | 10 | ||
8 | /** | 11 | /** |
@@ -36,21 +39,24 @@ class Bookmark | |||
36 | /** @var array List of bookmark's tags */ | 39 | /** @var array List of bookmark's tags */ |
37 | protected $tags; | 40 | protected $tags; |
38 | 41 | ||
39 | /** @var string Thumbnail's URL - false if no thumbnail could be found */ | 42 | /** @var string|bool|null Thumbnail's URL - initialized at null, false if no thumbnail could be found */ |
40 | protected $thumbnail; | 43 | protected $thumbnail; |
41 | 44 | ||
42 | /** @var bool Set to true if the bookmark is set as sticky */ | 45 | /** @var bool Set to true if the bookmark is set as sticky */ |
43 | protected $sticky; | 46 | protected $sticky; |
44 | 47 | ||
45 | /** @var DateTime Creation datetime */ | 48 | /** @var DateTimeInterface Creation datetime */ |
46 | protected $created; | 49 | protected $created; |
47 | 50 | ||
48 | /** @var DateTime Update datetime */ | 51 | /** @var DateTimeInterface datetime */ |
49 | protected $updated; | 52 | protected $updated; |
50 | 53 | ||
51 | /** @var bool True if the bookmark can only be seen while logged in */ | 54 | /** @var bool True if the bookmark can only be seen while logged in */ |
52 | protected $private; | 55 | protected $private; |
53 | 56 | ||
57 | /** @var mixed[] Available to store any additional content for a bookmark. Currently used for search highlight. */ | ||
58 | protected $additionalContent = []; | ||
59 | |||
54 | /** | 60 | /** |
55 | * Initialize a link from array data. Especially useful to create a Bookmark from former link storage format. | 61 | * Initialize a link from array data. Especially useful to create a Bookmark from former link storage format. |
56 | * | 62 | * |
@@ -58,25 +64,25 @@ class Bookmark | |||
58 | * | 64 | * |
59 | * @return $this | 65 | * @return $this |
60 | */ | 66 | */ |
61 | public function fromArray($data) | 67 | public function fromArray(array $data): Bookmark |
62 | { | 68 | { |
63 | $this->id = $data['id']; | 69 | $this->id = $data['id'] ?? null; |
64 | $this->shortUrl = $data['shorturl']; | 70 | $this->shortUrl = $data['shorturl'] ?? null; |
65 | $this->url = $data['url']; | 71 | $this->url = $data['url'] ?? null; |
66 | $this->title = $data['title']; | 72 | $this->title = $data['title'] ?? null; |
67 | $this->description = $data['description']; | 73 | $this->description = $data['description'] ?? null; |
68 | $this->thumbnail = isset($data['thumbnail']) ? $data['thumbnail'] : null; | 74 | $this->thumbnail = $data['thumbnail'] ?? null; |
69 | $this->sticky = isset($data['sticky']) ? $data['sticky'] : false; | 75 | $this->sticky = $data['sticky'] ?? false; |
70 | $this->created = $data['created']; | 76 | $this->created = $data['created'] ?? null; |
71 | if (is_array($data['tags'])) { | 77 | if (is_array($data['tags'])) { |
72 | $this->tags = $data['tags']; | 78 | $this->tags = $data['tags']; |
73 | } else { | 79 | } else { |
74 | $this->tags = preg_split('/\s+/', $data['tags'], -1, PREG_SPLIT_NO_EMPTY); | 80 | $this->tags = preg_split('/\s+/', $data['tags'] ?? '', -1, PREG_SPLIT_NO_EMPTY); |
75 | } | 81 | } |
76 | if (! empty($data['updated'])) { | 82 | if (! empty($data['updated'])) { |
77 | $this->updated = $data['updated']; | 83 | $this->updated = $data['updated']; |
78 | } | 84 | } |
79 | $this->private = $data['private'] ? true : false; | 85 | $this->private = ($data['private'] ?? false) ? true : false; |
80 | 86 | ||
81 | return $this; | 87 | return $this; |
82 | } | 88 | } |
@@ -92,24 +98,28 @@ class Bookmark | |||
92 | * - the URL with the permalink | 98 | * - the URL with the permalink |
93 | * - the title with the URL | 99 | * - the title with the URL |
94 | * | 100 | * |
101 | * Also make sure that we do not save search highlights in the datastore. | ||
102 | * | ||
95 | * @throws InvalidBookmarkException | 103 | * @throws InvalidBookmarkException |
96 | */ | 104 | */ |
97 | public function validate() | 105 | public function validate(): void |
98 | { | 106 | { |
99 | if ($this->id === null | 107 | if ($this->id === null |
100 | || ! is_int($this->id) | 108 | || ! is_int($this->id) |
101 | || empty($this->shortUrl) | 109 | || empty($this->shortUrl) |
102 | || empty($this->created) | 110 | || empty($this->created) |
103 | || ! $this->created instanceof DateTime | ||
104 | ) { | 111 | ) { |
105 | throw new InvalidBookmarkException($this); | 112 | throw new InvalidBookmarkException($this); |
106 | } | 113 | } |
107 | if (empty($this->url)) { | 114 | if (empty($this->url)) { |
108 | $this->url = '?'. $this->shortUrl; | 115 | $this->url = '/shaare/'. $this->shortUrl; |
109 | } | 116 | } |
110 | if (empty($this->title)) { | 117 | if (empty($this->title)) { |
111 | $this->title = $this->url; | 118 | $this->title = $this->url; |
112 | } | 119 | } |
120 | if (array_key_exists('search_highlight', $this->additionalContent)) { | ||
121 | unset($this->additionalContent['search_highlight']); | ||
122 | } | ||
113 | } | 123 | } |
114 | 124 | ||
115 | /** | 125 | /** |
@@ -118,11 +128,11 @@ class Bookmark | |||
118 | * - created: with the current datetime | 128 | * - created: with the current datetime |
119 | * - shortUrl: with a generated small hash from the date and the given ID | 129 | * - shortUrl: with a generated small hash from the date and the given ID |
120 | * | 130 | * |
121 | * @param int $id | 131 | * @param int|null $id |
122 | * | 132 | * |
123 | * @return Bookmark | 133 | * @return Bookmark |
124 | */ | 134 | */ |
125 | public function setId($id) | 135 | public function setId(?int $id): Bookmark |
126 | { | 136 | { |
127 | $this->id = $id; | 137 | $this->id = $id; |
128 | if (empty($this->created)) { | 138 | if (empty($this->created)) { |
@@ -138,9 +148,9 @@ class Bookmark | |||
138 | /** | 148 | /** |
139 | * Get the Id. | 149 | * Get the Id. |
140 | * | 150 | * |
141 | * @return int | 151 | * @return int|null |
142 | */ | 152 | */ |
143 | public function getId() | 153 | public function getId(): ?int |
144 | { | 154 | { |
145 | return $this->id; | 155 | return $this->id; |
146 | } | 156 | } |
@@ -148,9 +158,9 @@ class Bookmark | |||
148 | /** | 158 | /** |
149 | * Get the ShortUrl. | 159 | * Get the ShortUrl. |
150 | * | 160 | * |
151 | * @return string | 161 | * @return string|null |
152 | */ | 162 | */ |
153 | public function getShortUrl() | 163 | public function getShortUrl(): ?string |
154 | { | 164 | { |
155 | return $this->shortUrl; | 165 | return $this->shortUrl; |
156 | } | 166 | } |
@@ -158,9 +168,9 @@ class Bookmark | |||
158 | /** | 168 | /** |
159 | * Get the Url. | 169 | * Get the Url. |
160 | * | 170 | * |
161 | * @return string | 171 | * @return string|null |
162 | */ | 172 | */ |
163 | public function getUrl() | 173 | public function getUrl(): ?string |
164 | { | 174 | { |
165 | return $this->url; | 175 | return $this->url; |
166 | } | 176 | } |
@@ -170,7 +180,7 @@ class Bookmark | |||
170 | * | 180 | * |
171 | * @return string | 181 | * @return string |
172 | */ | 182 | */ |
173 | public function getTitle() | 183 | public function getTitle(): ?string |
174 | { | 184 | { |
175 | return $this->title; | 185 | return $this->title; |
176 | } | 186 | } |
@@ -180,7 +190,7 @@ class Bookmark | |||
180 | * | 190 | * |
181 | * @return string | 191 | * @return string |
182 | */ | 192 | */ |
183 | public function getDescription() | 193 | public function getDescription(): string |
184 | { | 194 | { |
185 | return ! empty($this->description) ? $this->description : ''; | 195 | return ! empty($this->description) ? $this->description : ''; |
186 | } | 196 | } |
@@ -188,9 +198,9 @@ class Bookmark | |||
188 | /** | 198 | /** |
189 | * Get the Created. | 199 | * Get the Created. |
190 | * | 200 | * |
191 | * @return DateTime | 201 | * @return DateTimeInterface |
192 | */ | 202 | */ |
193 | public function getCreated() | 203 | public function getCreated(): ?DateTimeInterface |
194 | { | 204 | { |
195 | return $this->created; | 205 | return $this->created; |
196 | } | 206 | } |
@@ -198,9 +208,9 @@ class Bookmark | |||
198 | /** | 208 | /** |
199 | * Get the Updated. | 209 | * Get the Updated. |
200 | * | 210 | * |
201 | * @return DateTime | 211 | * @return DateTimeInterface |
202 | */ | 212 | */ |
203 | public function getUpdated() | 213 | public function getUpdated(): ?DateTimeInterface |
204 | { | 214 | { |
205 | return $this->updated; | 215 | return $this->updated; |
206 | } | 216 | } |
@@ -208,11 +218,11 @@ class Bookmark | |||
208 | /** | 218 | /** |
209 | * Set the ShortUrl. | 219 | * Set the ShortUrl. |
210 | * | 220 | * |
211 | * @param string $shortUrl | 221 | * @param string|null $shortUrl |
212 | * | 222 | * |
213 | * @return Bookmark | 223 | * @return Bookmark |
214 | */ | 224 | */ |
215 | public function setShortUrl($shortUrl) | 225 | public function setShortUrl(?string $shortUrl): Bookmark |
216 | { | 226 | { |
217 | $this->shortUrl = $shortUrl; | 227 | $this->shortUrl = $shortUrl; |
218 | 228 | ||
@@ -222,14 +232,14 @@ class Bookmark | |||
222 | /** | 232 | /** |
223 | * Set the Url. | 233 | * Set the Url. |
224 | * | 234 | * |
225 | * @param string $url | 235 | * @param string|null $url |
226 | * @param array $allowedProtocols | 236 | * @param string[] $allowedProtocols |
227 | * | 237 | * |
228 | * @return Bookmark | 238 | * @return Bookmark |
229 | */ | 239 | */ |
230 | public function setUrl($url, $allowedProtocols = []) | 240 | public function setUrl(?string $url, array $allowedProtocols = []): Bookmark |
231 | { | 241 | { |
232 | $url = trim($url); | 242 | $url = $url !== null ? trim($url) : ''; |
233 | if (! empty($url)) { | 243 | if (! empty($url)) { |
234 | $url = whitelist_protocols($url, $allowedProtocols); | 244 | $url = whitelist_protocols($url, $allowedProtocols); |
235 | } | 245 | } |
@@ -241,13 +251,13 @@ class Bookmark | |||
241 | /** | 251 | /** |
242 | * Set the Title. | 252 | * Set the Title. |
243 | * | 253 | * |
244 | * @param string $title | 254 | * @param string|null $title |
245 | * | 255 | * |
246 | * @return Bookmark | 256 | * @return Bookmark |
247 | */ | 257 | */ |
248 | public function setTitle($title) | 258 | public function setTitle(?string $title): Bookmark |
249 | { | 259 | { |
250 | $this->title = trim($title); | 260 | $this->title = $title !== null ? trim($title) : ''; |
251 | 261 | ||
252 | return $this; | 262 | return $this; |
253 | } | 263 | } |
@@ -255,11 +265,11 @@ class Bookmark | |||
255 | /** | 265 | /** |
256 | * Set the Description. | 266 | * Set the Description. |
257 | * | 267 | * |
258 | * @param string $description | 268 | * @param string|null $description |
259 | * | 269 | * |
260 | * @return Bookmark | 270 | * @return Bookmark |
261 | */ | 271 | */ |
262 | public function setDescription($description) | 272 | public function setDescription(?string $description): Bookmark |
263 | { | 273 | { |
264 | $this->description = $description; | 274 | $this->description = $description; |
265 | 275 | ||
@@ -270,11 +280,11 @@ class Bookmark | |||
270 | * Set the Created. | 280 | * Set the Created. |
271 | * Note: you shouldn't set this manually except for special cases (like bookmark import) | 281 | * Note: you shouldn't set this manually except for special cases (like bookmark import) |
272 | * | 282 | * |
273 | * @param DateTime $created | 283 | * @param DateTimeInterface|null $created |
274 | * | 284 | * |
275 | * @return Bookmark | 285 | * @return Bookmark |
276 | */ | 286 | */ |
277 | public function setCreated($created) | 287 | public function setCreated(?DateTimeInterface $created): Bookmark |
278 | { | 288 | { |
279 | $this->created = $created; | 289 | $this->created = $created; |
280 | 290 | ||
@@ -284,11 +294,11 @@ class Bookmark | |||
284 | /** | 294 | /** |
285 | * Set the Updated. | 295 | * Set the Updated. |
286 | * | 296 | * |
287 | * @param DateTime $updated | 297 | * @param DateTimeInterface|null $updated |
288 | * | 298 | * |
289 | * @return Bookmark | 299 | * @return Bookmark |
290 | */ | 300 | */ |
291 | public function setUpdated($updated) | 301 | public function setUpdated(?DateTimeInterface $updated): Bookmark |
292 | { | 302 | { |
293 | $this->updated = $updated; | 303 | $this->updated = $updated; |
294 | 304 | ||
@@ -300,7 +310,7 @@ class Bookmark | |||
300 | * | 310 | * |
301 | * @return bool | 311 | * @return bool |
302 | */ | 312 | */ |
303 | public function isPrivate() | 313 | public function isPrivate(): bool |
304 | { | 314 | { |
305 | return $this->private ? true : false; | 315 | return $this->private ? true : false; |
306 | } | 316 | } |
@@ -308,11 +318,11 @@ class Bookmark | |||
308 | /** | 318 | /** |
309 | * Set the Private. | 319 | * Set the Private. |
310 | * | 320 | * |
311 | * @param bool $private | 321 | * @param bool|null $private |
312 | * | 322 | * |
313 | * @return Bookmark | 323 | * @return Bookmark |
314 | */ | 324 | */ |
315 | public function setPrivate($private) | 325 | public function setPrivate(?bool $private): Bookmark |
316 | { | 326 | { |
317 | $this->private = $private ? true : false; | 327 | $this->private = $private ? true : false; |
318 | 328 | ||
@@ -322,9 +332,9 @@ class Bookmark | |||
322 | /** | 332 | /** |
323 | * Get the Tags. | 333 | * Get the Tags. |
324 | * | 334 | * |
325 | * @return array | 335 | * @return string[] |
326 | */ | 336 | */ |
327 | public function getTags() | 337 | public function getTags(): array |
328 | { | 338 | { |
329 | return is_array($this->tags) ? $this->tags : []; | 339 | return is_array($this->tags) ? $this->tags : []; |
330 | } | 340 | } |
@@ -332,13 +342,13 @@ class Bookmark | |||
332 | /** | 342 | /** |
333 | * Set the Tags. | 343 | * Set the Tags. |
334 | * | 344 | * |
335 | * @param array $tags | 345 | * @param string[]|null $tags |
336 | * | 346 | * |
337 | * @return Bookmark | 347 | * @return Bookmark |
338 | */ | 348 | */ |
339 | public function setTags($tags) | 349 | public function setTags(?array $tags): Bookmark |
340 | { | 350 | { |
341 | $this->setTagsString(implode(' ', $tags)); | 351 | $this->setTagsString(implode(' ', $tags ?? [])); |
342 | 352 | ||
343 | return $this; | 353 | return $this; |
344 | } | 354 | } |
@@ -346,7 +356,7 @@ class Bookmark | |||
346 | /** | 356 | /** |
347 | * Get the Thumbnail. | 357 | * Get the Thumbnail. |
348 | * | 358 | * |
349 | * @return string|bool | 359 | * @return string|bool|null Thumbnail's URL - initialized at null, false if no thumbnail could be found |
350 | */ | 360 | */ |
351 | public function getThumbnail() | 361 | public function getThumbnail() |
352 | { | 362 | { |
@@ -356,11 +366,11 @@ class Bookmark | |||
356 | /** | 366 | /** |
357 | * Set the Thumbnail. | 367 | * Set the Thumbnail. |
358 | * | 368 | * |
359 | * @param string|bool $thumbnail | 369 | * @param string|bool|null $thumbnail Thumbnail's URL - false if no thumbnail could be found |
360 | * | 370 | * |
361 | * @return Bookmark | 371 | * @return Bookmark |
362 | */ | 372 | */ |
363 | public function setThumbnail($thumbnail) | 373 | public function setThumbnail($thumbnail): Bookmark |
364 | { | 374 | { |
365 | $this->thumbnail = $thumbnail; | 375 | $this->thumbnail = $thumbnail; |
366 | 376 | ||
@@ -368,11 +378,29 @@ class Bookmark | |||
368 | } | 378 | } |
369 | 379 | ||
370 | /** | 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 | /** | ||
371 | * Get the Sticky. | 399 | * Get the Sticky. |
372 | * | 400 | * |
373 | * @return bool | 401 | * @return bool |
374 | */ | 402 | */ |
375 | public function isSticky() | 403 | public function isSticky(): bool |
376 | { | 404 | { |
377 | return $this->sticky ? true : false; | 405 | return $this->sticky ? true : false; |
378 | } | 406 | } |
@@ -380,11 +408,11 @@ class Bookmark | |||
380 | /** | 408 | /** |
381 | * Set the Sticky. | 409 | * Set the Sticky. |
382 | * | 410 | * |
383 | * @param bool $sticky | 411 | * @param bool|null $sticky |
384 | * | 412 | * |
385 | * @return Bookmark | 413 | * @return Bookmark |
386 | */ | 414 | */ |
387 | public function setSticky($sticky) | 415 | public function setSticky(?bool $sticky): Bookmark |
388 | { | 416 | { |
389 | $this->sticky = $sticky ? true : false; | 417 | $this->sticky = $sticky ? true : false; |
390 | 418 | ||
@@ -394,7 +422,7 @@ class Bookmark | |||
394 | /** | 422 | /** |
395 | * @return string Bookmark's tags as a string, separated by a space | 423 | * @return string Bookmark's tags as a string, separated by a space |
396 | */ | 424 | */ |
397 | public function getTagsString() | 425 | public function getTagsString(): string |
398 | { | 426 | { |
399 | return implode(' ', $this->getTags()); | 427 | return implode(' ', $this->getTags()); |
400 | } | 428 | } |
@@ -402,10 +430,10 @@ class Bookmark | |||
402 | /** | 430 | /** |
403 | * @return bool | 431 | * @return bool |
404 | */ | 432 | */ |
405 | public function isNote() | 433 | public function isNote(): bool |
406 | { | 434 | { |
407 | // We check empty value to get a valid result if the link has not been saved yet | 435 | // We check empty value to get a valid result if the link has not been saved yet |
408 | return empty($this->url) || $this->url[0] === '?'; | 436 | return empty($this->url) || startsWith($this->url, '/shaare/') || $this->url[0] === '?'; |
409 | } | 437 | } |
410 | 438 | ||
411 | /** | 439 | /** |
@@ -415,14 +443,14 @@ class Bookmark | |||
415 | * - multiple spaces will be removed | 443 | * - multiple spaces will be removed |
416 | * - trailing dash in tags will be removed | 444 | * - trailing dash in tags will be removed |
417 | * | 445 | * |
418 | * @param string $tags | 446 | * @param string|null $tags |
419 | * | 447 | * |
420 | * @return $this | 448 | * @return $this |
421 | */ | 449 | */ |
422 | public function setTagsString($tags) | 450 | public function setTagsString(?string $tags): Bookmark |
423 | { | 451 | { |
424 | // Remove first '-' char in tags. | 452 | // Remove first '-' char in tags. |
425 | $tags = preg_replace('/(^| )\-/', '$1', $tags); | 453 | $tags = preg_replace('/(^| )\-/', '$1', $tags ?? ''); |
426 | // Explode all tags separted by spaces or commas | 454 | // Explode all tags separted by spaces or commas |
427 | $tags = preg_split('/[\s,]+/', $tags); | 455 | $tags = preg_split('/[\s,]+/', $tags); |
428 | // Remove eventual empty values | 456 | // Remove eventual empty values |
@@ -434,12 +462,50 @@ class Bookmark | |||
434 | } | 462 | } |
435 | 463 | ||
436 | /** | 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 | /** | ||
437 | * Rename a tag in tags list. | 503 | * Rename a tag in tags list. |
438 | * | 504 | * |
439 | * @param string $fromTag | 505 | * @param string $fromTag |
440 | * @param string $toTag | 506 | * @param string $toTag |
441 | */ | 507 | */ |
442 | public function renameTag($fromTag, $toTag) | 508 | public function renameTag(string $fromTag, string $toTag): void |
443 | { | 509 | { |
444 | if (($pos = array_search($fromTag, $this->tags)) !== false) { | 510 | if (($pos = array_search($fromTag, $this->tags)) !== false) { |
445 | $this->tags[$pos] = trim($toTag); | 511 | $this->tags[$pos] = trim($toTag); |
@@ -451,7 +517,7 @@ class Bookmark | |||
451 | * | 517 | * |
452 | * @param string $tag | 518 | * @param string $tag |
453 | */ | 519 | */ |
454 | public function deleteTag($tag) | 520 | public function deleteTag(string $tag): void |
455 | { | 521 | { |
456 | if (($pos = array_search($tag, $this->tags)) !== false) { | 522 | if (($pos = array_search($tag, $this->tags)) !== false) { |
457 | unset($this->tags[$pos]); | 523 | unset($this->tags[$pos]); |
diff --git a/application/bookmark/BookmarkArray.php b/application/bookmark/BookmarkArray.php index d87d43b4..67bb3b73 100644 --- a/application/bookmark/BookmarkArray.php +++ b/application/bookmark/BookmarkArray.php | |||
@@ -1,5 +1,7 @@ | |||
1 | <?php | 1 | <?php |
2 | 2 | ||
3 | declare(strict_types=1); | ||
4 | |||
3 | namespace Shaarli\Bookmark; | 5 | namespace Shaarli\Bookmark; |
4 | 6 | ||
5 | use Shaarli\Bookmark\Exception\InvalidBookmarkException; | 7 | use Shaarli\Bookmark\Exception\InvalidBookmarkException; |
@@ -187,13 +189,13 @@ class BookmarkArray implements \Iterator, \Countable, \ArrayAccess | |||
187 | /** | 189 | /** |
188 | * Returns a bookmark offset in bookmarks array from its unique ID. | 190 | * Returns a bookmark offset in bookmarks array from its unique ID. |
189 | * | 191 | * |
190 | * @param int $id Persistent ID of a bookmark. | 192 | * @param int|null $id Persistent ID of a bookmark. |
191 | * | 193 | * |
192 | * @return int Real offset in local array, or null if doesn't exist. | 194 | * @return int Real offset in local array, or null if doesn't exist. |
193 | */ | 195 | */ |
194 | protected function getBookmarkOffset($id) | 196 | protected function getBookmarkOffset(?int $id): ?int |
195 | { | 197 | { |
196 | if (isset($this->ids[$id])) { | 198 | if ($id !== null && isset($this->ids[$id])) { |
197 | return $this->ids[$id]; | 199 | return $this->ids[$id]; |
198 | } | 200 | } |
199 | return null; | 201 | return null; |
@@ -205,7 +207,7 @@ class BookmarkArray implements \Iterator, \Countable, \ArrayAccess | |||
205 | * | 207 | * |
206 | * @return int next ID. | 208 | * @return int next ID. |
207 | */ | 209 | */ |
208 | public function getNextId() | 210 | public function getNextId(): int |
209 | { | 211 | { |
210 | if (!empty($this->ids)) { | 212 | if (!empty($this->ids)) { |
211 | return max(array_keys($this->ids)) + 1; | 213 | return max(array_keys($this->ids)) + 1; |
@@ -214,11 +216,11 @@ class BookmarkArray implements \Iterator, \Countable, \ArrayAccess | |||
214 | } | 216 | } |
215 | 217 | ||
216 | /** | 218 | /** |
217 | * @param $url | 219 | * @param string $url |
218 | * | 220 | * |
219 | * @return Bookmark|null | 221 | * @return Bookmark|null |
220 | */ | 222 | */ |
221 | public function getByUrl($url) | 223 | public function getByUrl(string $url): ?Bookmark |
222 | { | 224 | { |
223 | if (! empty($url) | 225 | if (! empty($url) |
224 | && isset($this->urls[$url]) | 226 | && isset($this->urls[$url]) |
@@ -234,16 +236,17 @@ class BookmarkArray implements \Iterator, \Countable, \ArrayAccess | |||
234 | * | 236 | * |
235 | * Also update the urls and ids mapping arrays. | 237 | * Also update the urls and ids mapping arrays. |
236 | * | 238 | * |
237 | * @param string $order ASC|DESC | 239 | * @param string $order ASC|DESC |
240 | * @param bool $ignoreSticky If set to true, sticky bookmarks won't be first | ||
238 | */ | 241 | */ |
239 | public function reorder($order = 'DESC') | 242 | public function reorder(string $order = 'DESC', bool $ignoreSticky = false): void |
240 | { | 243 | { |
241 | $order = $order === 'ASC' ? -1 : 1; | 244 | $order = $order === 'ASC' ? -1 : 1; |
242 | // Reorder array by dates. | 245 | // Reorder array by dates. |
243 | usort($this->bookmarks, function ($a, $b) use ($order) { | 246 | usort($this->bookmarks, function ($a, $b) use ($order, $ignoreSticky) { |
244 | /** @var $a Bookmark */ | 247 | /** @var $a Bookmark */ |
245 | /** @var $b Bookmark */ | 248 | /** @var $b Bookmark */ |
246 | if ($a->isSticky() !== $b->isSticky()) { | 249 | if (false === $ignoreSticky && $a->isSticky() !== $b->isSticky()) { |
247 | return $a->isSticky() ? -1 : 1; | 250 | return $a->isSticky() ? -1 : 1; |
248 | } | 251 | } |
249 | return $a->getCreated() < $b->getCreated() ? 1 * $order : -1 * $order; | 252 | return $a->getCreated() < $b->getCreated() ? 1 * $order : -1 * $order; |
diff --git a/application/bookmark/BookmarkFileService.php b/application/bookmark/BookmarkFileService.php index 9c59e139..3ea98a45 100644 --- a/application/bookmark/BookmarkFileService.php +++ b/application/bookmark/BookmarkFileService.php | |||
@@ -1,17 +1,21 @@ | |||
1 | <?php | 1 | <?php |
2 | 2 | ||
3 | declare(strict_types=1); | ||
3 | 4 | ||
4 | namespace Shaarli\Bookmark; | 5 | namespace Shaarli\Bookmark; |
5 | 6 | ||
6 | 7 | use DateTime; | |
7 | use Exception; | 8 | use Exception; |
9 | use malkusch\lock\mutex\Mutex; | ||
8 | use Shaarli\Bookmark\Exception\BookmarkNotFoundException; | 10 | use Shaarli\Bookmark\Exception\BookmarkNotFoundException; |
11 | use Shaarli\Bookmark\Exception\DatastoreNotInitializedException; | ||
9 | use Shaarli\Bookmark\Exception\EmptyDataStoreException; | 12 | use Shaarli\Bookmark\Exception\EmptyDataStoreException; |
10 | use Shaarli\Config\ConfigManager; | 13 | use Shaarli\Config\ConfigManager; |
11 | use Shaarli\Formatter\BookmarkMarkdownFormatter; | 14 | use Shaarli\Formatter\BookmarkMarkdownFormatter; |
12 | use Shaarli\History; | 15 | use Shaarli\History; |
13 | use Shaarli\Legacy\LegacyLinkDB; | 16 | use Shaarli\Legacy\LegacyLinkDB; |
14 | use Shaarli\Legacy\LegacyUpdater; | 17 | use Shaarli\Legacy\LegacyUpdater; |
18 | use Shaarli\Render\PageCacheManager; | ||
15 | use Shaarli\Updater\UpdaterUtils; | 19 | use Shaarli\Updater\UpdaterUtils; |
16 | 20 | ||
17 | /** | 21 | /** |
@@ -39,17 +43,25 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
39 | /** @var History instance */ | 43 | /** @var History instance */ |
40 | protected $history; | 44 | protected $history; |
41 | 45 | ||
46 | /** @var PageCacheManager instance */ | ||
47 | protected $pageCacheManager; | ||
48 | |||
42 | /** @var bool true for logged in users. Default value to retrieve private bookmarks. */ | 49 | /** @var bool true for logged in users. Default value to retrieve private bookmarks. */ |
43 | protected $isLoggedIn; | 50 | protected $isLoggedIn; |
44 | 51 | ||
52 | /** @var Mutex */ | ||
53 | protected $mutex; | ||
54 | |||
45 | /** | 55 | /** |
46 | * @inheritDoc | 56 | * @inheritDoc |
47 | */ | 57 | */ |
48 | public function __construct(ConfigManager $conf, History $history, $isLoggedIn) | 58 | public function __construct(ConfigManager $conf, History $history, Mutex $mutex, bool $isLoggedIn) |
49 | { | 59 | { |
50 | $this->conf = $conf; | 60 | $this->conf = $conf; |
51 | $this->history = $history; | 61 | $this->history = $history; |
52 | $this->bookmarksIO = new BookmarkIO($this->conf); | 62 | $this->mutex = $mutex; |
63 | $this->pageCacheManager = new PageCacheManager($this->conf->get('resource.page_cache'), $isLoggedIn); | ||
64 | $this->bookmarksIO = new BookmarkIO($this->conf, $this->mutex); | ||
53 | $this->isLoggedIn = $isLoggedIn; | 65 | $this->isLoggedIn = $isLoggedIn; |
54 | 66 | ||
55 | if (!$this->isLoggedIn && $this->conf->get('privacy.hide_public_links', false)) { | 67 | if (!$this->isLoggedIn && $this->conf->get('privacy.hide_public_links', false)) { |
@@ -57,10 +69,16 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
57 | } else { | 69 | } else { |
58 | try { | 70 | try { |
59 | $this->bookmarks = $this->bookmarksIO->read(); | 71 | $this->bookmarks = $this->bookmarksIO->read(); |
60 | } catch (EmptyDataStoreException $e) { | 72 | } catch (EmptyDataStoreException|DatastoreNotInitializedException $e) { |
61 | $this->bookmarks = new BookmarkArray(); | 73 | $this->bookmarks = new BookmarkArray(); |
62 | if ($isLoggedIn) { | 74 | |
63 | $this->save(); | 75 | if ($this->isLoggedIn) { |
76 | // Datastore file does not exists, we initialize it with default bookmarks. | ||
77 | if ($e instanceof DatastoreNotInitializedException) { | ||
78 | $this->initialize(); | ||
79 | } else { | ||
80 | $this->save(); | ||
81 | } | ||
64 | } | 82 | } |
65 | } | 83 | } |
66 | 84 | ||
@@ -79,22 +97,25 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
79 | /** | 97 | /** |
80 | * @inheritDoc | 98 | * @inheritDoc |
81 | */ | 99 | */ |
82 | public function findByHash($hash) | 100 | public function findByHash(string $hash, string $privateKey = null): Bookmark |
83 | { | 101 | { |
84 | $bookmark = $this->bookmarkFilter->filter(BookmarkFilter::$FILTER_HASH, $hash); | 102 | $bookmark = $this->bookmarkFilter->filter(BookmarkFilter::$FILTER_HASH, $hash); |
85 | // PHP 7.3 introduced array_key_first() to avoid this hack | 103 | // PHP 7.3 introduced array_key_first() to avoid this hack |
86 | $first = reset($bookmark); | 104 | $first = reset($bookmark); |
87 | if (! $this->isLoggedIn && $first->isPrivate()) { | 105 | if (!$this->isLoggedIn |
88 | throw new Exception('Not authorized'); | 106 | && $first->isPrivate() |
107 | && (empty($privateKey) || $privateKey !== $first->getAdditionalContentEntry('private_key')) | ||
108 | ) { | ||
109 | throw new BookmarkNotFoundException(); | ||
89 | } | 110 | } |
90 | 111 | ||
91 | return $bookmark; | 112 | return $first; |
92 | } | 113 | } |
93 | 114 | ||
94 | /** | 115 | /** |
95 | * @inheritDoc | 116 | * @inheritDoc |
96 | */ | 117 | */ |
97 | public function findByUrl($url) | 118 | public function findByUrl(string $url): ?Bookmark |
98 | { | 119 | { |
99 | return $this->bookmarks->getByUrl($url); | 120 | return $this->bookmarks->getByUrl($url); |
100 | } | 121 | } |
@@ -102,19 +123,28 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
102 | /** | 123 | /** |
103 | * @inheritDoc | 124 | * @inheritDoc |
104 | */ | 125 | */ |
105 | public function search($request = [], $visibility = null, $caseSensitive = false, $untaggedOnly = false) | 126 | public function search( |
106 | { | 127 | array $request = [], |
128 | string $visibility = null, | ||
129 | bool $caseSensitive = false, | ||
130 | bool $untaggedOnly = false, | ||
131 | bool $ignoreSticky = false | ||
132 | ) { | ||
107 | if ($visibility === null) { | 133 | if ($visibility === null) { |
108 | $visibility = $this->isLoggedIn ? BookmarkFilter::$ALL : BookmarkFilter::$PUBLIC; | 134 | $visibility = $this->isLoggedIn ? BookmarkFilter::$ALL : BookmarkFilter::$PUBLIC; |
109 | } | 135 | } |
110 | 136 | ||
111 | // Filter bookmark database according to parameters. | 137 | // Filter bookmark database according to parameters. |
112 | $searchtags = isset($request['searchtags']) ? $request['searchtags'] : ''; | 138 | $searchTags = isset($request['searchtags']) ? $request['searchtags'] : ''; |
113 | $searchterm = isset($request['searchterm']) ? $request['searchterm'] : ''; | 139 | $searchTerm = isset($request['searchterm']) ? $request['searchterm'] : ''; |
140 | |||
141 | if ($ignoreSticky) { | ||
142 | $this->bookmarks->reorder('DESC', true); | ||
143 | } | ||
114 | 144 | ||
115 | return $this->bookmarkFilter->filter( | 145 | return $this->bookmarkFilter->filter( |
116 | BookmarkFilter::$FILTER_TAG | BookmarkFilter::$FILTER_TEXT, | 146 | BookmarkFilter::$FILTER_TAG | BookmarkFilter::$FILTER_TEXT, |
117 | [$searchtags, $searchterm], | 147 | [$searchTags, $searchTerm], |
118 | $caseSensitive, | 148 | $caseSensitive, |
119 | $visibility, | 149 | $visibility, |
120 | $untaggedOnly | 150 | $untaggedOnly |
@@ -124,7 +154,7 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
124 | /** | 154 | /** |
125 | * @inheritDoc | 155 | * @inheritDoc |
126 | */ | 156 | */ |
127 | public function get($id, $visibility = null) | 157 | public function get(int $id, string $visibility = null): Bookmark |
128 | { | 158 | { |
129 | if (! isset($this->bookmarks[$id])) { | 159 | if (! isset($this->bookmarks[$id])) { |
130 | throw new BookmarkNotFoundException(); | 160 | throw new BookmarkNotFoundException(); |
@@ -147,20 +177,17 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
147 | /** | 177 | /** |
148 | * @inheritDoc | 178 | * @inheritDoc |
149 | */ | 179 | */ |
150 | public function set($bookmark, $save = true) | 180 | public function set(Bookmark $bookmark, bool $save = true): Bookmark |
151 | { | 181 | { |
152 | if ($this->isLoggedIn !== true) { | 182 | if (true !== $this->isLoggedIn) { |
153 | throw new Exception(t('You\'re not authorized to alter the datastore')); | 183 | throw new Exception(t('You\'re not authorized to alter the datastore')); |
154 | } | 184 | } |
155 | if (! $bookmark instanceof Bookmark) { | ||
156 | throw new Exception(t('Provided data is invalid')); | ||
157 | } | ||
158 | if (! isset($this->bookmarks[$bookmark->getId()])) { | 185 | if (! isset($this->bookmarks[$bookmark->getId()])) { |
159 | throw new BookmarkNotFoundException(); | 186 | throw new BookmarkNotFoundException(); |
160 | } | 187 | } |
161 | $bookmark->validate(); | 188 | $bookmark->validate(); |
162 | 189 | ||
163 | $bookmark->setUpdated(new \DateTime()); | 190 | $bookmark->setUpdated(new DateTime()); |
164 | $this->bookmarks[$bookmark->getId()] = $bookmark; | 191 | $this->bookmarks[$bookmark->getId()] = $bookmark; |
165 | if ($save === true) { | 192 | if ($save === true) { |
166 | $this->save(); | 193 | $this->save(); |
@@ -172,15 +199,12 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
172 | /** | 199 | /** |
173 | * @inheritDoc | 200 | * @inheritDoc |
174 | */ | 201 | */ |
175 | public function add($bookmark, $save = true) | 202 | public function add(Bookmark $bookmark, bool $save = true): Bookmark |
176 | { | 203 | { |
177 | if ($this->isLoggedIn !== true) { | 204 | if (true !== $this->isLoggedIn) { |
178 | throw new Exception(t('You\'re not authorized to alter the datastore')); | 205 | throw new Exception(t('You\'re not authorized to alter the datastore')); |
179 | } | 206 | } |
180 | if (! $bookmark instanceof Bookmark) { | 207 | if (!empty($bookmark->getId())) { |
181 | throw new Exception(t('Provided data is invalid')); | ||
182 | } | ||
183 | if (! empty($bookmark->getId())) { | ||
184 | throw new Exception(t('This bookmarks already exists')); | 208 | throw new Exception(t('This bookmarks already exists')); |
185 | } | 209 | } |
186 | $bookmark->setId($this->bookmarks->getNextId()); | 210 | $bookmark->setId($this->bookmarks->getNextId()); |
@@ -197,14 +221,11 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
197 | /** | 221 | /** |
198 | * @inheritDoc | 222 | * @inheritDoc |
199 | */ | 223 | */ |
200 | public function addOrSet($bookmark, $save = true) | 224 | public function addOrSet(Bookmark $bookmark, bool $save = true): Bookmark |
201 | { | 225 | { |
202 | if ($this->isLoggedIn !== true) { | 226 | if (true !== $this->isLoggedIn) { |
203 | throw new Exception(t('You\'re not authorized to alter the datastore')); | 227 | throw new Exception(t('You\'re not authorized to alter the datastore')); |
204 | } | 228 | } |
205 | if (! $bookmark instanceof Bookmark) { | ||
206 | throw new Exception('Provided data is invalid'); | ||
207 | } | ||
208 | if ($bookmark->getId() === null) { | 229 | if ($bookmark->getId() === null) { |
209 | return $this->add($bookmark, $save); | 230 | return $this->add($bookmark, $save); |
210 | } | 231 | } |
@@ -214,14 +235,11 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
214 | /** | 235 | /** |
215 | * @inheritDoc | 236 | * @inheritDoc |
216 | */ | 237 | */ |
217 | public function remove($bookmark, $save = true) | 238 | public function remove(Bookmark $bookmark, bool $save = true): void |
218 | { | 239 | { |
219 | if ($this->isLoggedIn !== true) { | 240 | if (true !== $this->isLoggedIn) { |
220 | throw new Exception(t('You\'re not authorized to alter the datastore')); | 241 | throw new Exception(t('You\'re not authorized to alter the datastore')); |
221 | } | 242 | } |
222 | if (! $bookmark instanceof Bookmark) { | ||
223 | throw new Exception(t('Provided data is invalid')); | ||
224 | } | ||
225 | if (! isset($this->bookmarks[$bookmark->getId()])) { | 243 | if (! isset($this->bookmarks[$bookmark->getId()])) { |
226 | throw new BookmarkNotFoundException(); | 244 | throw new BookmarkNotFoundException(); |
227 | } | 245 | } |
@@ -236,7 +254,7 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
236 | /** | 254 | /** |
237 | * @inheritDoc | 255 | * @inheritDoc |
238 | */ | 256 | */ |
239 | public function exists($id, $visibility = null) | 257 | public function exists(int $id, string $visibility = null): bool |
240 | { | 258 | { |
241 | if (! isset($this->bookmarks[$id])) { | 259 | if (! isset($this->bookmarks[$id])) { |
242 | return false; | 260 | return false; |
@@ -259,7 +277,7 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
259 | /** | 277 | /** |
260 | * @inheritDoc | 278 | * @inheritDoc |
261 | */ | 279 | */ |
262 | public function count($visibility = null) | 280 | public function count(string $visibility = null): int |
263 | { | 281 | { |
264 | return count($this->search([], $visibility)); | 282 | return count($this->search([], $visibility)); |
265 | } | 283 | } |
@@ -267,21 +285,22 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
267 | /** | 285 | /** |
268 | * @inheritDoc | 286 | * @inheritDoc |
269 | */ | 287 | */ |
270 | public function save() | 288 | public function save(): void |
271 | { | 289 | { |
272 | if (!$this->isLoggedIn) { | 290 | if (true !== $this->isLoggedIn) { |
273 | // TODO: raise an Exception instead | 291 | // TODO: raise an Exception instead |
274 | die('You are not authorized to change the database.'); | 292 | die('You are not authorized to change the database.'); |
275 | } | 293 | } |
294 | |||
276 | $this->bookmarks->reorder(); | 295 | $this->bookmarks->reorder(); |
277 | $this->bookmarksIO->write($this->bookmarks); | 296 | $this->bookmarksIO->write($this->bookmarks); |
278 | invalidateCaches($this->conf->get('resource.page_cache')); | 297 | $this->pageCacheManager->invalidateCaches(); |
279 | } | 298 | } |
280 | 299 | ||
281 | /** | 300 | /** |
282 | * @inheritDoc | 301 | * @inheritDoc |
283 | */ | 302 | */ |
284 | public function bookmarksCountPerTag($filteringTags = [], $visibility = null) | 303 | public function bookmarksCountPerTag(array $filteringTags = [], string $visibility = null): array |
285 | { | 304 | { |
286 | $bookmarks = $this->search(['searchtags' => $filteringTags], $visibility); | 305 | $bookmarks = $this->search(['searchtags' => $filteringTags], $visibility); |
287 | $tags = []; | 306 | $tags = []; |
@@ -291,6 +310,7 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
291 | if (empty($tag) | 310 | if (empty($tag) |
292 | || (! $this->isLoggedIn && startsWith($tag, '.')) | 311 | || (! $this->isLoggedIn && startsWith($tag, '.')) |
293 | || $tag === BookmarkMarkdownFormatter::NO_MD_TAG | 312 | || $tag === BookmarkMarkdownFormatter::NO_MD_TAG |
313 | || in_array($tag, $filteringTags, true) | ||
294 | ) { | 314 | ) { |
295 | continue; | 315 | continue; |
296 | } | 316 | } |
@@ -316,45 +336,68 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
316 | $keys = array_keys($tags); | 336 | $keys = array_keys($tags); |
317 | $tmpTags = array_combine($keys, $keys); | 337 | $tmpTags = array_combine($keys, $keys); |
318 | array_multisort($tags, SORT_DESC, $tmpTags, SORT_ASC, $tags); | 338 | array_multisort($tags, SORT_DESC, $tmpTags, SORT_ASC, $tags); |
339 | |||
319 | return $tags; | 340 | return $tags; |
320 | } | 341 | } |
321 | 342 | ||
322 | /** | 343 | /** |
323 | * @inheritDoc | 344 | * @inheritDoc |
324 | */ | 345 | */ |
325 | public function days() | 346 | public function findByDate( |
326 | { | 347 | \DateTimeInterface $from, |
327 | $bookmarkDays = []; | 348 | \DateTimeInterface $to, |
328 | foreach ($this->search() as $bookmark) { | 349 | ?\DateTimeInterface &$previous, |
329 | $bookmarkDays[$bookmark->getCreated()->format('Ymd')] = 0; | 350 | ?\DateTimeInterface &$next |
351 | ): array { | ||
352 | $out = []; | ||
353 | $previous = null; | ||
354 | $next = null; | ||
355 | |||
356 | foreach ($this->search([], null, false, false, true) as $bookmark) { | ||
357 | if ($to < $bookmark->getCreated()) { | ||
358 | $next = $bookmark->getCreated(); | ||
359 | } else if ($from < $bookmark->getCreated() && $to > $bookmark->getCreated()) { | ||
360 | $out[] = $bookmark; | ||
361 | } else { | ||
362 | if ($previous !== null) { | ||
363 | break; | ||
364 | } | ||
365 | $previous = $bookmark->getCreated(); | ||
366 | } | ||
330 | } | 367 | } |
331 | $bookmarkDays = array_keys($bookmarkDays); | ||
332 | sort($bookmarkDays); | ||
333 | 368 | ||
334 | return $bookmarkDays; | 369 | return $out; |
335 | } | 370 | } |
336 | 371 | ||
337 | /** | 372 | /** |
338 | * @inheritDoc | 373 | * @inheritDoc |
339 | */ | 374 | */ |
340 | public function filterDay($request) | 375 | public function getLatest(): ?Bookmark |
341 | { | 376 | { |
342 | return $this->bookmarkFilter->filter(BookmarkFilter::$FILTER_DAY, $request); | 377 | foreach ($this->search([], null, false, false, true) as $bookmark) { |
378 | return $bookmark; | ||
379 | } | ||
380 | |||
381 | return null; | ||
343 | } | 382 | } |
344 | 383 | ||
345 | /** | 384 | /** |
346 | * @inheritDoc | 385 | * @inheritDoc |
347 | */ | 386 | */ |
348 | public function initialize() | 387 | public function initialize(): void |
349 | { | 388 | { |
350 | $initializer = new BookmarkInitializer($this); | 389 | $initializer = new BookmarkInitializer($this); |
351 | $initializer->initialize(); | 390 | $initializer->initialize(); |
391 | |||
392 | if (true === $this->isLoggedIn) { | ||
393 | $this->save(); | ||
394 | } | ||
352 | } | 395 | } |
353 | 396 | ||
354 | /** | 397 | /** |
355 | * Handles migration to the new database format (BookmarksArray). | 398 | * Handles migration to the new database format (BookmarksArray). |
356 | */ | 399 | */ |
357 | protected function migrate() | 400 | protected function migrate(): void |
358 | { | 401 | { |
359 | $bookmarkDb = new LegacyLinkDB( | 402 | $bookmarkDb = new LegacyLinkDB( |
360 | $this->conf->get('resource.datastore'), | 403 | $this->conf->get('resource.datastore'), |
diff --git a/application/bookmark/BookmarkFilter.php b/application/bookmark/BookmarkFilter.php index fd556679..c79386ea 100644 --- a/application/bookmark/BookmarkFilter.php +++ b/application/bookmark/BookmarkFilter.php | |||
@@ -1,5 +1,7 @@ | |||
1 | <?php | 1 | <?php |
2 | 2 | ||
3 | declare(strict_types=1); | ||
4 | |||
3 | namespace Shaarli\Bookmark; | 5 | namespace Shaarli\Bookmark; |
4 | 6 | ||
5 | use Exception; | 7 | use Exception; |
@@ -77,8 +79,13 @@ class BookmarkFilter | |||
77 | * | 79 | * |
78 | * @throws BookmarkNotFoundException | 80 | * @throws BookmarkNotFoundException |
79 | */ | 81 | */ |
80 | public function filter($type, $request, $casesensitive = false, $visibility = 'all', $untaggedonly = false) | 82 | public function filter( |
81 | { | 83 | string $type, |
84 | $request, | ||
85 | bool $casesensitive = false, | ||
86 | string $visibility = 'all', | ||
87 | bool $untaggedonly = false | ||
88 | ) { | ||
82 | if (!in_array($visibility, ['all', 'public', 'private'])) { | 89 | if (!in_array($visibility, ['all', 'public', 'private'])) { |
83 | $visibility = 'all'; | 90 | $visibility = 'all'; |
84 | } | 91 | } |
@@ -115,7 +122,7 @@ class BookmarkFilter | |||
115 | return $this->filterTags($request, $casesensitive, $visibility); | 122 | return $this->filterTags($request, $casesensitive, $visibility); |
116 | } | 123 | } |
117 | case self::$FILTER_DAY: | 124 | case self::$FILTER_DAY: |
118 | return $this->filterDay($request); | 125 | return $this->filterDay($request, $visibility); |
119 | default: | 126 | default: |
120 | return $this->noFilter($visibility); | 127 | return $this->noFilter($visibility); |
121 | } | 128 | } |
@@ -128,7 +135,7 @@ class BookmarkFilter | |||
128 | * | 135 | * |
129 | * @return Bookmark[] filtered bookmarks. | 136 | * @return Bookmark[] filtered bookmarks. |
130 | */ | 137 | */ |
131 | private function noFilter($visibility = 'all') | 138 | private function noFilter(string $visibility = 'all') |
132 | { | 139 | { |
133 | if ($visibility === 'all') { | 140 | if ($visibility === 'all') { |
134 | return $this->bookmarks; | 141 | return $this->bookmarks; |
@@ -151,11 +158,11 @@ class BookmarkFilter | |||
151 | * | 158 | * |
152 | * @param string $smallHash permalink hash. | 159 | * @param string $smallHash permalink hash. |
153 | * | 160 | * |
154 | * @return array $filtered array containing permalink data. | 161 | * @return Bookmark[] $filtered array containing permalink data. |
155 | * | 162 | * |
156 | * @throws \Shaarli\Bookmark\Exception\BookmarkNotFoundException if the smallhash doesn't match any link. | 163 | * @throws BookmarkNotFoundException if the smallhash doesn't match any link. |
157 | */ | 164 | */ |
158 | private function filterSmallHash($smallHash) | 165 | private function filterSmallHash(string $smallHash) |
159 | { | 166 | { |
160 | foreach ($this->bookmarks as $key => $l) { | 167 | foreach ($this->bookmarks as $key => $l) { |
161 | if ($smallHash == $l->getShortUrl()) { | 168 | if ($smallHash == $l->getShortUrl()) { |
@@ -186,15 +193,15 @@ class BookmarkFilter | |||
186 | * @param string $searchterms search query. | 193 | * @param string $searchterms search query. |
187 | * @param string $visibility Optional: return only all/private/public bookmarks. | 194 | * @param string $visibility Optional: return only all/private/public bookmarks. |
188 | * | 195 | * |
189 | * @return array search results. | 196 | * @return Bookmark[] search results. |
190 | */ | 197 | */ |
191 | private function filterFulltext($searchterms, $visibility = 'all') | 198 | private function filterFulltext(string $searchterms, string $visibility = 'all') |
192 | { | 199 | { |
193 | if (empty($searchterms)) { | 200 | if (empty($searchterms)) { |
194 | return $this->noFilter($visibility); | 201 | return $this->noFilter($visibility); |
195 | } | 202 | } |
196 | 203 | ||
197 | $filtered = array(); | 204 | $filtered = []; |
198 | $search = mb_convert_case(html_entity_decode($searchterms), MB_CASE_LOWER, 'UTF-8'); | 205 | $search = mb_convert_case(html_entity_decode($searchterms), MB_CASE_LOWER, 'UTF-8'); |
199 | $exactRegex = '/"([^"]+)"/'; | 206 | $exactRegex = '/"([^"]+)"/'; |
200 | // Retrieve exact search terms. | 207 | // Retrieve exact search terms. |
@@ -206,8 +213,8 @@ class BookmarkFilter | |||
206 | $explodedSearchAnd = array_values(array_filter($explodedSearchAnd)); | 213 | $explodedSearchAnd = array_values(array_filter($explodedSearchAnd)); |
207 | 214 | ||
208 | // Filter excluding terms and update andSearch. | 215 | // Filter excluding terms and update andSearch. |
209 | $excludeSearch = array(); | 216 | $excludeSearch = []; |
210 | $andSearch = array(); | 217 | $andSearch = []; |
211 | foreach ($explodedSearchAnd as $needle) { | 218 | foreach ($explodedSearchAnd as $needle) { |
212 | if ($needle[0] == '-' && strlen($needle) > 1) { | 219 | if ($needle[0] == '-' && strlen($needle) > 1) { |
213 | $excludeSearch[] = substr($needle, 1); | 220 | $excludeSearch[] = substr($needle, 1); |
@@ -227,33 +234,38 @@ class BookmarkFilter | |||
227 | } | 234 | } |
228 | } | 235 | } |
229 | 236 | ||
230 | // Concatenate link fields to search across fields. | 237 | $lengths = []; |
231 | // Adds a '\' separator for exact search terms. | 238 | $content = $this->buildFullTextSearchableLink($link, $lengths); |
232 | $content = mb_convert_case($link->getTitle(), MB_CASE_LOWER, 'UTF-8') .'\\'; | ||
233 | $content .= mb_convert_case($link->getDescription(), MB_CASE_LOWER, 'UTF-8') .'\\'; | ||
234 | $content .= mb_convert_case($link->getUrl(), MB_CASE_LOWER, 'UTF-8') .'\\'; | ||
235 | $content .= mb_convert_case($link->getTagsString(), MB_CASE_LOWER, 'UTF-8') .'\\'; | ||
236 | 239 | ||
237 | // Be optimistic | 240 | // Be optimistic |
238 | $found = true; | 241 | $found = true; |
242 | $foundPositions = []; | ||
239 | 243 | ||
240 | // First, we look for exact term search | 244 | // First, we look for exact term search |
241 | for ($i = 0; $i < count($exactSearch) && $found; $i++) { | 245 | // Then iterate over keywords, if keyword is not found, |
242 | $found = strpos($content, $exactSearch[$i]) !== false; | ||
243 | } | ||
244 | |||
245 | // Iterate over keywords, if keyword is not found, | ||
246 | // no need to check for the others. We want all or nothing. | 246 | // no need to check for the others. We want all or nothing. |
247 | for ($i = 0; $i < count($andSearch) && $found; $i++) { | 247 | foreach ([$exactSearch, $andSearch] as $search) { |
248 | $found = strpos($content, $andSearch[$i]) !== false; | 248 | for ($i = 0; $i < count($search) && $found !== false; $i++) { |
249 | $found = mb_strpos($content, $search[$i]); | ||
250 | if ($found === false) { | ||
251 | break; | ||
252 | } | ||
253 | |||
254 | $foundPositions[] = ['start' => $found, 'end' => $found + mb_strlen($search[$i])]; | ||
255 | } | ||
249 | } | 256 | } |
250 | 257 | ||
251 | // Exclude terms. | 258 | // Exclude terms. |
252 | for ($i = 0; $i < count($excludeSearch) && $found; $i++) { | 259 | for ($i = 0; $i < count($excludeSearch) && $found !== false; $i++) { |
253 | $found = strpos($content, $excludeSearch[$i]) === false; | 260 | $found = strpos($content, $excludeSearch[$i]) === false; |
254 | } | 261 | } |
255 | 262 | ||
256 | if ($found) { | 263 | if ($found !== false) { |
264 | $link->addAdditionalContentEntry( | ||
265 | 'search_highlight', | ||
266 | $this->postProcessFoundPositions($lengths, $foundPositions) | ||
267 | ); | ||
268 | |||
257 | $filtered[$id] = $link; | 269 | $filtered[$id] = $link; |
258 | } | 270 | } |
259 | } | 271 | } |
@@ -268,7 +280,7 @@ class BookmarkFilter | |||
268 | * | 280 | * |
269 | * @return string generated regex fragment | 281 | * @return string generated regex fragment |
270 | */ | 282 | */ |
271 | private static function tag2regex($tag) | 283 | private static function tag2regex(string $tag): string |
272 | { | 284 | { |
273 | $len = strlen($tag); | 285 | $len = strlen($tag); |
274 | if (!$len || $tag === "-" || $tag === "*") { | 286 | if (!$len || $tag === "-" || $tag === "*") { |
@@ -314,13 +326,13 @@ class BookmarkFilter | |||
314 | * You can specify one or more tags, separated by space or a comma, e.g. | 326 | * You can specify one or more tags, separated by space or a comma, e.g. |
315 | * print_r($mydb->filterTags('linux programming')); | 327 | * print_r($mydb->filterTags('linux programming')); |
316 | * | 328 | * |
317 | * @param string $tags list of tags separated by commas or blank spaces. | 329 | * @param string|array $tags list of tags, separated by commas or blank spaces if passed as string. |
318 | * @param bool $casesensitive ignore case if false. | 330 | * @param bool $casesensitive ignore case if false. |
319 | * @param string $visibility Optional: return only all/private/public bookmarks. | 331 | * @param string $visibility Optional: return only all/private/public bookmarks. |
320 | * | 332 | * |
321 | * @return array filtered bookmarks. | 333 | * @return Bookmark[] filtered bookmarks. |
322 | */ | 334 | */ |
323 | public function filterTags($tags, $casesensitive = false, $visibility = 'all') | 335 | public function filterTags($tags, bool $casesensitive = false, string $visibility = 'all') |
324 | { | 336 | { |
325 | // get single tags (we may get passed an array, even though the docs say different) | 337 | // get single tags (we may get passed an array, even though the docs say different) |
326 | $inputTags = $tags; | 338 | $inputTags = $tags; |
@@ -396,9 +408,9 @@ class BookmarkFilter | |||
396 | * | 408 | * |
397 | * @param string $visibility return only all/private/public bookmarks. | 409 | * @param string $visibility return only all/private/public bookmarks. |
398 | * | 410 | * |
399 | * @return array filtered bookmarks. | 411 | * @return Bookmark[] filtered bookmarks. |
400 | */ | 412 | */ |
401 | public function filterUntagged($visibility) | 413 | public function filterUntagged(string $visibility) |
402 | { | 414 | { |
403 | $filtered = []; | 415 | $filtered = []; |
404 | foreach ($this->bookmarks as $key => $link) { | 416 | foreach ($this->bookmarks as $key => $link) { |
@@ -425,21 +437,26 @@ class BookmarkFilter | |||
425 | * print_r($mydb->filterDay('20120125')); | 437 | * print_r($mydb->filterDay('20120125')); |
426 | * | 438 | * |
427 | * @param string $day day to filter. | 439 | * @param string $day day to filter. |
428 | * | 440 | * @param string $visibility return only all/private/public bookmarks. |
429 | * @return array all link matching given day. | 441 | |
442 | * @return Bookmark[] all link matching given day. | ||
430 | * | 443 | * |
431 | * @throws Exception if date format is invalid. | 444 | * @throws Exception if date format is invalid. |
432 | */ | 445 | */ |
433 | public function filterDay($day) | 446 | public function filterDay(string $day, string $visibility) |
434 | { | 447 | { |
435 | if (!checkDateFormat('Ymd', $day)) { | 448 | if (!checkDateFormat('Ymd', $day)) { |
436 | throw new Exception('Invalid date format'); | 449 | throw new Exception('Invalid date format'); |
437 | } | 450 | } |
438 | 451 | ||
439 | $filtered = array(); | 452 | $filtered = []; |
440 | foreach ($this->bookmarks as $key => $l) { | 453 | foreach ($this->bookmarks as $key => $bookmark) { |
441 | if ($l->getCreated()->format('Ymd') == $day) { | 454 | if ($visibility === static::$PUBLIC && $bookmark->isPrivate()) { |
442 | $filtered[$key] = $l; | 455 | continue; |
456 | } | ||
457 | |||
458 | if ($bookmark->getCreated()->format('Ymd') == $day) { | ||
459 | $filtered[$key] = $bookmark; | ||
443 | } | 460 | } |
444 | } | 461 | } |
445 | 462 | ||
@@ -455,9 +472,9 @@ class BookmarkFilter | |||
455 | * @param string $tags string containing a list of tags. | 472 | * @param string $tags string containing a list of tags. |
456 | * @param bool $casesensitive will convert everything to lowercase if false. | 473 | * @param bool $casesensitive will convert everything to lowercase if false. |
457 | * | 474 | * |
458 | * @return array filtered tags string. | 475 | * @return string[] filtered tags string. |
459 | */ | 476 | */ |
460 | public static function tagsStrToArray($tags, $casesensitive) | 477 | public static function tagsStrToArray(string $tags, bool $casesensitive): array |
461 | { | 478 | { |
462 | // We use UTF-8 conversion to handle various graphemes (i.e. cyrillic, or greek) | 479 | // We use UTF-8 conversion to handle various graphemes (i.e. cyrillic, or greek) |
463 | $tagsOut = $casesensitive ? $tags : mb_convert_case($tags, MB_CASE_LOWER, 'UTF-8'); | 480 | $tagsOut = $casesensitive ? $tags : mb_convert_case($tags, MB_CASE_LOWER, 'UTF-8'); |
@@ -465,4 +482,74 @@ class BookmarkFilter | |||
465 | 482 | ||
466 | return preg_split('/\s+/', $tagsOut, -1, PREG_SPLIT_NO_EMPTY); | 483 | return preg_split('/\s+/', $tagsOut, -1, PREG_SPLIT_NO_EMPTY); |
467 | } | 484 | } |
485 | |||
486 | /** | ||
487 | * This method finalize the content of the foundPositions array, | ||
488 | * by associated all search results to their associated bookmark field, | ||
489 | * making sure that there is no overlapping results, etc. | ||
490 | * | ||
491 | * @param array $fieldLengths Start and end positions of every bookmark fields in the aggregated bookmark content. | ||
492 | * @param array $foundPositions Positions where the search results were found in the aggregated content. | ||
493 | * | ||
494 | * @return array Updated $foundPositions, by bookmark field. | ||
495 | */ | ||
496 | protected function postProcessFoundPositions(array $fieldLengths, array $foundPositions): array | ||
497 | { | ||
498 | // Sort results by starting position ASC. | ||
499 | usort($foundPositions, function (array $entryA, array $entryB): int { | ||
500 | return $entryA['start'] > $entryB['start'] ? 1 : -1; | ||
501 | }); | ||
502 | |||
503 | $out = []; | ||
504 | $currentMax = -1; | ||
505 | foreach ($foundPositions as $foundPosition) { | ||
506 | // we do not allow overlapping highlights | ||
507 | if ($foundPosition['start'] < $currentMax) { | ||
508 | continue; | ||
509 | } | ||
510 | |||
511 | $currentMax = $foundPosition['end']; | ||
512 | foreach ($fieldLengths as $part => $length) { | ||
513 | if ($foundPosition['start'] < $length['start'] || $foundPosition['start'] > $length['end']) { | ||
514 | continue; | ||
515 | } | ||
516 | |||
517 | $out[$part][] = [ | ||
518 | 'start' => $foundPosition['start'] - $length['start'], | ||
519 | 'end' => $foundPosition['end'] - $length['start'], | ||
520 | ]; | ||
521 | break; | ||
522 | } | ||
523 | } | ||
524 | |||
525 | return $out; | ||
526 | } | ||
527 | |||
528 | /** | ||
529 | * Concatenate link fields to search across fields. Adds a '\' separator for exact search terms. | ||
530 | * Also populate $length array with starting and ending positions of every bookmark field | ||
531 | * inside concatenated content. | ||
532 | * | ||
533 | * @param Bookmark $link | ||
534 | * @param array $lengths (by reference) | ||
535 | * | ||
536 | * @return string Lowercase concatenated fields content. | ||
537 | */ | ||
538 | protected function buildFullTextSearchableLink(Bookmark $link, array &$lengths): string | ||
539 | { | ||
540 | $content = mb_convert_case($link->getTitle(), MB_CASE_LOWER, 'UTF-8') .'\\'; | ||
541 | $content .= mb_convert_case($link->getDescription(), MB_CASE_LOWER, 'UTF-8') .'\\'; | ||
542 | $content .= mb_convert_case($link->getUrl(), MB_CASE_LOWER, 'UTF-8') .'\\'; | ||
543 | $content .= mb_convert_case($link->getTagsString(), MB_CASE_LOWER, 'UTF-8') .'\\'; | ||
544 | |||
545 | $lengths['title'] = ['start' => 0, 'end' => mb_strlen($link->getTitle())]; | ||
546 | $nextField = $lengths['title']['end'] + 1; | ||
547 | $lengths['description'] = ['start' => $nextField, 'end' => $nextField + mb_strlen($link->getDescription())]; | ||
548 | $nextField = $lengths['description']['end'] + 1; | ||
549 | $lengths['url'] = ['start' => $nextField, 'end' => $nextField + mb_strlen($link->getUrl())]; | ||
550 | $nextField = $lengths['url']['end'] + 1; | ||
551 | $lengths['tags'] = ['start' => $nextField, 'end' => $nextField + mb_strlen($link->getTagsString())]; | ||
552 | |||
553 | return $content; | ||
554 | } | ||
468 | } | 555 | } |
diff --git a/application/bookmark/BookmarkIO.php b/application/bookmark/BookmarkIO.php index ae9ffcb4..f40fa476 100644 --- a/application/bookmark/BookmarkIO.php +++ b/application/bookmark/BookmarkIO.php | |||
@@ -1,7 +1,12 @@ | |||
1 | <?php | 1 | <?php |
2 | 2 | ||
3 | declare(strict_types=1); | ||
4 | |||
3 | namespace Shaarli\Bookmark; | 5 | namespace Shaarli\Bookmark; |
4 | 6 | ||
7 | use malkusch\lock\mutex\Mutex; | ||
8 | use malkusch\lock\mutex\NoMutex; | ||
9 | use Shaarli\Bookmark\Exception\DatastoreNotInitializedException; | ||
5 | use Shaarli\Bookmark\Exception\EmptyDataStoreException; | 10 | use Shaarli\Bookmark\Exception\EmptyDataStoreException; |
6 | use Shaarli\Bookmark\Exception\NotWritableDataStoreException; | 11 | use Shaarli\Bookmark\Exception\NotWritableDataStoreException; |
7 | use Shaarli\Config\ConfigManager; | 12 | use Shaarli\Config\ConfigManager; |
@@ -26,11 +31,14 @@ class BookmarkIO | |||
26 | */ | 31 | */ |
27 | protected $conf; | 32 | protected $conf; |
28 | 33 | ||
34 | |||
35 | /** @var Mutex */ | ||
36 | protected $mutex; | ||
37 | |||
29 | /** | 38 | /** |
30 | * string Datastore PHP prefix | 39 | * string Datastore PHP prefix |
31 | */ | 40 | */ |
32 | protected static $phpPrefix = '<?php /* '; | 41 | protected static $phpPrefix = '<?php /* '; |
33 | |||
34 | /** | 42 | /** |
35 | * string Datastore PHP suffix | 43 | * string Datastore PHP suffix |
36 | */ | 44 | */ |
@@ -41,35 +49,46 @@ class BookmarkIO | |||
41 | * | 49 | * |
42 | * @param ConfigManager $conf instance | 50 | * @param ConfigManager $conf instance |
43 | */ | 51 | */ |
44 | public function __construct($conf) | 52 | public function __construct(ConfigManager $conf, Mutex $mutex = null) |
45 | { | 53 | { |
54 | if ($mutex === null) { | ||
55 | // This should only happen with legacy classes | ||
56 | $mutex = new NoMutex(); | ||
57 | } | ||
46 | $this->conf = $conf; | 58 | $this->conf = $conf; |
47 | $this->datastore = $conf->get('resource.datastore'); | 59 | $this->datastore = $conf->get('resource.datastore'); |
60 | $this->mutex = $mutex; | ||
48 | } | 61 | } |
49 | 62 | ||
50 | /** | 63 | /** |
51 | * Reads database from disk to memory | 64 | * Reads database from disk to memory |
52 | * | 65 | * |
53 | * @return BookmarkArray instance | 66 | * @return Bookmark[] |
54 | * | 67 | * |
55 | * @throws NotWritableDataStoreException Data couldn't be loaded | 68 | * @throws NotWritableDataStoreException Data couldn't be loaded |
56 | * @throws EmptyDataStoreException Datastore doesn't exist | 69 | * @throws EmptyDataStoreException Datastore file exists but does not contain any bookmark |
70 | * @throws DatastoreNotInitializedException File does not exists | ||
57 | */ | 71 | */ |
58 | public function read() | 72 | public function read() |
59 | { | 73 | { |
60 | if (! file_exists($this->datastore)) { | 74 | if (! file_exists($this->datastore)) { |
61 | throw new EmptyDataStoreException(); | 75 | throw new DatastoreNotInitializedException(); |
62 | } | 76 | } |
63 | 77 | ||
64 | if (!is_writable($this->datastore)) { | 78 | if (!is_writable($this->datastore)) { |
65 | throw new NotWritableDataStoreException($this->datastore); | 79 | throw new NotWritableDataStoreException($this->datastore); |
66 | } | 80 | } |
67 | 81 | ||
82 | $content = null; | ||
83 | $this->mutex->synchronized(function () use (&$content) { | ||
84 | $content = file_get_contents($this->datastore); | ||
85 | }); | ||
86 | |||
68 | // Note that gzinflate is faster than gzuncompress. | 87 | // Note that gzinflate is faster than gzuncompress. |
69 | // See: http://www.php.net/manual/en/function.gzdeflate.php#96439 | 88 | // See: http://www.php.net/manual/en/function.gzdeflate.php#96439 |
70 | $links = unserialize(gzinflate(base64_decode( | 89 | $links = unserialize(gzinflate(base64_decode( |
71 | substr(file_get_contents($this->datastore), | 90 | substr($content, strlen(self::$phpPrefix), -strlen(self::$phpSuffix)) |
72 | strlen(self::$phpPrefix), -strlen(self::$phpSuffix))))); | 91 | ))); |
73 | 92 | ||
74 | if (empty($links)) { | 93 | if (empty($links)) { |
75 | if (filesize($this->datastore) > 100) { | 94 | if (filesize($this->datastore) > 100) { |
@@ -84,7 +103,7 @@ class BookmarkIO | |||
84 | /** | 103 | /** |
85 | * Saves the database from memory to disk | 104 | * Saves the database from memory to disk |
86 | * | 105 | * |
87 | * @param BookmarkArray $links instance. | 106 | * @param Bookmark[] $links |
88 | * | 107 | * |
89 | * @throws NotWritableDataStoreException the datastore is not writable | 108 | * @throws NotWritableDataStoreException the datastore is not writable |
90 | */ | 109 | */ |
@@ -98,11 +117,13 @@ class BookmarkIO | |||
98 | throw new NotWritableDataStoreException(dirname($this->datastore)); | 117 | throw new NotWritableDataStoreException(dirname($this->datastore)); |
99 | } | 118 | } |
100 | 119 | ||
101 | file_put_contents( | 120 | $data = self::$phpPrefix.base64_encode(gzdeflate(serialize($links))).self::$phpSuffix; |
102 | $this->datastore, | ||
103 | self::$phpPrefix.base64_encode(gzdeflate(serialize($links))).self::$phpSuffix | ||
104 | ); | ||
105 | 121 | ||
106 | invalidateCaches($this->conf->get('resource.page_cache')); | 122 | $this->mutex->synchronized(function () use ($data) { |
123 | file_put_contents( | ||
124 | $this->datastore, | ||
125 | $data | ||
126 | ); | ||
127 | }); | ||
107 | } | 128 | } |
108 | } | 129 | } |
diff --git a/application/bookmark/BookmarkInitializer.php b/application/bookmark/BookmarkInitializer.php index 9eee9a35..04b996f3 100644 --- a/application/bookmark/BookmarkInitializer.php +++ b/application/bookmark/BookmarkInitializer.php | |||
@@ -1,13 +1,14 @@ | |||
1 | <?php | 1 | <?php |
2 | 2 | ||
3 | declare(strict_types=1); | ||
4 | |||
3 | namespace Shaarli\Bookmark; | 5 | namespace Shaarli\Bookmark; |
4 | 6 | ||
5 | /** | 7 | /** |
6 | * Class BookmarkInitializer | 8 | * Class BookmarkInitializer |
7 | * | 9 | * |
8 | * This class is used to initialized default bookmarks after a fresh install of Shaarli. | 10 | * This class is used to initialized default bookmarks after a fresh install of Shaarli. |
9 | * It is no longer call when the data store is empty, | 11 | * It should be only called if the datastore file does not exist(users might want to delete the default bookmarks). |
10 | * because user might want to delete default bookmarks after the install. | ||
11 | * | 12 | * |
12 | * To prevent data corruption, it does not overwrite existing bookmarks, | 13 | * To prevent data corruption, it does not overwrite existing bookmarks, |
13 | * even though there should not be any. | 14 | * even though there should not be any. |
@@ -24,7 +25,7 @@ class BookmarkInitializer | |||
24 | * | 25 | * |
25 | * @param BookmarkServiceInterface $bookmarkService | 26 | * @param BookmarkServiceInterface $bookmarkService |
26 | */ | 27 | */ |
27 | public function __construct($bookmarkService) | 28 | public function __construct(BookmarkServiceInterface $bookmarkService) |
28 | { | 29 | { |
29 | $this->bookmarkService = $bookmarkService; | 30 | $this->bookmarkService = $bookmarkService; |
30 | } | 31 | } |
@@ -32,28 +33,80 @@ class BookmarkInitializer | |||
32 | /** | 33 | /** |
33 | * Initialize the data store with default bookmarks | 34 | * Initialize the data store with default bookmarks |
34 | */ | 35 | */ |
35 | public function initialize() | 36 | public function initialize(): void |
36 | { | 37 | { |
37 | $bookmark = new Bookmark(); | 38 | $bookmark = new Bookmark(); |
38 | $bookmark->setTitle(t('My secret stuff... - Pastebin.com')); | 39 | $bookmark->setTitle('quicksilver (loop) on Vimeo ' . t('(private bookmark with thumbnail demo)')); |
39 | $bookmark->setUrl('http://sebsauvage.net/paste/?8434b27936c09649#bR7XsXhoTiLcqCpQbmOpBi3rq2zzQUC5hBI7ZT1O3x8=', []); | 40 | $bookmark->setUrl('https://vimeo.com/153493904'); |
40 | $bookmark->setDescription(t('Shhhh! I\'m a private link only YOU can see. You can delete me too.')); | 41 | $bookmark->setDescription(t( |
41 | $bookmark->setTagsString('secretstuff'); | 42 | 'Shaarli will automatically pick up the thumbnail for links to a variety of websites. |
43 | |||
44 | Explore your new Shaarli instance by trying out controls and menus. | ||
45 | Visit the project on [Github](https://github.com/shaarli/Shaarli) or [the documentation](https://shaarli.readthedocs.io/en/master/) to learn more about Shaarli. | ||
46 | |||
47 | Now you can edit or delete the default shaares. | ||
48 | ' | ||
49 | )); | ||
50 | $bookmark->setTagsString('shaarli help thumbnail'); | ||
51 | $bookmark->setPrivate(true); | ||
52 | $this->bookmarkService->add($bookmark, false); | ||
53 | |||
54 | $bookmark = new Bookmark(); | ||
55 | $bookmark->setTitle(t('Note: Shaare descriptions')); | ||
56 | $bookmark->setDescription(t( | ||
57 | 'Adding a shaare without entering a URL creates a text-only "note" post such as this one. | ||
58 | This note is private, so you are the only one able to see it while logged in. | ||
59 | |||
60 | You can use this to keep notes, post articles, code snippets, and much more. | ||
61 | |||
62 | The Markdown formatting setting allows you to format your notes and bookmark description: | ||
63 | |||
64 | ### Title headings | ||
65 | |||
66 | #### Multiple headings levels | ||
67 | * bullet lists | ||
68 | * _italic_ text | ||
69 | * **bold** text | ||
70 | * ~~strike through~~ text | ||
71 | * `code` blocks | ||
72 | * images | ||
73 | * [links](https://en.wikipedia.org/wiki/Markdown) | ||
74 | |||
75 | Markdown also supports tables: | ||
76 | |||
77 | | Name | Type | Color | Qty | | ||
78 | | ------- | --------- | ------ | ----- | | ||
79 | | Orange | Fruit | Orange | 126 | | ||
80 | | Apple | Fruit | Any | 62 | | ||
81 | | Lemon | Fruit | Yellow | 30 | | ||
82 | | Carrot | Vegetable | Red | 14 | | ||
83 | ' | ||
84 | )); | ||
85 | $bookmark->setTagsString('shaarli help'); | ||
42 | $bookmark->setPrivate(true); | 86 | $bookmark->setPrivate(true); |
43 | $this->bookmarkService->add($bookmark); | 87 | $this->bookmarkService->add($bookmark, false); |
44 | 88 | ||
45 | $bookmark = new Bookmark(); | 89 | $bookmark = new Bookmark(); |
46 | $bookmark->setTitle(t('The personal, minimalist, super-fast, database free, bookmarking service')); | 90 | $bookmark->setTitle( |
47 | $bookmark->setUrl('https://shaarli.readthedocs.io', []); | 91 | 'Shaarli - ' . t('The personal, minimalist, super-fast, database free, bookmarking service') |
92 | ); | ||
48 | $bookmark->setDescription(t( | 93 | $bookmark->setDescription(t( |
49 | 'Welcome to Shaarli! This is your first public bookmark. ' | 94 | 'Welcome to Shaarli! |
50 | . 'To edit or delete me, you must first login. | 95 | |
96 | Shaarli allows you to bookmark your favorite pages, and share them with others or store them privately. | ||
97 | You can add a description to your bookmarks, such as this one, and tag them. | ||
98 | |||
99 | Create a new shaare by clicking the `+Shaare` button, or using any of the recommended tools (browser extension, mobile app, bookmarklet, REST API, etc.). | ||
51 | 100 | ||
52 | To learn how to use Shaarli, consult the link "Documentation" at the bottom of this page. | 101 | You can easily retrieve your links, even with thousands of them, using the internal search engine, or search through tags (e.g. this Shaare is tagged with `shaarli` and `help`). |
102 | Hashtags such as #shaarli #help are also supported. | ||
103 | You can also filter the available [RSS feed](/feed/atom) and picture wall by tag or plaintext search. | ||
53 | 104 | ||
54 | You use the community supported version of the original Shaarli project, by Sebastien Sauvage.' | 105 | We hope that you will enjoy using Shaarli, maintained with ❤️ by the community! |
106 | Feel free to open [an issue](https://github.com/shaarli/Shaarli/issues) if you have a suggestion or encounter an issue. | ||
107 | ' | ||
55 | )); | 108 | )); |
56 | $bookmark->setTagsString('opensource software'); | 109 | $bookmark->setTagsString('shaarli help'); |
57 | $this->bookmarkService->add($bookmark); | 110 | $this->bookmarkService->add($bookmark, false); |
58 | } | 111 | } |
59 | } | 112 | } |
diff --git a/application/bookmark/BookmarkServiceInterface.php b/application/bookmark/BookmarkServiceInterface.php index 7b7a4f09..08cdbb4e 100644 --- a/application/bookmark/BookmarkServiceInterface.php +++ b/application/bookmark/BookmarkServiceInterface.php | |||
@@ -1,73 +1,73 @@ | |||
1 | <?php | 1 | <?php |
2 | 2 | ||
3 | namespace Shaarli\Bookmark; | 3 | declare(strict_types=1); |
4 | 4 | ||
5 | namespace Shaarli\Bookmark; | ||
5 | 6 | ||
6 | use Shaarli\Bookmark\Exception\BookmarkNotFoundException; | 7 | use Shaarli\Bookmark\Exception\BookmarkNotFoundException; |
7 | use Shaarli\Bookmark\Exception\NotWritableDataStoreException; | 8 | use Shaarli\Bookmark\Exception\NotWritableDataStoreException; |
8 | use Shaarli\Config\ConfigManager; | ||
9 | use Shaarli\Exceptions\IOException; | ||
10 | use Shaarli\History; | ||
11 | 9 | ||
12 | /** | 10 | /** |
13 | * Class BookmarksService | 11 | * Class BookmarksService |
14 | * | 12 | * |
15 | * This is the entry point to manipulate the bookmark DB. | 13 | * This is the entry point to manipulate the bookmark DB. |
14 | * | ||
15 | * Regarding return types of a list of bookmarks, it can either be an array or an ArrayAccess implementation, | ||
16 | * so until PHP 8.0 is the minimal supported version with union return types it cannot be explicitly added. | ||
16 | */ | 17 | */ |
17 | interface BookmarkServiceInterface | 18 | interface BookmarkServiceInterface |
18 | { | 19 | { |
19 | /** | 20 | /** |
20 | * BookmarksService constructor. | ||
21 | * | ||
22 | * @param ConfigManager $conf instance | ||
23 | * @param History $history instance | ||
24 | * @param bool $isLoggedIn true if the current user is logged in | ||
25 | */ | ||
26 | public function __construct(ConfigManager $conf, History $history, $isLoggedIn); | ||
27 | |||
28 | /** | ||
29 | * Find a bookmark by hash | 21 | * Find a bookmark by hash |
30 | * | 22 | * |
31 | * @param string $hash | 23 | * @param string $hash Bookmark's hash |
24 | * @param string|null $privateKey Optional key used to access private links while logged out | ||
32 | * | 25 | * |
33 | * @return mixed | 26 | * @return Bookmark |
34 | * | 27 | * |
35 | * @throws \Exception | 28 | * @throws \Exception |
36 | */ | 29 | */ |
37 | public function findByHash($hash); | 30 | public function findByHash(string $hash, string $privateKey = null); |
38 | 31 | ||
39 | /** | 32 | /** |
40 | * @param $url | 33 | * @param $url |
41 | * | 34 | * |
42 | * @return Bookmark|null | 35 | * @return Bookmark|null |
43 | */ | 36 | */ |
44 | public function findByUrl($url); | 37 | public function findByUrl(string $url): ?Bookmark; |
45 | 38 | ||
46 | /** | 39 | /** |
47 | * Search bookmarks | 40 | * Search bookmarks |
48 | * | 41 | * |
49 | * @param mixed $request | 42 | * @param array $request |
50 | * @param string $visibility | 43 | * @param ?string $visibility |
51 | * @param bool $caseSensitive | 44 | * @param bool $caseSensitive |
52 | * @param bool $untaggedOnly | 45 | * @param bool $untaggedOnly |
46 | * @param bool $ignoreSticky | ||
53 | * | 47 | * |
54 | * @return Bookmark[] | 48 | * @return Bookmark[] |
55 | */ | 49 | */ |
56 | public function search($request = [], $visibility = null, $caseSensitive = false, $untaggedOnly = false); | 50 | public function search( |
51 | array $request = [], | ||
52 | string $visibility = null, | ||
53 | bool $caseSensitive = false, | ||
54 | bool $untaggedOnly = false, | ||
55 | bool $ignoreSticky = false | ||
56 | ); | ||
57 | 57 | ||
58 | /** | 58 | /** |
59 | * Get a single bookmark by its ID. | 59 | * Get a single bookmark by its ID. |
60 | * | 60 | * |
61 | * @param int $id Bookmark ID | 61 | * @param int $id Bookmark ID |
62 | * @param string $visibility all|public|private e.g. with public, accessing a private bookmark will throw an | 62 | * @param ?string $visibility all|public|private e.g. with public, accessing a private bookmark will throw an |
63 | * exception | 63 | * exception |
64 | * | 64 | * |
65 | * @return Bookmark | 65 | * @return Bookmark |
66 | * | 66 | * |
67 | * @throws BookmarkNotFoundException | 67 | * @throws BookmarkNotFoundException |
68 | * @throws \Exception | 68 | * @throws \Exception |
69 | */ | 69 | */ |
70 | public function get($id, $visibility = null); | 70 | public function get(int $id, string $visibility = null); |
71 | 71 | ||
72 | /** | 72 | /** |
73 | * Updates an existing bookmark (depending on its ID). | 73 | * Updates an existing bookmark (depending on its ID). |
@@ -80,7 +80,7 @@ interface BookmarkServiceInterface | |||
80 | * @throws BookmarkNotFoundException | 80 | * @throws BookmarkNotFoundException |
81 | * @throws \Exception | 81 | * @throws \Exception |
82 | */ | 82 | */ |
83 | public function set($bookmark, $save = true); | 83 | public function set(Bookmark $bookmark, bool $save = true): Bookmark; |
84 | 84 | ||
85 | /** | 85 | /** |
86 | * Adds a new bookmark (the ID must be empty). | 86 | * Adds a new bookmark (the ID must be empty). |
@@ -92,7 +92,7 @@ interface BookmarkServiceInterface | |||
92 | * | 92 | * |
93 | * @throws \Exception | 93 | * @throws \Exception |
94 | */ | 94 | */ |
95 | public function add($bookmark, $save = true); | 95 | public function add(Bookmark $bookmark, bool $save = true): Bookmark; |
96 | 96 | ||
97 | /** | 97 | /** |
98 | * Adds or updates a bookmark depending on its ID: | 98 | * Adds or updates a bookmark depending on its ID: |
@@ -106,7 +106,7 @@ interface BookmarkServiceInterface | |||
106 | * | 106 | * |
107 | * @throws \Exception | 107 | * @throws \Exception |
108 | */ | 108 | */ |
109 | public function addOrSet($bookmark, $save = true); | 109 | public function addOrSet(Bookmark $bookmark, bool $save = true): Bookmark; |
110 | 110 | ||
111 | /** | 111 | /** |
112 | * Deletes a bookmark. | 112 | * Deletes a bookmark. |
@@ -116,65 +116,72 @@ interface BookmarkServiceInterface | |||
116 | * | 116 | * |
117 | * @throws \Exception | 117 | * @throws \Exception |
118 | */ | 118 | */ |
119 | public function remove($bookmark, $save = true); | 119 | public function remove(Bookmark $bookmark, bool $save = true): void; |
120 | 120 | ||
121 | /** | 121 | /** |
122 | * Get a single bookmark by its ID. | 122 | * Get a single bookmark by its ID. |
123 | * | 123 | * |
124 | * @param int $id Bookmark ID | 124 | * @param int $id Bookmark ID |
125 | * @param string $visibility all|public|private e.g. with public, accessing a private bookmark will throw an | 125 | * @param ?string $visibility all|public|private e.g. with public, accessing a private bookmark will throw an |
126 | * exception | 126 | * exception |
127 | * | 127 | * |
128 | * @return bool | 128 | * @return bool |
129 | */ | 129 | */ |
130 | public function exists($id, $visibility = null); | 130 | public function exists(int $id, string $visibility = null): bool; |
131 | 131 | ||
132 | /** | 132 | /** |
133 | * Return the number of available bookmarks for given visibility. | 133 | * Return the number of available bookmarks for given visibility. |
134 | * | 134 | * |
135 | * @param string $visibility public|private|all | 135 | * @param ?string $visibility public|private|all |
136 | * | 136 | * |
137 | * @return int Number of bookmarks | 137 | * @return int Number of bookmarks |
138 | */ | 138 | */ |
139 | public function count($visibility = null); | 139 | public function count(string $visibility = null): int; |
140 | 140 | ||
141 | /** | 141 | /** |
142 | * Write the datastore. | 142 | * Write the datastore. |
143 | * | 143 | * |
144 | * @throws NotWritableDataStoreException | 144 | * @throws NotWritableDataStoreException |
145 | */ | 145 | */ |
146 | public function save(); | 146 | public function save(): void; |
147 | 147 | ||
148 | /** | 148 | /** |
149 | * Returns the list tags appearing in the bookmarks with the given tags | 149 | * Returns the list tags appearing in the bookmarks with the given tags |
150 | * | 150 | * |
151 | * @param array $filteringTags tags selecting the bookmarks to consider | 151 | * @param array|null $filteringTags tags selecting the bookmarks to consider |
152 | * @param string $visibility process only all/private/public bookmarks | 152 | * @param string|null $visibility process only all/private/public bookmarks |
153 | * | 153 | * |
154 | * @return array tag => bookmarksCount | 154 | * @return array tag => bookmarksCount |
155 | */ | 155 | */ |
156 | public function bookmarksCountPerTag($filteringTags = [], $visibility = 'all'); | 156 | public function bookmarksCountPerTag(array $filteringTags = [], ?string $visibility = null): array; |
157 | 157 | ||
158 | /** | 158 | /** |
159 | * Returns the list of days containing articles (oldest first) | 159 | * Return a list of bookmark matching provided period of time. |
160 | * It also update directly previous and next date outside of given period found in the datastore. | ||
161 | * | ||
162 | * @param \DateTimeInterface $from Starting date. | ||
163 | * @param \DateTimeInterface $to Ending date. | ||
164 | * @param \DateTimeInterface|null $previous (by reference) updated with first created date found before $from. | ||
165 | * @param \DateTimeInterface|null $next (by reference) updated with first created date found after $to. | ||
160 | * | 166 | * |
161 | * @return array containing days (in format YYYYMMDD). | 167 | * @return array List of bookmarks matching provided period of time. |
162 | */ | 168 | */ |
163 | public function days(); | 169 | public function findByDate( |
170 | \DateTimeInterface $from, | ||
171 | \DateTimeInterface $to, | ||
172 | ?\DateTimeInterface &$previous, | ||
173 | ?\DateTimeInterface &$next | ||
174 | ): array; | ||
164 | 175 | ||
165 | /** | 176 | /** |
166 | * Returns the list of articles for a given day. | 177 | * Returns the latest bookmark by creation date. |
167 | * | 178 | * |
168 | * @param string $request day to filter. Format: YYYYMMDD. | 179 | * @return Bookmark|null Found Bookmark or null if the datastore is empty. |
169 | * | ||
170 | * @return Bookmark[] list of shaare found. | ||
171 | * | ||
172 | * @throws BookmarkNotFoundException | ||
173 | */ | 180 | */ |
174 | public function filterDay($request); | 181 | public function getLatest(): ?Bookmark; |
175 | 182 | ||
176 | /** | 183 | /** |
177 | * Creates the default database after a fresh install. | 184 | * Creates the default database after a fresh install. |
178 | */ | 185 | */ |
179 | public function initialize(); | 186 | public function initialize(): void; |
180 | } | 187 | } |
diff --git a/application/bookmark/LinkUtils.php b/application/bookmark/LinkUtils.php index 88379430..faf5dbfd 100644 --- a/application/bookmark/LinkUtils.php +++ b/application/bookmark/LinkUtils.php | |||
@@ -3,112 +3,6 @@ | |||
3 | use Shaarli\Bookmark\Bookmark; | 3 | use Shaarli\Bookmark\Bookmark; |
4 | 4 | ||
5 | /** | 5 | /** |
6 | * Get cURL callback function for CURLOPT_WRITEFUNCTION | ||
7 | * | ||
8 | * @param string $charset to extract from the downloaded page (reference) | ||
9 | * @param string $title to extract from the downloaded page (reference) | ||
10 | * @param string $description to extract from the downloaded page (reference) | ||
11 | * @param string $keywords to extract from the downloaded page (reference) | ||
12 | * @param bool $retrieveDescription Automatically tries to retrieve description and keywords from HTML content | ||
13 | * @param string $curlGetInfo Optionally overrides curl_getinfo function | ||
14 | * | ||
15 | * @return Closure | ||
16 | */ | ||
17 | function get_curl_download_callback( | ||
18 | &$charset, | ||
19 | &$title, | ||
20 | &$description, | ||
21 | &$keywords, | ||
22 | $retrieveDescription, | ||
23 | $curlGetInfo = 'curl_getinfo' | ||
24 | ) { | ||
25 | $isRedirected = false; | ||
26 | $currentChunk = 0; | ||
27 | $foundChunk = null; | ||
28 | |||
29 | /** | ||
30 | * cURL callback function for CURLOPT_WRITEFUNCTION (called during the download). | ||
31 | * | ||
32 | * While downloading the remote page, we check that the HTTP code is 200 and content type is 'html/text' | ||
33 | * Then we extract the title and the charset and stop the download when it's done. | ||
34 | * | ||
35 | * @param resource $ch cURL resource | ||
36 | * @param string $data chunk of data being downloaded | ||
37 | * | ||
38 | * @return int|bool length of $data or false if we need to stop the download | ||
39 | */ | ||
40 | return function (&$ch, $data) use ( | ||
41 | $retrieveDescription, | ||
42 | $curlGetInfo, | ||
43 | &$charset, | ||
44 | &$title, | ||
45 | &$description, | ||
46 | &$keywords, | ||
47 | &$isRedirected, | ||
48 | &$currentChunk, | ||
49 | &$foundChunk | ||
50 | ) { | ||
51 | $currentChunk++; | ||
52 | $responseCode = $curlGetInfo($ch, CURLINFO_RESPONSE_CODE); | ||
53 | if (!empty($responseCode) && in_array($responseCode, [301, 302])) { | ||
54 | $isRedirected = true; | ||
55 | return strlen($data); | ||
56 | } | ||
57 | if (!empty($responseCode) && $responseCode !== 200) { | ||
58 | return false; | ||
59 | } | ||
60 | // After a redirection, the content type will keep the previous request value | ||
61 | // until it finds the next content-type header. | ||
62 | if (! $isRedirected || strpos(strtolower($data), 'content-type') !== false) { | ||
63 | $contentType = $curlGetInfo($ch, CURLINFO_CONTENT_TYPE); | ||
64 | } | ||
65 | if (!empty($contentType) && strpos($contentType, 'text/html') === false) { | ||
66 | return false; | ||
67 | } | ||
68 | if (!empty($contentType) && empty($charset)) { | ||
69 | $charset = header_extract_charset($contentType); | ||
70 | } | ||
71 | if (empty($charset)) { | ||
72 | $charset = html_extract_charset($data); | ||
73 | } | ||
74 | if (empty($title)) { | ||
75 | $title = html_extract_title($data); | ||
76 | $foundChunk = ! empty($title) ? $currentChunk : $foundChunk; | ||
77 | } | ||
78 | if ($retrieveDescription && empty($description)) { | ||
79 | $description = html_extract_tag('description', $data); | ||
80 | $foundChunk = ! empty($description) ? $currentChunk : $foundChunk; | ||
81 | } | ||
82 | if ($retrieveDescription && empty($keywords)) { | ||
83 | $keywords = html_extract_tag('keywords', $data); | ||
84 | if (! empty($keywords)) { | ||
85 | $foundChunk = $currentChunk; | ||
86 | // Keywords use the format tag1, tag2 multiple words, tag | ||
87 | // So we format them to match Shaarli's separator and glue multiple words with '-' | ||
88 | $keywords = implode(' ', array_map(function($keyword) { | ||
89 | return implode('-', preg_split('/\s+/', trim($keyword))); | ||
90 | }, explode(',', $keywords))); | ||
91 | } | ||
92 | } | ||
93 | |||
94 | // We got everything we want, stop the download. | ||
95 | // If we already found either the title, description or keywords, | ||
96 | // it's highly unlikely that we'll found the other metas further than | ||
97 | // in the same chunk of data or the next one. So we also stop the download after that. | ||
98 | if ((!empty($responseCode) && !empty($contentType) && !empty($charset)) && $foundChunk !== null | ||
99 | && (! $retrieveDescription | ||
100 | || $foundChunk < $currentChunk | ||
101 | || (!empty($title) && !empty($description) && !empty($keywords)) | ||
102 | ) | ||
103 | ) { | ||
104 | return false; | ||
105 | } | ||
106 | |||
107 | return strlen($data); | ||
108 | }; | ||
109 | } | ||
110 | |||
111 | /** | ||
112 | * Extract title from an HTML document. | 6 | * Extract title from an HTML document. |
113 | * | 7 | * |
114 | * @param string $html HTML content where to look for a title. | 8 | * @param string $html HTML content where to look for a title. |
@@ -132,7 +26,7 @@ function html_extract_title($html) | |||
132 | */ | 26 | */ |
133 | function header_extract_charset($header) | 27 | function header_extract_charset($header) |
134 | { | 28 | { |
135 | preg_match('/charset="?([^; ]+)/i', $header, $match); | 29 | preg_match('/charset=["\']?([^; "\']+)/i', $header, $match); |
136 | if (! empty($match[1])) { | 30 | if (! empty($match[1])) { |
137 | return strtolower(trim($match[1])); | 31 | return strtolower(trim($match[1])); |
138 | } | 32 | } |
@@ -172,11 +66,13 @@ function html_extract_tag($tag, $html) | |||
172 | { | 66 | { |
173 | $propertiesKey = ['property', 'name', 'itemprop']; | 67 | $propertiesKey = ['property', 'name', 'itemprop']; |
174 | $properties = implode('|', $propertiesKey); | 68 | $properties = implode('|', $propertiesKey); |
69 | // We need a OR here to accept either 'property=og:noquote' or 'property="og:unrelated og:my-tag"' | ||
70 | $orCondition = '["\']?(?:og:)?'. $tag .'["\']?|["\'][^\'"]*?(?:og:)?' . $tag . '[^\'"]*?[\'"]'; | ||
175 | // Try to retrieve OpenGraph image. | 71 | // Try to retrieve OpenGraph image. |
176 | $ogRegex = '#<meta[^>]+(?:'. $properties .')=["\']?(?:og:)?'. $tag .'["\'\s][^>]*content=["\']?(.*?)["\'/>]#'; | 72 | $ogRegex = '#<meta[^>]+(?:'. $properties .')=(?:'. $orCondition .')[^>]*content=["\'](.*?)["\'].*?>#'; |
177 | // If the attributes are not in the order property => content (e.g. Github) | 73 | // If the attributes are not in the order property => content (e.g. Github) |
178 | // New regex to keep this readable... more or less. | 74 | // New regex to keep this readable... more or less. |
179 | $ogRegexReverse = '#<meta[^>]+content=["\']([^"\']+)[^>]+(?:'. $properties .')=["\']?(?:og)?:'. $tag .'["\'\s/>]#'; | 75 | $ogRegexReverse = '#<meta[^>]+content=["\'](.*?)["\'][^>]+(?:'. $properties .')=(?:'. $orCondition .').*?>#'; |
180 | 76 | ||
181 | if (preg_match($ogRegex, $html, $matches) > 0 | 77 | if (preg_match($ogRegex, $html, $matches) > 0 |
182 | || preg_match($ogRegexReverse, $html, $matches) > 0 | 78 | || preg_match($ogRegexReverse, $html, $matches) > 0 |
@@ -220,7 +116,7 @@ function hashtag_autolink($description, $indexUrl = '') | |||
220 | * \p{Mn} - any non marking space (accents, umlauts, etc) | 116 | * \p{Mn} - any non marking space (accents, umlauts, etc) |
221 | */ | 117 | */ |
222 | $regex = '/(^|\s)#([\p{Pc}\p{N}\p{L}\p{Mn}]+)/mui'; | 118 | $regex = '/(^|\s)#([\p{Pc}\p{N}\p{L}\p{Mn}]+)/mui'; |
223 | $replacement = '$1<a href="'. $indexUrl .'?addtag=$2" title="Hashtag $2">#$2</a>'; | 119 | $replacement = '$1<a href="'. $indexUrl .'./add-tag/$2" title="Hashtag $2">#$2</a>'; |
224 | return preg_replace($regex, $replacement, $description); | 120 | return preg_replace($regex, $replacement, $description); |
225 | } | 121 | } |
226 | 122 | ||
diff --git a/application/bookmark/exception/DatastoreNotInitializedException.php b/application/bookmark/exception/DatastoreNotInitializedException.php new file mode 100644 index 00000000..f495049d --- /dev/null +++ b/application/bookmark/exception/DatastoreNotInitializedException.php | |||
@@ -0,0 +1,10 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Shaarli\Bookmark\Exception; | ||
6 | |||
7 | class DatastoreNotInitializedException extends \Exception | ||
8 | { | ||
9 | |||
10 | } | ||