]>
Commit | Line | Data |
---|---|---|
336a28fa A |
1 | <?php |
2 | ||
3 | ||
4 | namespace Shaarli\Bookmark; | |
5 | ||
6 | ||
7 | use Exception; | |
fd1ddad9 | 8 | use malkusch\lock\mutex\Mutex; |
336a28fa | 9 | use Shaarli\Bookmark\Exception\BookmarkNotFoundException; |
d6e5f04d | 10 | use Shaarli\Bookmark\Exception\DatastoreNotInitializedException; |
336a28fa A |
11 | use Shaarli\Bookmark\Exception\EmptyDataStoreException; |
12 | use Shaarli\Config\ConfigManager; | |
a39acb25 | 13 | use Shaarli\Formatter\BookmarkMarkdownFormatter; |
336a28fa A |
14 | use Shaarli\History; |
15 | use Shaarli\Legacy\LegacyLinkDB; | |
16 | use Shaarli\Legacy\LegacyUpdater; | |
b0428aa9 | 17 | use Shaarli\Render\PageCacheManager; |
336a28fa A |
18 | use Shaarli\Updater\UpdaterUtils; |
19 | ||
20 | /** | |
21 | * Class BookmarksService | |
22 | * | |
23 | * This is the entry point to manipulate the bookmark DB. | |
24 | * It manipulates loads links from a file data store containing all bookmarks. | |
25 | * | |
26 | * It also triggers the legacy format (bookmarks as arrays) migration. | |
27 | */ | |
28 | class BookmarkFileService implements BookmarkServiceInterface | |
29 | { | |
30 | /** @var Bookmark[] instance */ | |
31 | protected $bookmarks; | |
32 | ||
33 | /** @var BookmarkIO instance */ | |
34 | protected $bookmarksIO; | |
35 | ||
36 | /** @var BookmarkFilter */ | |
37 | protected $bookmarkFilter; | |
38 | ||
39 | /** @var ConfigManager instance */ | |
40 | protected $conf; | |
41 | ||
42 | /** @var History instance */ | |
43 | protected $history; | |
44 | ||
b0428aa9 A |
45 | /** @var PageCacheManager instance */ |
46 | protected $pageCacheManager; | |
47 | ||
336a28fa A |
48 | /** @var bool true for logged in users. Default value to retrieve private bookmarks. */ |
49 | protected $isLoggedIn; | |
50 | ||
fd1ddad9 A |
51 | /** @var Mutex */ |
52 | protected $mutex; | |
53 | ||
336a28fa A |
54 | /** |
55 | * @inheritDoc | |
56 | */ | |
fd1ddad9 | 57 | public function __construct(ConfigManager $conf, History $history, Mutex $mutex, $isLoggedIn) |
336a28fa A |
58 | { |
59 | $this->conf = $conf; | |
60 | $this->history = $history; | |
fd1ddad9 | 61 | $this->mutex = $mutex; |
c4d5be53 | 62 | $this->pageCacheManager = new PageCacheManager($this->conf->get('resource.page_cache'), $isLoggedIn); |
fd1ddad9 | 63 | $this->bookmarksIO = new BookmarkIO($this->conf, $this->mutex); |
336a28fa A |
64 | $this->isLoggedIn = $isLoggedIn; |
65 | ||
66 | if (!$this->isLoggedIn && $this->conf->get('privacy.hide_public_links', false)) { | |
67 | $this->bookmarks = []; | |
68 | } else { | |
69 | try { | |
70 | $this->bookmarks = $this->bookmarksIO->read(); | |
d6e5f04d | 71 | } catch (EmptyDataStoreException|DatastoreNotInitializedException $e) { |
336a28fa | 72 | $this->bookmarks = new BookmarkArray(); |
d6e5f04d | 73 | |
c4ad3d4f | 74 | if ($this->isLoggedIn) { |
d6e5f04d A |
75 | // Datastore file does not exists, we initialize it with default bookmarks. |
76 | if ($e instanceof DatastoreNotInitializedException) { | |
77 | $this->initialize(); | |
78 | } else { | |
79 | $this->save(); | |
80 | } | |
336a28fa A |
81 | } |
82 | } | |
83 | ||
84 | if (! $this->bookmarks instanceof BookmarkArray) { | |
85 | $this->migrate(); | |
86 | exit( | |
87 | 'Your data store has been migrated, please reload the page.'. PHP_EOL . | |
88 | 'If this message keeps showing up, please delete data/updates.txt file.' | |
89 | ); | |
90 | } | |
91 | } | |
92 | ||
93 | $this->bookmarkFilter = new BookmarkFilter($this->bookmarks); | |
94 | } | |
95 | ||
96 | /** | |
97 | * @inheritDoc | |
98 | */ | |
99 | public function findByHash($hash) | |
100 | { | |
101 | $bookmark = $this->bookmarkFilter->filter(BookmarkFilter::$FILTER_HASH, $hash); | |
102 | // PHP 7.3 introduced array_key_first() to avoid this hack | |
103 | $first = reset($bookmark); | |
104 | if (! $this->isLoggedIn && $first->isPrivate()) { | |
105 | throw new Exception('Not authorized'); | |
106 | } | |
107 | ||
1a8ac737 | 108 | return $first; |
336a28fa A |
109 | } |
110 | ||
111 | /** | |
112 | * @inheritDoc | |
113 | */ | |
114 | public function findByUrl($url) | |
115 | { | |
116 | return $this->bookmarks->getByUrl($url); | |
117 | } | |
118 | ||
119 | /** | |
120 | * @inheritDoc | |
121 | */ | |
a8e210fa A |
122 | public function search( |
123 | $request = [], | |
124 | $visibility = null, | |
125 | $caseSensitive = false, | |
126 | $untaggedOnly = false, | |
127 | bool $ignoreSticky = false | |
128 | ) { | |
336a28fa A |
129 | if ($visibility === null) { |
130 | $visibility = $this->isLoggedIn ? BookmarkFilter::$ALL : BookmarkFilter::$PUBLIC; | |
131 | } | |
132 | ||
133 | // Filter bookmark database according to parameters. | |
134 | $searchtags = isset($request['searchtags']) ? $request['searchtags'] : ''; | |
135 | $searchterm = isset($request['searchterm']) ? $request['searchterm'] : ''; | |
136 | ||
a8e210fa A |
137 | if ($ignoreSticky) { |
138 | $this->bookmarks->reorder('DESC', true); | |
139 | } | |
140 | ||
336a28fa A |
141 | return $this->bookmarkFilter->filter( |
142 | BookmarkFilter::$FILTER_TAG | BookmarkFilter::$FILTER_TEXT, | |
143 | [$searchtags, $searchterm], | |
144 | $caseSensitive, | |
145 | $visibility, | |
146 | $untaggedOnly | |
147 | ); | |
148 | } | |
149 | ||
150 | /** | |
151 | * @inheritDoc | |
152 | */ | |
153 | public function get($id, $visibility = null) | |
154 | { | |
155 | if (! isset($this->bookmarks[$id])) { | |
156 | throw new BookmarkNotFoundException(); | |
157 | } | |
158 | ||
159 | if ($visibility === null) { | |
a39acb25 | 160 | $visibility = $this->isLoggedIn ? BookmarkFilter::$ALL : BookmarkFilter::$PUBLIC; |
336a28fa A |
161 | } |
162 | ||
163 | $bookmark = $this->bookmarks[$id]; | |
164 | if (($bookmark->isPrivate() && $visibility != 'all' && $visibility != 'private') | |
165 | || (! $bookmark->isPrivate() && $visibility != 'all' && $visibility != 'public') | |
166 | ) { | |
167 | throw new Exception('Unauthorized'); | |
168 | } | |
169 | ||
170 | return $bookmark; | |
171 | } | |
172 | ||
173 | /** | |
174 | * @inheritDoc | |
175 | */ | |
176 | public function set($bookmark, $save = true) | |
177 | { | |
d6e5f04d | 178 | if (true !== $this->isLoggedIn) { |
336a28fa A |
179 | throw new Exception(t('You\'re not authorized to alter the datastore')); |
180 | } | |
181 | if (! $bookmark instanceof Bookmark) { | |
182 | throw new Exception(t('Provided data is invalid')); | |
183 | } | |
184 | if (! isset($this->bookmarks[$bookmark->getId()])) { | |
185 | throw new BookmarkNotFoundException(); | |
186 | } | |
187 | $bookmark->validate(); | |
188 | ||
189 | $bookmark->setUpdated(new \DateTime()); | |
190 | $this->bookmarks[$bookmark->getId()] = $bookmark; | |
191 | if ($save === true) { | |
192 | $this->save(); | |
193 | $this->history->updateLink($bookmark); | |
194 | } | |
195 | return $this->bookmarks[$bookmark->getId()]; | |
196 | } | |
197 | ||
198 | /** | |
199 | * @inheritDoc | |
200 | */ | |
201 | public function add($bookmark, $save = true) | |
202 | { | |
d6e5f04d | 203 | if (true !== $this->isLoggedIn) { |
336a28fa A |
204 | throw new Exception(t('You\'re not authorized to alter the datastore')); |
205 | } | |
206 | if (! $bookmark instanceof Bookmark) { | |
207 | throw new Exception(t('Provided data is invalid')); | |
208 | } | |
209 | if (! empty($bookmark->getId())) { | |
210 | throw new Exception(t('This bookmarks already exists')); | |
211 | } | |
212 | $bookmark->setId($this->bookmarks->getNextId()); | |
213 | $bookmark->validate(); | |
214 | ||
215 | $this->bookmarks[$bookmark->getId()] = $bookmark; | |
216 | if ($save === true) { | |
217 | $this->save(); | |
218 | $this->history->addLink($bookmark); | |
219 | } | |
220 | return $this->bookmarks[$bookmark->getId()]; | |
221 | } | |
222 | ||
223 | /** | |
224 | * @inheritDoc | |
225 | */ | |
226 | public function addOrSet($bookmark, $save = true) | |
227 | { | |
d6e5f04d | 228 | if (true !== $this->isLoggedIn) { |
336a28fa A |
229 | throw new Exception(t('You\'re not authorized to alter the datastore')); |
230 | } | |
231 | if (! $bookmark instanceof Bookmark) { | |
232 | throw new Exception('Provided data is invalid'); | |
233 | } | |
234 | if ($bookmark->getId() === null) { | |
235 | return $this->add($bookmark, $save); | |
236 | } | |
237 | return $this->set($bookmark, $save); | |
238 | } | |
239 | ||
240 | /** | |
241 | * @inheritDoc | |
242 | */ | |
243 | public function remove($bookmark, $save = true) | |
244 | { | |
d6e5f04d | 245 | if (true !== $this->isLoggedIn) { |
336a28fa A |
246 | throw new Exception(t('You\'re not authorized to alter the datastore')); |
247 | } | |
248 | if (! $bookmark instanceof Bookmark) { | |
249 | throw new Exception(t('Provided data is invalid')); | |
250 | } | |
251 | if (! isset($this->bookmarks[$bookmark->getId()])) { | |
252 | throw new BookmarkNotFoundException(); | |
253 | } | |
254 | ||
255 | unset($this->bookmarks[$bookmark->getId()]); | |
256 | if ($save === true) { | |
257 | $this->save(); | |
258 | $this->history->deleteLink($bookmark); | |
259 | } | |
260 | } | |
261 | ||
262 | /** | |
263 | * @inheritDoc | |
264 | */ | |
265 | public function exists($id, $visibility = null) | |
266 | { | |
267 | if (! isset($this->bookmarks[$id])) { | |
268 | return false; | |
269 | } | |
270 | ||
271 | if ($visibility === null) { | |
272 | $visibility = $this->isLoggedIn ? 'all' : 'public'; | |
273 | } | |
274 | ||
275 | $bookmark = $this->bookmarks[$id]; | |
276 | if (($bookmark->isPrivate() && $visibility != 'all' && $visibility != 'private') | |
277 | || (! $bookmark->isPrivate() && $visibility != 'all' && $visibility != 'public') | |
278 | ) { | |
279 | return false; | |
280 | } | |
281 | ||
282 | return true; | |
283 | } | |
284 | ||
285 | /** | |
286 | * @inheritDoc | |
287 | */ | |
288 | public function count($visibility = null) | |
289 | { | |
290 | return count($this->search([], $visibility)); | |
291 | } | |
292 | ||
293 | /** | |
294 | * @inheritDoc | |
295 | */ | |
296 | public function save() | |
297 | { | |
d6e5f04d | 298 | if (true !== $this->isLoggedIn) { |
336a28fa A |
299 | // TODO: raise an Exception instead |
300 | die('You are not authorized to change the database.'); | |
301 | } | |
c4ad3d4f | 302 | |
336a28fa A |
303 | $this->bookmarks->reorder(); |
304 | $this->bookmarksIO->write($this->bookmarks); | |
b0428aa9 | 305 | $this->pageCacheManager->invalidateCaches(); |
336a28fa A |
306 | } |
307 | ||
308 | /** | |
309 | * @inheritDoc | |
310 | */ | |
311 | public function bookmarksCountPerTag($filteringTags = [], $visibility = null) | |
312 | { | |
313 | $bookmarks = $this->search(['searchtags' => $filteringTags], $visibility); | |
314 | $tags = []; | |
315 | $caseMapping = []; | |
316 | foreach ($bookmarks as $bookmark) { | |
317 | foreach ($bookmark->getTags() as $tag) { | |
a39acb25 A |
318 | if (empty($tag) |
319 | || (! $this->isLoggedIn && startsWith($tag, '.')) | |
320 | || $tag === BookmarkMarkdownFormatter::NO_MD_TAG | |
c79473bd | 321 | || in_array($tag, $filteringTags, true) |
a39acb25 | 322 | ) { |
336a28fa A |
323 | continue; |
324 | } | |
a39acb25 | 325 | |
336a28fa A |
326 | // The first case found will be displayed. |
327 | if (!isset($caseMapping[strtolower($tag)])) { | |
328 | $caseMapping[strtolower($tag)] = $tag; | |
329 | $tags[$caseMapping[strtolower($tag)]] = 0; | |
330 | } | |
331 | $tags[$caseMapping[strtolower($tag)]]++; | |
332 | } | |
333 | } | |
334 | ||
335 | /* | |
336 | * Formerly used arsort(), which doesn't define the sort behaviour for equal values. | |
337 | * Also, this function doesn't produce the same result between PHP 5.6 and 7. | |
338 | * | |
339 | * So we now use array_multisort() to sort tags by DESC occurrences, | |
340 | * then ASC alphabetically for equal values. | |
341 | * | |
342 | * @see https://github.com/shaarli/Shaarli/issues/1142 | |
343 | */ | |
344 | $keys = array_keys($tags); | |
345 | $tmpTags = array_combine($keys, $keys); | |
346 | array_multisort($tags, SORT_DESC, $tmpTags, SORT_ASC, $tags); | |
347 | return $tags; | |
348 | } | |
349 | ||
350 | /** | |
351 | * @inheritDoc | |
352 | */ | |
353 | public function days() | |
354 | { | |
355 | $bookmarkDays = []; | |
356 | foreach ($this->search() as $bookmark) { | |
357 | $bookmarkDays[$bookmark->getCreated()->format('Ymd')] = 0; | |
358 | } | |
359 | $bookmarkDays = array_keys($bookmarkDays); | |
360 | sort($bookmarkDays); | |
361 | ||
362 | return $bookmarkDays; | |
363 | } | |
364 | ||
365 | /** | |
366 | * @inheritDoc | |
367 | */ | |
368 | public function filterDay($request) | |
369 | { | |
27ddfec3 A |
370 | $visibility = $this->isLoggedIn ? BookmarkFilter::$ALL : BookmarkFilter::$PUBLIC; |
371 | ||
372 | return $this->bookmarkFilter->filter(BookmarkFilter::$FILTER_DAY, $request, false, $visibility); | |
336a28fa A |
373 | } |
374 | ||
375 | /** | |
376 | * @inheritDoc | |
377 | */ | |
378 | public function initialize() | |
379 | { | |
380 | $initializer = new BookmarkInitializer($this); | |
381 | $initializer->initialize(); | |
336a28fa | 382 | |
d6e5f04d A |
383 | if (true === $this->isLoggedIn) { |
384 | $this->save(); | |
385 | } | |
c4ad3d4f A |
386 | } |
387 | ||
336a28fa A |
388 | /** |
389 | * Handles migration to the new database format (BookmarksArray). | |
390 | */ | |
391 | protected function migrate() | |
392 | { | |
393 | $bookmarkDb = new LegacyLinkDB( | |
394 | $this->conf->get('resource.datastore'), | |
395 | true, | |
396 | false | |
397 | ); | |
398 | $updater = new LegacyUpdater( | |
399 | UpdaterUtils::read_updates_file($this->conf->get('resource.updates')), | |
400 | $bookmarkDb, | |
401 | $this->conf, | |
402 | true | |
403 | ); | |
404 | $newUpdates = $updater->update(); | |
405 | if (! empty($newUpdates)) { | |
406 | UpdaterUtils::write_updates_file( | |
407 | $this->conf->get('resource.updates'), | |
408 | $updater->getDoneUpdates() | |
409 | ); | |
410 | } | |
411 | } | |
412 | } |