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/LinkDB.php';
require_once 'application/LinkFilter.php'; require_once 'application/LinkFilter.php';
require_once 'application/LinkUtils.php'; require_once 'application/LinkUtils.php';
require_once 'application/NetscapeBookmarkUtils.php';
require_once 'application/TimeZone.php'; require_once 'application/TimeZone.php';
require_once 'application/Url.php'; require_once 'application/Url.php';
require_once 'application/Utils.php'; require_once 'application/Utils.php';
@ -1584,44 +1585,36 @@ function renderPage()
} }
// -------- Export as Netscape Bookmarks HTML file. // -------- Export as Netscape Bookmarks HTML file.
if ($targetPage == Router::$PAGE_EXPORT) if ($targetPage == Router::$PAGE_EXPORT) {
{ if (empty($_GET['selection'])) {
if (empty($_GET['what']))
{
$PAGE->assign('linkcount',count($LINKSDB)); $PAGE->assign('linkcount',count($LINKSDB));
$PAGE->renderPage('export'); $PAGE->renderPage('export');
exit; exit;
} }
$exportWhat=$_GET['what'];
if (!array_intersect(array('all','public','private'),array($exportWhat))) die('What are you trying to export???');
header('Content-Type: text/html; charset=utf-8'); // export as bookmarks_(all|private|public)_YYYYmmdd_HHMMSS.html
header('Content-disposition: attachment; filename=bookmarks_'.$exportWhat.'_'.strval(date('Ymd_His')).'.html'); $selection = $_GET['selection'];
$currentdate=date('Y/m/d H:i:s'); try {
echo <<<HTML $PAGE->assign(
<!DOCTYPE NETSCAPE-Bookmark-file-1> 'links',
<!-- This is an automatically generated file. NetscapeBookmarkUtils::filterAndFormat($LINKSDB, $selection)
It will be read and overwritten. );
DO NOT EDIT! --> } catch (Exception $exc) {
<!-- Shaarli {$exportWhat} bookmarks export on {$currentdate} --> header('Content-Type: text/plain; charset=utf-8');
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8"> echo $exc->getMessage();
<TITLE>Bookmarks</TITLE> exit;
<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";
}
} }
exit; $now = new DateTime();
header('Content-Type: text/html; charset=utf-8');
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;
} }
// -------- User is uploading a file for import // -------- User is uploading a file for import

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