LinkDB.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  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
  15. * - linkdate: date of the creation of this entry, in the form YYYYMMDD_HHMMSS
  16. * (e.g.'20110914_192317')
  17. * - private: Is this link private? 0=no, other value=yes
  18. * - tags: tags attached to this entry (separated by spaces)
  19. * - title Title of the link
  20. * - url URL of the link. Can be absolute or relative.
  21. * Relative URLs are permalinks (e.g.'?m-ukcw')
  22. *
  23. * Implements 3 interfaces:
  24. * - ArrayAccess: behaves like an associative array;
  25. * - Countable: there is a count() method;
  26. * - Iterator: usable in foreach () loops.
  27. */
  28. class LinkDB implements Iterator, Countable, ArrayAccess
  29. {
  30. // List of links (associative array)
  31. // - key: link date (e.g. "20110823_124546"),
  32. // - value: associative array (keys: title, description...)
  33. private $links;
  34. // List of all recorded URLs (key=url, value=linkdate)
  35. // for fast reserve search (url-->linkdate)
  36. private $urls;
  37. // List of linkdate keys (for the Iterator interface implementation)
  38. private $keys;
  39. // Position in the $this->keys array (for the Iterator interface)
  40. private $position;
  41. // Is the user logged in? (used to filter private links)
  42. private $loggedIn;
  43. /**
  44. * Creates a new LinkDB
  45. *
  46. * Checks if the datastore exists; else, attempts to create a dummy one.
  47. *
  48. * @param $isLoggedIn is the user logged in?
  49. */
  50. function __construct($isLoggedIn)
  51. {
  52. // FIXME: do not access $GLOBALS, pass the datastore instead
  53. $this->loggedIn = $isLoggedIn;
  54. $this->checkDB();
  55. $this->readdb();
  56. }
  57. /**
  58. * Countable - Counts elements of an object
  59. */
  60. public function count()
  61. {
  62. return count($this->links);
  63. }
  64. /**
  65. * ArrayAccess - Assigns a value to the specified offset
  66. */
  67. public function offsetSet($offset, $value)
  68. {
  69. // TODO: use exceptions instead of "die"
  70. if (!$this->loggedIn) {
  71. die('You are not authorized to add a link.');
  72. }
  73. if (empty($value['linkdate']) || empty($value['url'])) {
  74. die('Internal Error: A link should always have a linkdate and URL.');
  75. }
  76. if (empty($offset)) {
  77. die('You must specify a key.');
  78. }
  79. $this->links[$offset] = $value;
  80. $this->urls[$value['url']]=$offset;
  81. }
  82. /**
  83. * ArrayAccess - Whether or not an offset exists
  84. */
  85. public function offsetExists($offset)
  86. {
  87. return array_key_exists($offset, $this->links);
  88. }
  89. /**
  90. * ArrayAccess - Unsets an offset
  91. */
  92. public function offsetUnset($offset)
  93. {
  94. if (!$this->loggedIn) {
  95. // TODO: raise an exception
  96. die('You are not authorized to delete a link.');
  97. }
  98. $url = $this->links[$offset]['url'];
  99. unset($this->urls[$url]);
  100. unset($this->links[$offset]);
  101. }
  102. /**
  103. * ArrayAccess - Returns the value at specified offset
  104. */
  105. public function offsetGet($offset)
  106. {
  107. return isset($this->links[$offset]) ? $this->links[$offset] : null;
  108. }
  109. /**
  110. * Iterator - Returns the current element
  111. */
  112. function current()
  113. {
  114. return $this->links[$this->keys[$this->position]];
  115. }
  116. /**
  117. * Iterator - Returns the key of the current element
  118. */
  119. function key()
  120. {
  121. return $this->keys[$this->position];
  122. }
  123. /**
  124. * Iterator - Moves forward to next element
  125. */
  126. function next()
  127. {
  128. ++$this->position;
  129. }
  130. /**
  131. * Iterator - Rewinds the Iterator to the first element
  132. *
  133. * Entries are sorted by date (latest first)
  134. */
  135. function rewind()
  136. {
  137. $this->keys = array_keys($this->links);
  138. rsort($this->keys);
  139. $this->position = 0;
  140. }
  141. /**
  142. * Iterator - Checks if current position is valid
  143. */
  144. function valid()
  145. {
  146. return isset($this->keys[$this->position]);
  147. }
  148. /**
  149. * Checks if the DB directory and file exist
  150. *
  151. * If no DB file is found, creates a dummy DB.
  152. */
  153. private function checkDB()
  154. {
  155. if (file_exists($GLOBALS['config']['DATASTORE'])) {
  156. return;
  157. }
  158. // Create a dummy database for example
  159. $this->links = array();
  160. $link = array(
  161. 'title'=>'Shaarli - sebsauvage.net',
  162. 'url'=>'http://sebsauvage.net/wiki/doku.php?id=php:shaarli',
  163. 'description'=>'Welcome to Shaarli! This is a bookmark. To edit or delete me, you must first login.',
  164. 'private'=>0,
  165. 'linkdate'=>'20110914_190000',
  166. 'tags'=>'opensource software'
  167. );
  168. $this->links[$link['linkdate']] = $link;
  169. $link = array(
  170. 'title'=>'My secret stuff... - Pastebin.com',
  171. 'url'=>'http://sebsauvage.net/paste/?8434b27936c09649#bR7XsXhoTiLcqCpQbmOpBi3rq2zzQUC5hBI7ZT1O3x8=',
  172. 'description'=>'SShhhh!! I\'m a private link only YOU can see. You can delete me too.',
  173. 'private'=>1,
  174. 'linkdate'=>'20110914_074522',
  175. 'tags'=>'secretstuff'
  176. );
  177. $this->links[$link['linkdate']] = $link;
  178. // Write database to disk
  179. // TODO: raise an exception if the file is not write-able
  180. file_put_contents(
  181. // FIXME: do not use $GLOBALS
  182. $GLOBALS['config']['DATASTORE'],
  183. PHPPREFIX.base64_encode(gzdeflate(serialize($this->links))).PHPSUFFIX
  184. );
  185. }
  186. /**
  187. * Reads database from disk to memory
  188. */
  189. private function readdb()
  190. {
  191. // Read data
  192. // Note that gzinflate is faster than gzuncompress.
  193. // See: http://www.php.net/manual/en/function.gzdeflate.php#96439
  194. // FIXME: do not use $GLOBALS
  195. $this->links = array();
  196. if (file_exists($GLOBALS['config']['DATASTORE'])) {
  197. $this->links = unserialize(gzinflate(base64_decode(
  198. substr(file_get_contents($GLOBALS['config']['DATASTORE']),
  199. strlen(PHPPREFIX), -strlen(PHPSUFFIX)))));
  200. }
  201. // If user is not logged in, filter private links.
  202. if (!$this->loggedIn) {
  203. $toremove = array();
  204. foreach ($this->links as $link) {
  205. if ($link['private'] != 0) {
  206. $toremove[] = $link['linkdate'];
  207. }
  208. }
  209. foreach ($toremove as $linkdate) {
  210. unset($this->links[$linkdate]);
  211. }
  212. }
  213. // Keep the list of the mapping URLs-->linkdate up-to-date.
  214. $this->urls = array();
  215. foreach ($this->links as $link) {
  216. $this->urls[$link['url']] = $link['linkdate'];
  217. }
  218. }
  219. /**
  220. * Saves the database from memory to disk
  221. */
  222. public function savedb()
  223. {
  224. if (!$this->loggedIn) {
  225. // TODO: raise an Exception instead
  226. die('You are not authorized to change the database.');
  227. }
  228. file_put_contents(
  229. $GLOBALS['config']['DATASTORE'],
  230. PHPPREFIX.base64_encode(gzdeflate(serialize($this->links))).PHPSUFFIX
  231. );
  232. invalidateCaches();
  233. }
  234. /**
  235. * Returns the link for a given URL, or False if it does not exist.
  236. */
  237. public function getLinkFromUrl($url)
  238. {
  239. if (isset($this->urls[$url])) {
  240. return $this->links[$this->urls[$url]];
  241. }
  242. return false;
  243. }
  244. /**
  245. * Returns the list of links corresponding to a full-text search
  246. *
  247. * Searches:
  248. * - in the URLs, title and description;
  249. * - are case-insensitive.
  250. *
  251. * Example:
  252. * print_r($mydb->filterFulltext('hollandais'));
  253. *
  254. * mb_convert_case($val, MB_CASE_LOWER, 'UTF-8')
  255. * - allows to perform searches on Unicode text
  256. * - see https://github.com/shaarli/Shaarli/issues/75 for examples
  257. */
  258. public function filterFulltext($searchterms)
  259. {
  260. // FIXME: explode(' ',$searchterms) and perform a AND search.
  261. // FIXME: accept double-quotes to search for a string "as is"?
  262. $filtered = array();
  263. $search = mb_convert_case($searchterms, MB_CASE_LOWER, 'UTF-8');
  264. $keys = ['title', 'description', 'url', 'tags'];
  265. foreach ($this->links as $link) {
  266. $found = false;
  267. foreach ($keys as $key) {
  268. if (strpos(mb_convert_case($link[$key], MB_CASE_LOWER, 'UTF-8'),
  269. $search) !== false) {
  270. $found = true;
  271. }
  272. }
  273. if ($found) {
  274. $filtered[$link['linkdate']] = $link;
  275. }
  276. }
  277. krsort($filtered);
  278. return $filtered;
  279. }
  280. /**
  281. * Returns the list of links associated with a given list of tags
  282. *
  283. * You can specify one or more tags, separated by space or a comma, e.g.
  284. * print_r($mydb->filterTags('linux programming'));
  285. */
  286. public function filterTags($tags, $casesensitive=false)
  287. {
  288. // Same as above, we use UTF-8 conversion to handle various graphemes (i.e. cyrillic, or greek)
  289. // FIXME: is $casesensitive ever true?
  290. $t = str_replace(
  291. ',', ' ',
  292. ($casesensitive ? $tags : mb_convert_case($tags, MB_CASE_LOWER, 'UTF-8'))
  293. );
  294. $searchtags = explode(' ', $t);
  295. $filtered = array();
  296. foreach ($this->links as $l) {
  297. $linktags = explode(
  298. ' ',
  299. ($casesensitive ? $l['tags']:mb_convert_case($l['tags'], MB_CASE_LOWER, 'UTF-8'))
  300. );
  301. if (count(array_intersect($linktags, $searchtags)) == count($searchtags)) {
  302. $filtered[$l['linkdate']] = $l;
  303. }
  304. }
  305. krsort($filtered);
  306. return $filtered;
  307. }
  308. /**
  309. * Returns the list of articles for a given day, chronologically sorted
  310. *
  311. * Day must be in the form 'YYYYMMDD' (e.g. '20120125'), e.g.
  312. * print_r($mydb->filterDay('20120125'));
  313. */
  314. public function filterDay($day)
  315. {
  316. // TODO: check input format
  317. $filtered = array();
  318. foreach ($this->links as $l) {
  319. if (startsWith($l['linkdate'], $day)) {
  320. $filtered[$l['linkdate']] = $l;
  321. }
  322. }
  323. ksort($filtered);
  324. return $filtered;
  325. }
  326. /**
  327. * Returns the article corresponding to a smallHash
  328. */
  329. public function filterSmallHash($smallHash)
  330. {
  331. $filtered = array();
  332. foreach ($this->links as $l) {
  333. if ($smallHash == smallHash($l['linkdate'])) {
  334. // Yes, this is ugly and slow
  335. $filtered[$l['linkdate']] = $l;
  336. return $filtered;
  337. }
  338. }
  339. return $filtered;
  340. }
  341. /**
  342. * Returns the list of all tags
  343. * Output: associative array key=tags, value=0
  344. */
  345. public function allTags()
  346. {
  347. $tags = array();
  348. foreach ($this->links as $link) {
  349. foreach (explode(' ', $link['tags']) as $tag) {
  350. if (!empty($tag)) {
  351. $tags[$tag] = (empty($tags[$tag]) ? 1 : $tags[$tag] + 1);
  352. }
  353. }
  354. }
  355. // Sort tags by usage (most used tag first)
  356. arsort($tags);
  357. return $tags;
  358. }
  359. /**
  360. * Returns the list of days containing articles (oldest first)
  361. * Output: An array containing days (in format YYYYMMDD).
  362. */
  363. public function days()
  364. {
  365. $linkDays = array();
  366. foreach (array_keys($this->links) as $day) {
  367. $linkDays[substr($day, 0, 8)] = 0;
  368. }
  369. $linkDays = array_keys($linkDays);
  370. sort($linkDays);
  371. return $linkDays;
  372. }
  373. }
  374. ?>