Refactor Netscape bookmark exporting

Relates to https://github.com/shaarli/netscape-bookmark-parser/issues/5

Fixes:
- respect the Netscape bookmark format "specification"

Modifications:
- [application] introduce the NetscapeBookmarkUtils class
- [template] export           - improve formatting, rename export selection parameter
- [template] export.bookmarks - template for Netscape exports
- [tests] bookmark filtering, additional field generation

Signed-off-by: VirtualTam <virtualtam@flibidi.net>
This commit is contained in:
VirtualTam 2016-04-10 17:34:07 +02:00
parent 745304c842
commit cd5327bee8
5 changed files with 202 additions and 42 deletions

View file

@ -0,0 +1,47 @@
<?php
/**
* Utilities to import and export bookmarks using the Netscape format
*/
class NetscapeBookmarkUtils
{
/**
* Filters links and adds Netscape-formatted fields
*
* Added fields:
* - timestamp link addition date, using the Unix epoch format
* - taglist comma-separated tag list
*
* @param LinkDB $linkDb The link datastore
* @param string $selection Which links to export: (all|private|public)
*
* @throws Exception Invalid export selection
*
* @return array The links to be exported, with additional fields
*/
public static function filterAndFormat($linkDb, $selection)
{
// see tpl/export.html for possible values
if (! in_array($selection, array('all','public','private'))) {
throw new Exception('Invalid export selection: "'.$selection.'"');
}
$bookmarkLinks = array();
foreach ($linkDb as $link) {
if ($link['private'] != 0 && $selection == 'public') {
continue;
}
if ($link['private'] == 0 && $selection == 'private') {
continue;
}
$date = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $link['linkdate']);
$link['timestamp'] = $date->getTimestamp();
$link['taglist'] = str_replace(' ', ',', $link['tags']);
$bookmarkLinks[] = $link;
}
return $bookmarkLinks;
}
}

View file

