LinkDB.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386
  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. Used for displayable links (no redirector, relative, etc.).
  21. * Can be absolute or relative.
  22. * Relative URLs are permalinks (e.g.'?m-ukcw')
  23. * - real_url Absolute processed URL.
  24. *
  25. * Implements 3 interfaces:
  26. * - ArrayAccess: behaves like an associative array;
  27. * - Countable: there is a count() method;
  28. * - Iterator: usable in foreach () loops.
  29. */
  30. class LinkDB implements Iterator, Countable, ArrayAccess
  31. {
  32. // Links are stored as a PHP serialized string
  33. private $_datastore;
  34. // Datastore PHP prefix
  35. protected static $phpPrefix = '<?php /* ';
  36. // Datastore PHP suffix
  37. protected static $phpSuffix = ' */ ?>';
  38. // List of links (associative array)
  39. // - key: link date (e.g. "20110823_124546"),
  40. // - value: associative array (keys: title, description...)
  41. private $_links;
  42. // List of all recorded URLs (key=url, value=linkdate)
  43. // for fast reserve search (url-->linkdate)
  44. private $_urls;
  45. // List of linkdate keys (for the Iterator interface implementation)
  46. private $_keys;
  47. // Position in the $this->_keys array (for the Iterator interface)
  48. private $_position;
  49. // Is the user logged in? (used to filter private links)
  50. private $_loggedIn;
  51. // Hide public links
  52. private $_hidePublicLinks;
  53. // link redirector set in user settings.
  54. private $_redirector;
  55. /**
  56. * Creates a new LinkDB
  57. *
  58. * Checks if the datastore exists; else, attempts to create a dummy one.
  59. *
  60. * @param string $datastore datastore file path.
  61. * @param boolean $isLoggedIn is the user logged in?
  62. * @param boolean $hidePublicLinks if true all links are private.
  63. * @param string $redirector link redirector set in user settings.
  64. */
  65. function __construct($datastore, $isLoggedIn, $hidePublicLinks, $redirector = '')
  66. {
  67. $this->_datastore = $datastore;
  68. $this->_loggedIn = $isLoggedIn;
  69. $this->_hidePublicLinks = $hidePublicLinks;
  70. $this->_redirector = $redirector;
  71. $this->_checkDB();
  72. $this->_readDB();
  73. }
  74. /**
  75. * Countable - Counts elements of an object
  76. */
  77. public function count()
  78. {
  79. return count($this->_links);
  80. }
  81. /**
  82. * ArrayAccess - Assigns a value to the specified offset
  83. */
  84. public function offsetSet($offset, $value)
  85. {
  86. // TODO: use exceptions instead of "die"
  87. if (!$this->_loggedIn) {
  88. die('You are not authorized to add a link.');
  89. }
  90. if (empty($value['linkdate']) || empty($value['url'])) {
  91. die('Internal Error: A link should always have a linkdate and URL.');
  92. }
  93. if (empty($offset)) {
  94. die('You must specify a key.');
  95. }
  96. $this->_links[$offset] = $value;
  97. $this->_urls[$value['url']]=$offset;
  98. }
  99. /**
  100. * ArrayAccess - Whether or not an offset exists
  101. */
  102. public function offsetExists($offset)
  103. {
  104. return array_key_exists($offset, $this->_links);
  105. }
  106. /**
  107. * ArrayAccess - Unsets an offset
  108. */
  109. public function offsetUnset($offset)
  110. {
  111. if (!$this->_loggedIn) {
  112. // TODO: raise an exception
  113. die('You are not authorized to delete a link.');
  114. }
  115. $url = $this->_links[$offset]['url'];
  116. unset($this->_urls[$url]);
  117. unset($this->_links[$offset]);
  118. }
  119. /**
  120. * ArrayAccess - Returns the value at specified offset
  121. */
  122. public function offsetGet($offset)
  123. {
  124. return isset($this->_links[$offset]) ? $this->_links[$offset] : null;
  125. }
  126. /**
  127. * Iterator - Returns the current element
  128. */
  129. function current()
  130. {
  131. return $this->_links[$this->_keys[$this->_position]];
  132. }
  133. /**
  134. * Iterator - Returns the key of the current element
  135. */
  136. function key()
  137. {
  138. return $this->_keys[$this->_position];
  139. }
  140. /**
  141. * Iterator - Moves forward to next element
  142. */
  143. function next()
  144. {
  145. ++$this->_position;
  146. }
  147. /**
  148. * Iterator - Rewinds the Iterator to the first element
  149. *
  150. * Entries are sorted by date (latest first)
  151. */
  152. function rewind()
  153. {
  154. $this->_keys = array_keys($this->_links);
  155. rsort($this->_keys);
  156. $this->_position = 0;
  157. }
  158. /**
  159. * Iterator - Checks if current position is valid
  160. */
  161. function valid()
  162. {
  163. return isset($this->_keys[$this->_position]);
  164. }
  165. /**
  166. * Checks if the DB directory and file exist
  167. *
  168. * If no DB file is found, creates a dummy DB.
  169. */
  170. private function _checkDB()
  171. {
  172. if (file_exists($this->_datastore)) {
  173. return;
  174. }
  175. // Create a dummy database for example
  176. $this->_links = array();
  177. $link = array(
  178. 'title'=>' Shaarli: the personal, minimalist, super-fast, no-database delicious clone',
  179. 'url'=>'https://github.com/shaarli/Shaarli/wiki',
  180. 'description'=>'Welcome to Shaarli! This is your first public bookmark. To edit or delete me, you must first login.
  181. To learn how to use Shaarli, consult the link "Help/documentation" at the bottom of this page.
  182. You use the community supported version of the original Shaarli project, by Sebastien Sauvage.',
  183. 'private'=>0,
  184. 'linkdate'=> date('Ymd_His'),
  185. 'tags'=>'opensource software'
  186. );
  187. $this->_links[$link['linkdate']] = $link;
  188. $link = array(
  189. 'title'=>'My secret stuff... - Pastebin.com',
  190. 'url'=>'http://sebsauvage.net/paste/?8434b27936c09649#bR7XsXhoTiLcqCpQbmOpBi3rq2zzQUC5hBI7ZT1O3x8=',
  191. 'description'=>'Shhhh! I\'m a private link only YOU can see. You can delete me too.',
  192. 'private'=>1,
  193. 'linkdate'=> date('Ymd_His', strtotime('-1 minute')),
  194. 'tags'=>'secretstuff'
  195. );
  196. $this->_links[$link['linkdate']] = $link;
  197. // Write database to disk
  198. $this->writeDB();
  199. }
  200. /**
  201. * Reads database from disk to memory
  202. */
  203. private function _readDB()
  204. {
  205. // Public links are hidden and user not logged in => nothing to show
  206. if ($this->_hidePublicLinks && !$this->_loggedIn) {
  207. $this->_links = array();
  208. return;
  209. }
  210. // Read data
  211. // Note that gzinflate is faster than gzuncompress.
  212. // See: http://www.php.net/manual/en/function.gzdeflate.php#96439
  213. $this->_links = array();
  214. if (file_exists($this->_datastore)) {
  215. $this->_links = unserialize(gzinflate(base64_decode(
  216. substr(file_get_contents($this->_datastore),
  217. strlen(self::$phpPrefix), -strlen(self::$phpSuffix)))));
  218. }
  219. // If user is not logged in, filter private links.
  220. if (!$this->_loggedIn) {
  221. $toremove = array();
  222. foreach ($this->_links as $link) {
  223. if ($link['private'] != 0) {
  224. $toremove[] = $link['linkdate'];
  225. }
  226. }
  227. foreach ($toremove as $linkdate) {
  228. unset($this->_links[$linkdate]);
  229. }
  230. }
  231. // Keep the list of the mapping URLs-->linkdate up-to-date.
  232. $this->_urls = array();
  233. foreach ($this->_links as $link) {
  234. $this->_urls[$link['url']] = $link['linkdate'];
  235. }
  236. // Escape links data
  237. foreach($this->_links as &$link) {
  238. sanitizeLink($link);
  239. // Do not use the redirector for internal links (Shaarli note URL starting with a '?').
  240. if (!empty($this->_redirector) && !startsWith($link['url'], '?')) {
  241. $link['real_url'] = $this->_redirector . urlencode($link['url']);
  242. }
  243. else {
  244. $link['real_url'] = $link['url'];
  245. }
  246. }
  247. }
  248. /**
  249. * Saves the database from memory to disk
  250. *
  251. * @throws IOException the datastore is not writable
  252. */
  253. private function writeDB()
  254. {
  255. if (is_file($this->_datastore) && !is_writeable($this->_datastore)) {
  256. // The datastore exists but is not writeable
  257. throw new IOException($this->_datastore);
  258. } else if (!is_file($this->_datastore) && !is_writeable(dirname($this->_datastore))) {
  259. // The datastore does not exist and its parent directory is not writeable
  260. throw new IOException(dirname($this->_datastore));
  261. }
  262. file_put_contents(
  263. $this->_datastore,
  264. self::$phpPrefix.base64_encode(gzdeflate(serialize($this->_links))).self::$phpSuffix
  265. );
  266. }
  267. /**
  268. * Saves the database from memory to disk
  269. *
  270. * @param string $pageCacheDir page cache directory
  271. */
  272. public function savedb($pageCacheDir)
  273. {
  274. if (!$this->_loggedIn) {
  275. // TODO: raise an Exception instead
  276. die('You are not authorized to change the database.');
  277. }
  278. $this->writeDB();
  279. invalidateCaches($pageCacheDir);
  280. }
  281. /**
  282. * Returns the link for a given URL, or False if it does not exist.
  283. *
  284. * @param string $url URL to search for
  285. *
  286. * @return mixed the existing link if it exists, else 'false'
  287. */
  288. public function getLinkFromUrl($url)
  289. {
  290. if (isset($this->_urls[$url])) {
  291. return $this->_links[$this->_urls[$url]];
  292. }
  293. return false;
  294. }
  295. /**
  296. * Filter links.
  297. *
  298. * @param string $type Type of filter.
  299. * @param mixed $request Search request, string or array.
  300. * @param bool $casesensitive Optional: Perform case sensitive filter
  301. * @param bool $privateonly Optional: Returns private links only if true.
  302. *
  303. * @return array filtered links
  304. */
  305. public function filter($type, $request, $casesensitive = false, $privateonly = false)
  306. {
  307. $linkFilter = new LinkFilter($this->_links);
  308. $requestFilter = is_array($request) ? implode(' ', $request) : $request;
  309. return $linkFilter->filter($type, trim($requestFilter), $casesensitive, $privateonly);
  310. }
  311. /**
  312. * Returns the list of all tags
  313. * Output: associative array key=tags, value=0
  314. */
  315. public function allTags()
  316. {
  317. $tags = array();
  318. foreach ($this->_links as $link) {
  319. foreach (explode(' ', $link['tags']) as $tag) {
  320. if (!empty($tag)) {
  321. $tags[$tag] = (empty($tags[$tag]) ? 1 : $tags[$tag] + 1);
  322. }
  323. }
  324. }
  325. // Sort tags by usage (most used tag first)
  326. arsort($tags);
  327. return $tags;
  328. }
  329. /**
  330. * Returns the list of days containing articles (oldest first)
  331. * Output: An array containing days (in format YYYYMMDD).
  332. */
  333. public function days()
  334. {
  335. $linkDays = array();
  336. foreach (array_keys($this->_links) as $day) {
  337. $linkDays[substr($day, 0, 8)] = 0;
  338. }
  339. $linkDays = array_keys($linkDays);
  340. sort($linkDays);
  341. return $linkDays;
  342. }
  343. }