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