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