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