aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--application/NetscapeBookmarkUtils.php47
-rw-r--r--index.php57
-rw-r--r--tests/NetscapeBookmarkUtilsTest.php104
-rw-r--r--tpl/export.bookmarks.html10
-rw-r--r--tpl/export.html26
5 files changed, 202 insertions, 42 deletions
diff --git a/application/NetscapeBookmarkUtils.php b/application/NetscapeBookmarkUtils.php
new file mode 100644
index 00000000..8a296705
--- /dev/null
+++ b/application/NetscapeBookmarkUtils.php
@@ -0,0 +1,47 @@
1<?php
2
3/**
4 * Utilities to import and export bookmarks using the Netscape format
5 */
6class NetscapeBookmarkUtils
7{
8
9 /**
10 * Filters links and adds Netscape-formatted fields
11 *
12 * Added fields:
13 * - timestamp link addition date, using the Unix epoch format
14 * - taglist comma-separated tag list
15 *
16 * @param LinkDB $linkDb The link datastore
17 * @param string $selection Which links to export: (all|private|public)
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)
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
30 $bookmarkLinks = array();
31
32 foreach ($linkDb as $link) {
33 if ($link['private'] != 0 && $selection == 'public') {
34 continue;
35 }
36 if ($link['private'] == 0 && $selection == 'private') {
37 continue;
38 }
39 $date = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $link['linkdate']);
40 $link['timestamp'] = $date->getTimestamp();
41 $link['taglist'] = str_replace(' ', ',', $link['tags']);
42 $bookmarkLinks[] = $link;
43 }
44
45 return $bookmarkLinks;
46 }
47}
diff --git a/index.php b/index.php
index d3369a2d..456d93c2 100644
--- a/index.php
+++ b/index.php
@@ -161,6 +161,7 @@ require_once 'application/HttpUtils.php';
161require_once 'application/LinkDB.php'; 161require_once 'application/LinkDB.php';
162require_once 'application/LinkFilter.php'; 162require_once 'application/LinkFilter.php';
163require_once 'application/LinkUtils.php'; 163require_once 'application/LinkUtils.php';
164require_once 'application/NetscapeBookmarkUtils.php';
164require_once 'application/TimeZone.php'; 165require_once 'application/TimeZone.php';
165require_once 'application/Url.php'; 166require_once 'application/Url.php';
166require_once 'application/Utils.php'; 167require_once 'application/Utils.php';
@@ -1584,44 +1585,36 @@ function renderPage()
1584 } 1585 }
1585 1586
1586 // -------- Export as Netscape Bookmarks HTML file. 1587 // -------- Export as Netscape Bookmarks HTML file.
1587 if ($targetPage == Router::$PAGE_EXPORT) 1588 if ($targetPage == Router::$PAGE_EXPORT) {
1588 { 1589 if (empty($_GET['selection'])) {
1589 if (empty($_GET['what']))
1590 {
1591 $PAGE->assign('linkcount',count($LINKSDB)); 1590 $PAGE->assign('linkcount',count($LINKSDB));
1592 $PAGE->renderPage('export'); 1591 $PAGE->renderPage('export');
1593 exit; 1592 exit;
1594 } 1593 }
1595 $exportWhat=$_GET['what'];
1596 if (!array_intersect(array('all','public','private'),array($exportWhat))) die('What are you trying to export???');
1597 1594
1598 header('Content-Type: text/html; charset=utf-8'); 1595 // export as bookmarks_(all|private|public)_YYYYmmdd_HHMMSS.html
1599 header('Content-disposition: attachment; filename=bookmarks_'.$exportWhat.'_'.strval(date('Ymd_His')).'.html'); 1596 $selection = $_GET['selection'];
1600 $currentdate=date('Y/m/d H:i:s'); 1597 try {
1601 echo <<<HTML 1598 $PAGE->assign(
1602<!DOCTYPE NETSCAPE-Bookmark-file-1> 1599 'links',
1603<!-- This is an automatically generated file. 1600 NetscapeBookmarkUtils::filterAndFormat($LINKSDB, $selection)
1604 It will be read and overwritten. 1601 );
1605 DO NOT EDIT! --> 1602 } catch (Exception $exc) {
1606<!-- Shaarli {$exportWhat} bookmarks export on {$currentdate} --> 1603 header('Content-Type: text/plain; charset=utf-8');
1607<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8"> 1604 echo $exc->getMessage();
1608<TITLE>Bookmarks</TITLE> 1605 exit;
1609<H1>Bookmarks</H1>
1610HTML;
1611 foreach($LINKSDB as $link)
1612 {
1613 if ($exportWhat=='all' ||
1614 ($exportWhat=='private' && $link['private']!=0) ||
1615 ($exportWhat=='public' && $link['private']==0))
1616 {
1617 $date = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $link['linkdate']);
1618 echo '<DT><A HREF="'.$link['url'].'" ADD_DATE="'.$date->getTimestamp().'" PRIVATE="'.$link['private'].'"';
1619 if ($link['tags']!='') echo ' TAGS="'.str_replace(' ',',',$link['tags']).'"';
1620 echo '>'.$link['title']."</A>\n";
1621 if ($link['description']!='') echo '<DD>'.$link['description']."\n";
1622 }
1623 } 1606 }
1624 exit; 1607 $now = new DateTime();
1608 header('Content-Type: text/html; charset=utf-8');
1609 header(
1610 'Content-disposition: attachment; filename=bookmarks_'
1611 .$selection.'_'.$now->format(LinkDB::LINK_DATE_FORMAT).'.html'
1612 );
1613 $PAGE->assign('date', $now->format(DateTime::RFC822));
1614 $PAGE->assign('eol', PHP_EOL);
1615 $PAGE->assign('selection', $selection);
1616 $PAGE->renderPage('export.bookmarks');
1617 exit;
1625 } 1618 }
1626 1619
1627 // -------- User is uploading a file for import 1620 // -------- User is uploading a file for import
diff --git a/tests/NetscapeBookmarkUtilsTest.php b/tests/NetscapeBookmarkUtilsTest.php
new file mode 100644
index 00000000..b7472d92
--- /dev/null
+++ b/tests/NetscapeBookmarkUtilsTest.php
@@ -0,0 +1,104 @@
1<?php
2
3require_once 'application/NetscapeBookmarkUtils.php';
4
5/**
6 * Netscape bookmark import and export
7 */
8class NetscapeBookmarkUtilsTest extends PHPUnit_Framework_TestCase
9{
10 /**
11 * @var string datastore to test write operations
12 */
13 protected static $testDatastore = 'sandbox/datastore.php';
14
15 /**
16 * @var ReferenceLinkDB instance.
17 */
18 protected static $refDb = null;
19
20 /**
21 * @var LinkDB private LinkDB instance.
22 */
23 protected static $linkDb = null;
24
25 /**
26 * Instantiate reference data
27 */
28 public static function setUpBeforeClass()
29 {
30 self::$refDb = new ReferenceLinkDB();
31 self::$refDb->write(self::$testDatastore);
32 self::$linkDb = new LinkDB(self::$testDatastore, true, false);
33 }
34
35 /**
36 * Attempt to export an invalid link selection
37 * @expectedException Exception
38 * @expectedExceptionMessageRegExp /Invalid export selection/
39 */
40 public function testFilterAndFormatInvalid()
41 {
42 NetscapeBookmarkUtils::filterAndFormat(self::$linkDb, 'derp');
43 }
44
45 /**
46 * Prepare all links for export
47 */
48 public function testFilterAndFormatAll()
49 {
50 $links = NetscapeBookmarkUtils::filterAndFormat(self::$linkDb, 'all');
51 $this->assertEquals(self::$refDb->countLinks(), sizeof($links));
52 foreach ($links as $link) {
53 $date = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $link['linkdate']);
54 $this->assertEquals(
55 $date->getTimestamp(),
56 $link['timestamp']
57 );
58 $this->assertEquals(
59 str_replace(' ', ',', $link['tags']),
60 $link['taglist']
61 );
62 }
63 }
64
65 /**
66 * Prepare private links for export
67 */
68 public function testFilterAndFormatPrivate()
69 {
70 $links = NetscapeBookmarkUtils::filterAndFormat(self::$linkDb, 'private');
71 $this->assertEquals(self::$refDb->countPrivateLinks(), sizeof($links));
72 foreach ($links as $link) {
73 $date = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $link['linkdate']);
74 $this->assertEquals(
75 $date->getTimestamp(),
76 $link['timestamp']
77 );
78 $this->assertEquals(
79 str_replace(' ', ',', $link['tags']),
80 $link['taglist']
81 );
82 }
83 }
84
85 /**
86 * Prepare public links for export
87 */
88 public function testFilterAndFormatPublic()
89 {
90 $links = NetscapeBookmarkUtils::filterAndFormat(self::$linkDb, 'public');
91 $this->assertEquals(self::$refDb->countPublicLinks(), sizeof($links));
92 foreach ($links as $link) {
93 $date = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $link['linkdate']);
94 $this->assertEquals(
95 $date->getTimestamp(),
96 $link['timestamp']
97 );
98 $this->assertEquals(
99 str_replace(' ', ',', $link['tags']),
100 $link['taglist']
101 );
102 }
103 }
104}
diff --git a/tpl/export.bookmarks.html b/tpl/export.bookmarks.html
new file mode 100644
index 00000000..da733257
--- /dev/null
+++ b/tpl/export.bookmarks.html
@@ -0,0 +1,10 @@
1<!DOCTYPE NETSCAPE-Bookmark-file-1>
2<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
3<!-- This is an automatically generated file.
4 It will be read and overwritten.
5 Do Not Edit! -->{ignore}The RainTPL loop is formatted to avoid generating extra newlines{/ignore}
6<TITLE>{$pagetitle}</TITLE>
7<H1>Shaarli export of {$selection} bookmarks on {$date}</H1>
8<DL><p>{loop="links"}
9<DT><A HREF="{$value.url}" ADD_DATE="{$value.timestamp}" PRIVATE="{$value.private}" TAGS="{$value.taglist}">{$value.title}</A>{if="$value.description"}{$eol}<DD>{$value.description}{/if}{/loop}
10</DL><p>
diff --git a/tpl/export.html b/tpl/export.html
index 9d101db4..9582627a 100644
--- a/tpl/export.html
+++ b/tpl/export.html
@@ -2,15 +2,21 @@
2<html> 2<html>
3<head>{include="includes"}</head> 3<head>{include="includes"}</head>
4<body> 4<body>
5<div id="pageheader"> 5 <div id="pageheader">
6 {include="page.header"} 6 {include="page.header"}
7 <div id="toolsdiv"> 7 <div id="toolsdiv">
8 <a href="?do=export&amp;what=all"><b>Export all</b> <span>: Export all links</span></a><br><br> 8 <a href="?do=export&amp;selection=all">
9 <a href="?do=export&amp;what=public"><b>Export public</b> <span>: Export public links only</span></a><br><br> 9 <b>Export all</b><span>: Export all links</span>
10 <a href="?do=export&amp;what=private"><b>Export private</b> <span>: Export private links only</span></a> 10 </a><br>
11 <div class="clear"></div> 11 <a href="?do=export&amp;selection=public">
12 </div> 12 <b>Export public</b><span>: Only export public links</span>
13</div> 13 </a><br>
14{include="page.footer"} 14 <a href="?do=export&amp;selection=private">
15 <b>Export private</b><span>: Only export private links</span>
16 </a>
17 <div class="clear"></div>
18 </div>
19 </div>
20 {include="page.footer"}
15</body> 21</body>
16</html> 22</html>