NetscapeBookmarkUtils.php 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. <?php
  2. /**
  3. * Utilities to import and export bookmarks using the Netscape format
  4. */
  5. class NetscapeBookmarkUtils
  6. {
  7. /**
  8. * Filters links and adds Netscape-formatted fields
  9. *
  10. * Added fields:
  11. * - timestamp link addition date, using the Unix epoch format
  12. * - taglist comma-separated tag list
  13. *
  14. * @param LinkDB $linkDb Link datastore
  15. * @param string $selection Which links to export: (all|private|public)
  16. * @param bool $prependNoteUrl Prepend note permalinks with the server's URL
  17. * @param string $indexUrl Absolute URL of the Shaarli index page
  18. *
  19. * @throws Exception Invalid export selection
  20. *
  21. * @return array The links to be exported, with additional fields
  22. */
  23. public static function filterAndFormat($linkDb, $selection, $prependNoteUrl, $indexUrl)
  24. {
  25. // see tpl/export.html for possible values
  26. if (! in_array($selection, array('all', 'public', 'private'))) {
  27. throw new Exception('Invalid export selection: "'.$selection.'"');
  28. }
  29. $bookmarkLinks = array();
  30. foreach ($linkDb as $link) {
  31. if ($link['private'] != 0 && $selection == 'public') {
  32. continue;
  33. }
  34. if ($link['private'] == 0 && $selection == 'private') {
  35. continue;
  36. }
  37. $date = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $link['linkdate']);
  38. $link['timestamp'] = $date->getTimestamp();
  39. $link['taglist'] = str_replace(' ', ',', $link['tags']);
  40. if (startsWith($link['url'], '?') && $prependNoteUrl) {
  41. $link['url'] = $indexUrl . $link['url'];
  42. }
  43. $bookmarkLinks[] = $link;
  44. }
  45. return $bookmarkLinks;
  46. }
  47. /**
  48. * Generates an import status summary
  49. *
  50. * @param string $filename name of the file to import
  51. * @param int $filesize size of the file to import
  52. * @param int $importCount how many links were imported
  53. * @param int $overwriteCount how many links were overwritten
  54. * @param int $skipCount how many links were skipped
  55. *
  56. * @return string Summary of the bookmark import status
  57. */
  58. private static function importStatus(
  59. $filename,
  60. $filesize,
  61. $importCount=0,
  62. $overwriteCount=0,
  63. $skipCount=0
  64. )
  65. {
  66. $status = 'File '.$filename.' ('.$filesize.' bytes) ';
  67. if ($importCount == 0 && $overwriteCount == 0 && $skipCount == 0) {
  68. $status .= 'has an unknown file format. Nothing was imported.';
  69. } else {
  70. $status .= 'was successfully processed: '.$importCount.' links imported, ';
  71. $status .= $overwriteCount.' links overwritten, ';
  72. $status .= $skipCount.' links skipped.';
  73. }
  74. return $status;
  75. }
  76. /**
  77. * Imports Web bookmarks from an uploaded Netscape bookmark dump
  78. *
  79. * @param array $post Server $_POST parameters
  80. * @param array $files Server $_FILES parameters
  81. * @param LinkDB $linkDb Loaded LinkDB instance
  82. * @param string $pagecache Page cache
  83. *
  84. * @return string Summary of the bookmark import status
  85. */
  86. public static function import($post, $files, $linkDb, $pagecache)
  87. {
  88. $filename = $files['filetoupload']['name'];
  89. $filesize = $files['filetoupload']['size'];
  90. $data = file_get_contents($files['filetoupload']['tmp_name']);
  91. if (strpos($data, '<!DOCTYPE NETSCAPE-Bookmark-file-1>') === false) {
  92. return self::importStatus($filename, $filesize);
  93. }
  94. // Overwrite existing links?
  95. $overwrite = ! empty($post['overwrite']);
  96. // Add tags to all imported links?
  97. if (empty($post['default_tags'])) {
  98. $defaultTags = array();
  99. } else {
  100. $defaultTags = preg_split(
  101. '/[\s,]+/',
  102. escape($post['default_tags'])
  103. );
  104. }
  105. // links are imported as public by default
  106. $defaultPrivacy = 0;
  107. $parser = new NetscapeBookmarkParser(
  108. true, // nested tag support
  109. $defaultTags, // additional user-specified tags
  110. strval(1 - $defaultPrivacy) // defaultPub = 1 - defaultPrivacy
  111. );
  112. $bookmarks = $parser->parseString($data);
  113. $importCount = 0;
  114. $overwriteCount = 0;
  115. $skipCount = 0;
  116. foreach ($bookmarks as $bkm) {
  117. $private = $defaultPrivacy;
  118. if (empty($post['privacy']) || $post['privacy'] == 'default') {
  119. // use value from the imported file
  120. $private = $bkm['pub'] == '1' ? 0 : 1;
  121. } else if ($post['privacy'] == 'private') {
  122. // all imported links are private
  123. $private = 1;
  124. } else if ($post['privacy'] == 'public') {
  125. // all imported links are public
  126. $private = 0;
  127. }
  128. $newLink = array(
  129. 'title' => $bkm['title'],
  130. 'url' => $bkm['uri'],
  131. 'description' => $bkm['note'],
  132. 'private' => $private,
  133. 'linkdate'=> '',
  134. 'tags' => $bkm['tags']
  135. );
  136. $existingLink = $linkDb->getLinkFromUrl($bkm['uri']);
  137. if ($existingLink !== false) {
  138. if ($overwrite === false) {
  139. // Do not overwrite an existing link
  140. $skipCount++;
  141. continue;
  142. }
  143. // Overwrite an existing link, keep its date
  144. $newLink['linkdate'] = $existingLink['linkdate'];
  145. $linkDb[$existingLink['linkdate']] = $newLink;
  146. $importCount++;
  147. $overwriteCount++;
  148. continue;
  149. }
  150. // Add a new link
  151. $newLinkDate = new DateTime('@'.strval($bkm['time']));
  152. while (!empty($linkDb[$newLinkDate->format(LinkDB::LINK_DATE_FORMAT)])) {
  153. // Ensure the date/time is not already used
  154. // - this hack is necessary as the date/time acts as a primary key
  155. // - apply 1 second increments until an unused index is found
  156. // See https://github.com/shaarli/Shaarli/issues/351
  157. $newLinkDate->add(new DateInterval('PT1S'));
  158. }
  159. $linkDbDate = $newLinkDate->format(LinkDB::LINK_DATE_FORMAT);
  160. $newLink['linkdate'] = $linkDbDate;
  161. $linkDb[$linkDbDate] = $newLink;
  162. $importCount++;
  163. }
  164. $linkDb->save($pagecache);
  165. return self::importStatus(
  166. $filename,
  167. $filesize,
  168. $importCount,
  169. $overwriteCount,
  170. $skipCount
  171. );
  172. }
  173. }