@ -161,6 +161,7 @@ require_once 'application/HttpUtils.php';
require_once 'application/LinkDB.php';
require_once 'application/LinkFilter.php';
require_once 'application/LinkUtils.php';
require_once 'application/NetscapeBookmarkUtils.php';
require_once 'application/TimeZone.php';
require_once 'application/Url.php';
require_once 'application/Utils.php';
@ -1584,43 +1585,35 @@ function renderPage()
}
// -------- Export as Netscape Bookmarks HTML file.
if ($targetPage == Router::$PAGE_EXPORT)
{
if (empty($_GET['what']))
{
if ($targetPage == Router::$PAGE_EXPORT) {
if (empty($_GET['selection'])) {
$PAGE->assign('linkcount',count($LINKSDB));
$PAGE->renderPage('export');
exit;
}
$exportWhat=$_GET['what'];
if (!array_intersect(array('all','public','private'),array($exportWhat))) die('What are you trying to export???');
// export as bookmarks_(all|private|public)_YYYYmmdd_HHMMSS.html
$selection = $_GET['selection'];
try {
$PAGE->assign(
'links',
NetscapeBookmarkUtils::filterAndFormat($LINKSDB, $selection)
);
} catch (Exception $exc) {
header('Content-Type: text/plain; charset=utf-8');
echo $exc->getMessage();
exit;
}
$now = new DateTime();
header('Content-Type: text/html; charset=utf-8');
header('Content-disposition: attachment; filename=bookmarks_'.$exportWhat.'_'.strval(date('Ymd_His')).'.html');
$currentdate=date('Y/m/d H:i:s');
echo <<<HTML
<!DOCTYPE NETSCAPE-Bookmark-file-1>
<!-- This is an automatically generated file.
It will be read and overwritten.
DO NOT EDIT! -->
<!-- Shaarli {$exportWhat} bookmarks export on {$currentdate} -->
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
<TITLE>Bookmarks</TITLE>
<H1>Bookmarks</H1>
HTML;
foreach($LINKSDB as $link)
{
if ($exportWhat=='all' ||
($exportWhat=='private' && $link['private']!=0) ||
($exportWhat=='public' && $link['private']==0))
{
$date = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $link['linkdate']);
echo '<DT><A HREF="'.$link['url'].'" ADD_DATE="'.$date->getTimestamp().'" PRIVATE="'.$link['private'].'"';
if ($link['tags']!='') echo ' TAGS="'.str_replace(' ',',',$link['tags']).'"';
echo '>'.$link['title']."</A>\n";
if ($link['description']!='') echo '<DD>'.$link['description']."\n";
}
}
header(
'Content-disposition: attachment; filename=bookmarks_'
.$selection.'_'.$now->format(LinkDB::LINK_DATE_FORMAT).'.html'
);
$PAGE->assign('date', $now->format(DateTime::RFC822));
$PAGE->assign('eol', PHP_EOL);
$PAGE->assign('selection', $selection);
$PAGE->renderPage('export.bookmarks');
exit;
}

View file

@ -0,0 +1,104 @@
<?php
require_once 'application/NetscapeBookmarkUtils.php';
/**
* Netscape bookmark import and export
*/
class NetscapeBookmarkUtilsTest extends PHPUnit_Framework_TestCase
{
/**
* @var string datastore to test write operations
*/
protected static $testDatastore = 'sandbox/datastore.php';
/**
* @var ReferenceLinkDB instance.
*/
protected static $refDb = null;
/**
* @var LinkDB private LinkDB instance.
*/
protected static $linkDb = null;
/**
* Instantiate reference data
*/
public static function setUpBeforeClass()
{
self::$refDb = new ReferenceLinkDB();
self::$refDb->write(self::$testDatastore);
self::$linkDb = new LinkDB(self::$testDatastore, true, false);
}
/**
* Attempt to export an invalid link selection
* @expectedException Exception
* @expectedExceptionMessageRegExp /Invalid export selection/
*/
public function testFilterAndFormatInvalid()
{
NetscapeBookmarkUtils::filterAndFormat(self::$linkDb, 'derp');
}
/**
* Prepare all links for export
*/
public function testFilterAndFormatAll()
{
$links = NetscapeBookmarkUtils::filterAndFormat(self::$linkDb, 'all');
$this->assertEquals(self::$refDb->countLinks(), sizeof($links));
foreach ($links as $link) {
$date = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $link['linkdate']);
$this->assertEquals(
$date->getTimestamp(),
$link['timestamp']
);
$this->assertEquals(
str_replace(' ', ',', $link['tags']),
$link['taglist']
);
}
}
/**
* Prepare private links for export
*/
public function testFilterAndFormatPrivate()
{
$links = NetscapeBookmarkUtils::filterAndFormat(self::$linkDb, 'private');
$this->assertEquals(self::$refDb->countPrivateLinks(), sizeof($links));
foreach ($links as $link) {
$date = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $link['linkdate']);
$this->assertEquals(
$date->getTimestamp(),
$link['timestamp']
);
$this->assertEquals(
str_replace(' ', ',', $link['tags']),
$link['taglist']
);
}
}
/**
* Prepare public links for export
*/
public function testFilterAndFormatPublic()
{
$links = NetscapeBookmarkUtils::filterAndFormat(self::$linkDb, 'public');
$this->assertEquals(self::$refDb->countPublicLinks(), sizeof($links));
foreach ($links as $link) {
$date = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $link['linkdate']);
$this->assertEquals(
$date->getTimestamp(),
$link['timestamp']
);
$this->assertEquals(
str_replace(' ', ',', $link['tags']),
$link['taglist']
);
}
}
}

10
tpl/export.bookmarks.html Normal file
View file

@ -0,0 +1,10 @@
<!DOCTYPE NETSCAPE-Bookmark-file-1>
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
<!-- This is an automatically generated file.
It will be read and overwritten.
Do Not Edit! -->{ignore}The RainTPL loop is formatted to avoid generating extra newlines{/ignore}
<TITLE>{$pagetitle}</TITLE>
<H1>Shaarli export of {$selection} bookmarks on {$date}</H1>
<DL><p>{loop="links"}
<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}
</DL><p>

View file

@ -2,15 +2,21 @@
<html>
<head>{include="includes"}</head>
<body>
<div id="pageheader">
<div id="pageheader">
{include="page.header"}
<div id="toolsdiv">
<a href="?do=export&amp;what=all"><b>Export all</b> <span>: Export all links</span></a><br><br>
<a href="?do=export&amp;what=public"><b>Export public</b> <span>: Export public links only</span></a><br><br>
<a href="?do=export&amp;what=private"><b>Export private</b> <span>: Export private links only</span></a>
<a href="?do=export&amp;selection=all">
<b>Export all</b><span>: Export all links</span>
</a><br>
<a href="?do=export&amp;selection=public">
<b>Export public</b><span>: Only export public links</span>
</a><br>
<a href="?do=export&amp;selection=private">
<b>Export private</b><span>: Only export private links</span>
</a>
<div class="clear"></div>
</div>
</div>
{include="page.footer"}
</div>
{include="page.footer"}
</body>
</html>