]> git.immae.eu Git - github/shaarli/Shaarli.git/blame - application/LinkDB.php
LinkDB: do not prefix privates with an underscore
[github/shaarli/Shaarli.git] / application / LinkDB.php
CommitLineData
ca74886f
V
1<?php
2/**
3 * Data storage for links.
4 *
5 * This object behaves like an associative array.
6 *
7 * Example:
8 * $myLinks = new LinkDB();
9 * echo $myLinks['20110826_161819']['title'];
10 * foreach ($myLinks as $link)
11 * echo $link['title'].' at url '.$link['url'].'; description:'.$link['description'];
12 *
13 * Available keys:
14 * - description: description of the entry
9646b7da 15 * - linkdate: creation date of this entry, format: YYYYMMDD_HHMMSS
ca74886f 16 * (e.g.'20110914_192317')
9646b7da 17 * - updated: last modification date of this entry, format: YYYYMMDD_HHMMSS
ca74886f
V
18 * - private: Is this link private? 0=no, other value=yes
19 * - tags: tags attached to this entry (separated by spaces)
20 * - title Title of the link
49e62f22
A
21 * - url URL of the link. Used for displayable links (no redirector, relative, etc.).
22 * Can be absolute or relative.
ca74886f 23 * Relative URLs are permalinks (e.g.'?m-ukcw')
49e62f22 24 * - real_url Absolute processed URL.
ca74886f
V
25 *
26 * Implements 3 interfaces:
27 * - ArrayAccess: behaves like an associative array;
28 * - Countable: there is a count() method;
29 * - Iterator: usable in foreach () loops.
30 */
31class LinkDB implements Iterator, Countable, ArrayAccess
32{
9c8752a2 33 // Links are stored as a PHP serialized string
628b97cb 34 private $datastore;
9c8752a2 35
205a4277
V
36 // Link date storage format
37 const LINK_DATE_FORMAT = 'Ymd_His';
38
9c8752a2
V
39 // Datastore PHP prefix
40 protected static $phpPrefix = '<?php /* ';
41
42 // Datastore PHP suffix
43 protected static $phpSuffix = ' */ ?>';
44
ca74886f
V
45 // List of links (associative array)
46 // - key: link date (e.g. "20110823_124546"),
47 // - value: associative array (keys: title, description...)
628b97cb 48 private $links;
ca74886f
V
49
50 // List of all recorded URLs (key=url, value=linkdate)
51 // for fast reserve search (url-->linkdate)
628b97cb 52 private $urls;
ca74886f
V
53
54 // List of linkdate keys (for the Iterator interface implementation)
628b97cb 55 private $keys;
ca74886f 56
628b97cb
V
57 // Position in the $this->keys array (for the Iterator interface)
58 private $position;
ca74886f
V
59
60 // Is the user logged in? (used to filter private links)
628b97cb 61 private $loggedIn;
ca74886f 62
9f15ca9e 63 // Hide public links
628b97cb 64 private $hidePublicLinks;
9f15ca9e 65
90e5bd65 66 // link redirector set in user settings.
628b97cb 67 private $redirector;
90e5bd65 68
043eae70
A
69 /**
70 * Set this to `true` to urlencode link behind redirector link, `false` to leave it untouched.
71 *
72 * Example:
73 * anonym.to needs clean URL while dereferer.org needs urlencoded URL.
74 *
75 * @var boolean $redirectorEncode parameter: true or false
76 */
77 private $redirectorEncode;
78
ca74886f
V
79 /**
80 * Creates a new LinkDB
81 *
82 * Checks if the datastore exists; else, attempts to create a dummy one.
83 *
043eae70
A
84 * @param string $datastore datastore file path.
85 * @param boolean $isLoggedIn is the user logged in?
86 * @param boolean $hidePublicLinks if true all links are private.
87 * @param string $redirector link redirector set in user settings.
88 * @param boolean $redirectorEncode Enable urlencode on redirected urls (default: true).
ca74886f 89 */
043eae70
A
90 function __construct(
91 $datastore,
92 $isLoggedIn,
93 $hidePublicLinks,
94 $redirector = '',
95 $redirectorEncode = true
96 )
ca74886f 97 {
628b97cb
V
98 $this->datastore = $datastore;
99 $this->loggedIn = $isLoggedIn;
100 $this->hidePublicLinks = $hidePublicLinks;
101 $this->redirector = $redirector;
043eae70 102 $this->redirectorEncode = $redirectorEncode === true;
628b97cb
V
103 $this->checkDB();
104 $this->readDB();
ca74886f
V
105 }
106
107 /**
108 * Countable - Counts elements of an object
109 */
110 public function count()
111 {
628b97cb 112 return count($this->links);
ca74886f
V
113 }
114
115 /**
116 * ArrayAccess - Assigns a value to the specified offset
117 */
118 public function offsetSet($offset, $value)
119 {
120 // TODO: use exceptions instead of "die"
628b97cb 121 if (!$this->loggedIn) {
ca74886f
V
122 die('You are not authorized to add a link.');
123 }
124 if (empty($value['linkdate']) || empty($value['url'])) {
125 die('Internal Error: A link should always have a linkdate and URL.');
126 }
127 if (empty($offset)) {
128 die('You must specify a key.');
129 }
628b97cb
V
130 $this->links[$offset] = $value;
131 $this->urls[$value['url']]=$offset;
ca74886f
V
132 }
133
134 /**
135 * ArrayAccess - Whether or not an offset exists
136 */
137 public function offsetExists($offset)
138 {
628b97cb 139 return array_key_exists($offset, $this->links);
ca74886f
V
140 }
141
142 /**
143 * ArrayAccess - Unsets an offset
144 */
145 public function offsetUnset($offset)
146 {
628b97cb 147 if (!$this->loggedIn) {
ca74886f
V
148 // TODO: raise an exception
149 die('You are not authorized to delete a link.');
150 }
628b97cb
V
151 $url = $this->links[$offset]['url'];
152 unset($this->urls[$url]);
153 unset($this->links[$offset]);
ca74886f
V
154 }
155
156 /**
157 * ArrayAccess - Returns the value at specified offset
158 */
159 public function offsetGet($offset)
160 {
628b97cb 161 return isset($this->links[$offset]) ? $this->links[$offset] : null;
ca74886f
V
162 }
163
164 /**
165 * Iterator - Returns the current element
166 */
167 function current()
168 {
628b97cb 169 return $this->links[$this->keys[$this->position]];
ca74886f
V
170 }
171
172 /**
173 * Iterator - Returns the key of the current element
174 */
175 function key()
176 {
628b97cb 177 return $this->keys[$this->position];
ca74886f
V
178 }
179
180 /**
181 * Iterator - Moves forward to next element
182 */
183 function next()
184 {
628b97cb 185 ++$this->position;
ca74886f
V
186 }
187
188 /**
189 * Iterator - Rewinds the Iterator to the first element
190 *
191 * Entries are sorted by date (latest first)
192 */
193 function rewind()
194 {
628b97cb
V
195 $this->keys = array_keys($this->links);
196 rsort($this->keys);
197 $this->position = 0;
ca74886f
V
198 }
199
200 /**
201 * Iterator - Checks if current position is valid
202 */
203 function valid()
204 {
628b97cb 205 return isset($this->keys[$this->position]);
ca74886f
V
206 }
207
208 /**
209 * Checks if the DB directory and file exist
210 *
211 * If no DB file is found, creates a dummy DB.
212 */
628b97cb 213 private function checkDB()
ca74886f 214 {
628b97cb 215 if (file_exists($this->datastore)) {
ca74886f
V
216 return;
217 }
218
219 // Create a dummy database for example
628b97cb 220 $this->links = array();
ca74886f 221 $link = array(
598376d4
A
222 'title'=>' Shaarli: the personal, minimalist, super-fast, no-database delicious clone',
223 'url'=>'https://github.com/shaarli/Shaarli/wiki',
224 'description'=>'Welcome to Shaarli! This is your first public bookmark. To edit or delete me, you must first login.
225
226To learn how to use Shaarli, consult the link "Help/documentation" at the bottom of this page.
227
228You use the community supported version of the original Shaarli project, by Sebastien Sauvage.',
ca74886f 229 'private'=>0,
598376d4 230 'linkdate'=> date('Ymd_His'),
ca74886f
V
231 'tags'=>'opensource software'
232 );
628b97cb 233 $this->links[$link['linkdate']] = $link;
ca74886f
V
234
235 $link = array(
236 'title'=>'My secret stuff... - Pastebin.com',
237 'url'=>'http://sebsauvage.net/paste/?8434b27936c09649#bR7XsXhoTiLcqCpQbmOpBi3rq2zzQUC5hBI7ZT1O3x8=',
598376d4 238 'description'=>'Shhhh! I\'m a private link only YOU can see. You can delete me too.',
ca74886f 239 'private'=>1,
598376d4 240 'linkdate'=> date('Ymd_His', strtotime('-1 minute')),
ca74886f
V
241 'tags'=>'secretstuff'
242 );
628b97cb 243 $this->links[$link['linkdate']] = $link;
ca74886f
V
244
245 // Write database to disk
2e28269b 246 $this->writeDB();
ca74886f
V
247 }
248
249 /**
250 * Reads database from disk to memory
251 */
628b97cb 252 private function readDB()
ca74886f 253 {
578a84bd 254
255 // Public links are hidden and user not logged in => nothing to show
628b97cb
V
256 if ($this->hidePublicLinks && !$this->loggedIn) {
257 $this->links = array();
578a84bd 258 return;
259 }
260
ca74886f
V
261 // Read data
262 // Note that gzinflate is faster than gzuncompress.
263 // See: http://www.php.net/manual/en/function.gzdeflate.php#96439
628b97cb 264 $this->links = array();
ca74886f 265
628b97cb
V
266 if (file_exists($this->datastore)) {
267 $this->links = unserialize(gzinflate(base64_decode(
268 substr(file_get_contents($this->datastore),
9c8752a2 269 strlen(self::$phpPrefix), -strlen(self::$phpSuffix)))));
ca74886f
V
270 }
271
272 // If user is not logged in, filter private links.
628b97cb 273 if (!$this->loggedIn) {
ca74886f 274 $toremove = array();
628b97cb 275 foreach ($this->links as $link) {
ca74886f
V
276 if ($link['private'] != 0) {
277 $toremove[] = $link['linkdate'];
278 }
279 }
280 foreach ($toremove as $linkdate) {
628b97cb 281 unset($this->links[$linkdate]);
ca74886f
V
282 }
283 }
284
628b97cb
V
285 $this->urls = array();
286 foreach ($this->links as &$link) {
510377d2 287 // Keep the list of the mapping URLs-->linkdate up-to-date.
628b97cb 288 $this->urls[$link['url']] = $link['linkdate'];
195acf9f 289
510377d2 290 // Sanitize data fields.
90e5bd65 291 sanitizeLink($link);
195acf9f
A
292
293 // Remove private tags if the user is not logged in.
628b97cb 294 if (! $this->loggedIn) {
9866b408 295 $link['tags'] = preg_replace('/(^|\s+)\.[^($|\s)]+\s*/', ' ', $link['tags']);
195acf9f
A
296 }
297
90e5bd65 298 // Do not use the redirector for internal links (Shaarli note URL starting with a '?').
628b97cb
V
299 if (!empty($this->redirector) && !startsWith($link['url'], '?')) {
300 $link['real_url'] = $this->redirector;
043eae70
A
301 if ($this->redirectorEncode) {
302 $link['real_url'] .= urlencode(unescape($link['url']));
303 } else {
304 $link['real_url'] .= $link['url'];
305 }
90e5bd65
A
306 }
307 else {
308 $link['real_url'] = $link['url'];
309 }
5f85fcd8 310 }
ca74886f
V
311 }
312
2e28269b
V
313 /**
314 * Saves the database from memory to disk
315 *
316 * @throws IOException the datastore is not writable
317 */
318 private function writeDB()
319 {
628b97cb 320 if (is_file($this->datastore) && !is_writeable($this->datastore)) {
2e28269b 321 // The datastore exists but is not writeable
628b97cb
V
322 throw new IOException($this->datastore);
323 } else if (!is_file($this->datastore) && !is_writeable(dirname($this->datastore))) {
2e28269b 324 // The datastore does not exist and its parent directory is not writeable
628b97cb 325 throw new IOException(dirname($this->datastore));
2e28269b
V
326 }
327
328 file_put_contents(
628b97cb
V
329 $this->datastore,
330 self::$phpPrefix.base64_encode(gzdeflate(serialize($this->links))).self::$phpSuffix
2e28269b
V
331 );
332
333 }
334
ca74886f
V
335 /**
336 * Saves the database from memory to disk
01e48f26
V
337 *
338 * @param string $pageCacheDir page cache directory
ca74886f 339 */
01e48f26 340 public function savedb($pageCacheDir)
ca74886f 341 {
628b97cb 342 if (!$this->loggedIn) {
ca74886f
V
343 // TODO: raise an Exception instead
344 die('You are not authorized to change the database.');
345 }
2e28269b
V
346
347 $this->writeDB();
348
01e48f26 349 invalidateCaches($pageCacheDir);
ca74886f
V
350 }
351
352 /**
353 * Returns the link for a given URL, or False if it does not exist.
ef591e7e
GV
354 *
355 * @param string $url URL to search for
356 *
357 * @return mixed the existing link if it exists, else 'false'
ca74886f
V
358 */
359 public function getLinkFromUrl($url)
360 {
628b97cb
V
361 if (isset($this->urls[$url])) {
362 return $this->links[$this->urls[$url]];
ca74886f
V
363 }
364 return false;
365 }
366
367 /**
528a6f8a 368 * Returns the shaare corresponding to a smallHash.
ca74886f 369 *
528a6f8a
A
370 * @param string $request QUERY_STRING server parameter.
371 *
372 * @return array $filtered array containing permalink data.
373 *
374 * @throws LinkNotFoundException if the smallhash is malformed or doesn't match any link.
375 */
376 public function filterHash($request)
377 {
378 $request = substr($request, 0, 6);
628b97cb 379 $linkFilter = new LinkFilter($this->links);
528a6f8a
A
380 return $linkFilter->filter(LinkFilter::$FILTER_HASH, $request);
381 }
382
383 /**
384 * Returns the list of articles for a given day.
385 *
386 * @param string $request day to filter. Format: YYYYMMDD.
387 *
388 * @return array list of shaare found.
389 */
390 public function filterDay($request) {
628b97cb 391 $linkFilter = new LinkFilter($this->links);
528a6f8a
A
392 return $linkFilter->filter(LinkFilter::$FILTER_DAY, $request);
393 }
394
395 /**
396 * Filter links according to search parameters.
397 *
398 * @param array $filterRequest Search request content. Supported keys:
399 * - searchtags: list of tags
400 * - searchterm: term search
822bffce
A
401 * @param bool $casesensitive Optional: Perform case sensitive filter
402 * @param bool $privateonly Optional: Returns private links only if true.
ca74886f 403 *
528a6f8a 404 * @return array filtered links, all links if no suitable filter was provided.
ca74886f 405 */
528a6f8a 406 public function filterSearch($filterRequest = array(), $casesensitive = false, $privateonly = false)
55d0a5c4 407 {
528a6f8a
A
408 // Filter link database according to parameters.
409 $searchtags = !empty($filterRequest['searchtags']) ? escape($filterRequest['searchtags']) : '';
410 $searchterm = !empty($filterRequest['searchterm']) ? escape($filterRequest['searchterm']) : '';
411
412 // Search tags + fullsearch.
9ccca401 413 if (! empty($searchtags) && ! empty($searchterm)) {
528a6f8a
A
414 $type = LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT;
415 $request = array($searchtags, $searchterm);
416 }
417 // Search by tags.
418 elseif (! empty($searchtags)) {
419 $type = LinkFilter::$FILTER_TAG;
420 $request = $searchtags;
421 }
422 // Fulltext search.
423 elseif (! empty($searchterm)) {
424 $type = LinkFilter::$FILTER_TEXT;
425 $request = $searchterm;
426 }
427 // Otherwise, display without filtering.
428 else {
429 $type = '';
430 $request = '';
431 }
432
628b97cb 433 $linkFilter = new LinkFilter($this->links);
c51fae92 434 return $linkFilter->filter($type, $request, $casesensitive, $privateonly);
ca74886f
V
435 }
436
437 /**
438 * Returns the list of all tags
439 * Output: associative array key=tags, value=0
440 */
441 public function allTags()
442 {
443 $tags = array();
b1eb5d1d 444 $caseMapping = array();
628b97cb 445 foreach ($this->links as $link) {
4b35853d 446 foreach (preg_split('/\s+/', $link['tags'], 0, PREG_SPLIT_NO_EMPTY) as $tag) {
b1eb5d1d
A
447 if (empty($tag)) {
448 continue;
ca74886f 449 }
b1eb5d1d
A
450 // The first case found will be displayed.
451 if (!isset($caseMapping[strtolower($tag)])) {
452 $caseMapping[strtolower($tag)] = $tag;
453 $tags[$caseMapping[strtolower($tag)]] = 0;
454 }
455 $tags[$caseMapping[strtolower($tag)]]++;
ca74886f
V
456 }
457 }
458 // Sort tags by usage (most used tag first)
459 arsort($tags);
460 return $tags;
461 }
462
463 /**
464 * Returns the list of days containing articles (oldest first)
465 * Output: An array containing days (in format YYYYMMDD).
466 */
467 public function days()
468 {
469 $linkDays = array();
628b97cb 470 foreach (array_keys($this->links) as $day) {
ca74886f
V
471 $linkDays[substr($day, 0, 8)] = 0;
472 }
473 $linkDays = array_keys($linkDays);
474 sort($linkDays);
510377d2 475
ca74886f
V
476 return $linkDays;
477 }
478}