aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorVirtualTam <virtualtam@flibidi.net>2016-07-28 22:54:33 +0200
committerArthurHoaro <arthur@hoa.ro>2016-11-05 14:29:52 +0100
commitd6d8558723ed20a49cd602cb0adbc7f112254bd8 (patch)
tree071e3b9ff4c3d167dedea291cfcd6cd3c9d5d44e
parent05d8c485ec4df9a0016fa19d75f279795e23fc7b (diff)
downloadShaarli-d6d8558723ed20a49cd602cb0adbc7f112254bd8.tar.gz
Shaarli-d6d8558723ed20a49cd602cb0adbc7f112254bd8.tar.zst
Shaarli-d6d8558723ed20a49cd602cb0adbc7f112254bd8.zip
Refactor bookmark import using a generic Netscape parser
Relates to #607 Relates to #608 Relates to #493 (abandoned) Additions: - use Composer's autoload to load 3rd-party dependencies under vendor/ Modifications: - [import] replace the current parser with a generic, stable parser - move code to application/NetscapeBookmarkUtils - improve status report after parsing - [router] use the same endpoint for both bookmark upload and import dialog - [template] update bookmark import options - allow adding tags to all imported links - allow selecting the visibility (privacy) of imported links - [tests] ensure bookmarks are properly parsed and imported in the LinkDB - reuse reference input from the parser's test data See: - https://github.com/shaarli/netscape-bookmark-parser - https://getcomposer.org/doc/01-basic-usage.md#autoloading Signed-off-by: VirtualTam <virtualtam@flibidi.net>
-rw-r--r--application/NetscapeBookmarkUtils.php142
-rw-r--r--index.php139
-rw-r--r--tests/NetscapeBookmarkUtils/BookmarkExportTest.php (renamed from tests/NetscapeBookmarkUtilsTest.php)4
-rw-r--r--tests/NetscapeBookmarkUtils/BookmarkImportTest.php518
-rw-r--r--tests/NetscapeBookmarkUtils/input/empty.htm0
-rw-r--r--tests/NetscapeBookmarkUtils/input/netscape_basic.htm11
-rw-r--r--tests/NetscapeBookmarkUtils/input/netscape_nested.htm31
-rw-r--r--tests/NetscapeBookmarkUtils/input/no_doctype.htm7
-rw-r--r--tests/NetscapeBookmarkUtils/input/same_date.htm11
-rw-r--r--tpl/import.html38
10 files changed, 779 insertions, 122 deletions
diff --git a/application/NetscapeBookmarkUtils.php b/application/NetscapeBookmarkUtils.php
index fdbb0ad7..b99a432e 100644
--- a/application/NetscapeBookmarkUtils.php
+++ b/application/NetscapeBookmarkUtils.php
@@ -51,4 +51,146 @@ class NetscapeBookmarkUtils
51 51
52 return $bookmarkLinks; 52 return $bookmarkLinks;
53 } 53 }
54
55 /**
56 * Generates an import status summary
57 *
58 * @param string $filename name of the file to import
59 * @param int $filesize size of the file to import
60 * @param int $importCount how many links were imported
61 * @param int $overwriteCount how many links were overwritten
62 * @param int $skipCount how many links were skipped
63 *
64 * @return string Summary of the bookmark import status
65 */
66 private static function importStatus(
67 $filename,
68 $filesize,
69 $importCount=0,
70 $overwriteCount=0,
71 $skipCount=0
72 )
73 {
74 $status = 'File '.$filename.' ('.$filesize.' bytes) ';
75 if ($importCount == 0 && $overwriteCount == 0 && $skipCount == 0) {
76 $status .= 'has an unknown file format. Nothing was imported.';
77 } else {
78 $status .= 'was successfully processed: '.$importCount.' links imported, ';
79 $status .= $overwriteCount.' links overwritten, ';
80 $status .= $skipCount.' links skipped.';
81 }
82 return $status;
83 }
84
85 /**
86 * Imports Web bookmarks from an uploaded Netscape bookmark dump
87 *
88 * @param array $post Server $_POST parameters
89 * @param array $file Server $_FILES parameters
90 * @param LinkDB $linkDb Loaded LinkDB instance
91 * @param string $pagecache Page cache
92 *
93 * @return string Summary of the bookmark import status
94 */
95 public static function import($post, $files, $linkDb, $pagecache)
96 {
97 $filename = $files['filetoupload']['name'];
98 $filesize = $files['filetoupload']['size'];
99 $data = file_get_contents($files['filetoupload']['tmp_name']);
100
101 // Sniff file type
102 if (! startsWith($data, '<!DOCTYPE NETSCAPE-Bookmark-file-1>')) {
103 return self::importStatus($filename, $filesize);
104 }
105
106 // Overwrite existing links?
107 $overwrite = ! empty($post['overwrite']);
108
109 // Add tags to all imported links?
110 if (empty($post['default_tags'])) {
111 $defaultTags = array();
112 } else {
113 $defaultTags = preg_split(
114 '/[\s,]+/',
115 escape($post['default_tags'])
116 );
117 }
118
119 // links are imported as public by default
120 $defaultPrivacy = 0;
121
122 $parser = new NetscapeBookmarkParser(
123 true, // nested tag support
124 $defaultTags, // additional user-specified tags
125 strval(1 - $defaultPrivacy) // defaultPub = 1 - defaultPrivacy
126 );
127 $bookmarks = $parser->parseString($data);
128
129 $importCount = 0;
130 $overwriteCount = 0;
131 $skipCount = 0;
132
133 foreach ($bookmarks as $bkm) {
134 $private = $defaultPrivacy;
135 if (empty($post['privacy']) || $post['privacy'] == 'default') {
136 // use value from the imported file
137 $private = $bkm['pub'] == '1' ? 0 : 1;
138 } else if ($post['privacy'] == 'private') {
139 // all imported links are private
140 $private = 1;
141 } else if ($post['privacy'] == 'public') {
142 // all imported links are public
143 $private = 0;
144 }
145
146 $newLink = array(
147 'title' => $bkm['title'],
148 'url' => $bkm['uri'],
149 'description' => $bkm['note'],
150 'private' => $private,
151 'linkdate'=> '',
152 'tags' => $bkm['tags']
153 );
154
155 $existingLink = $linkDb->getLinkFromUrl($bkm['uri']);
156
157 if ($existingLink !== false) {
158 if ($overwrite === false) {
159 // Do not overwrite an existing link
160 $skipCount++;
161 continue;
162 }
163
164 // Overwrite an existing link, keep its date
165 $newLink['linkdate'] = $existingLink['linkdate'];
166 $linkDb[$existingLink['linkdate']] = $newLink;
167 $importCount++;
168 $overwriteCount++;
169 continue;
170 }
171
172 // Add a new link
173 $newLinkDate = new DateTime('@'.strval($bkm['time']));
174 while (!empty($linkDb[$newLinkDate->format(LinkDB::LINK_DATE_FORMAT)])) {
175 // Ensure the date/time is not already used
176 // - this hack is necessary as the date/time acts as a primary key
177 // - apply 1 second increments until an unused index is found
178 // See https://github.com/shaarli/Shaarli/issues/351
179 $newLinkDate->add(new DateInterval('PT1S'));
180 }
181 $linkDbDate = $newLinkDate->format(LinkDB::LINK_DATE_FORMAT);
182 $newLink['linkdate'] = $linkDbDate;
183 $linkDb[$linkDbDate] = $newLink;
184 $importCount++;
185 }
186
187 $linkDb->savedb($pagecache);
188 return self::importStatus(
189 $filename,
190 $filesize,
191 $importCount,
192 $overwriteCount,
193 $skipCount
194 );
195 }
54} 196}
diff --git a/index.php b/index.php
index 9ae798ba..1f148d78 100644
--- a/index.php
+++ b/index.php
@@ -44,6 +44,10 @@ error_reporting(E_ALL^E_WARNING);
44//error_reporting(-1); 44//error_reporting(-1);
45 45
46 46
47// 3rd-party libraries
48require_once 'inc/rain.tpl.class.php';
49require_once __DIR__ . '/vendor/autoload.php';
50
47// Shaarli library 51// Shaarli library
48require_once 'application/ApplicationUtils.php'; 52require_once 'application/ApplicationUtils.php';
49require_once 'application/Cache.php'; 53require_once 'application/Cache.php';
@@ -65,7 +69,6 @@ require_once 'application/Utils.php';
65require_once 'application/PluginManager.php'; 69require_once 'application/PluginManager.php';
66require_once 'application/Router.php'; 70require_once 'application/Router.php';
67require_once 'application/Updater.php'; 71require_once 'application/Updater.php';
68require_once 'inc/rain.tpl.class.php';
69 72
70// Ensure the PHP version is supported 73// Ensure the PHP version is supported
71try { 74try {
@@ -1468,26 +1471,37 @@ function renderPage($conf, $pluginManager)
1468 exit; 1471 exit;
1469 } 1472 }
1470 1473
1471 // -------- User is uploading a file for import 1474 if ($targetPage == Router::$PAGE_IMPORT) {
1472 if (isset($_SERVER['QUERY_STRING']) && startsWith($_SERVER['QUERY_STRING'], 'do=upload')) 1475 // Upload a Netscape bookmark dump to import its contents
1473 { 1476
1474 // If file is too big, some form field may be missing. 1477 if (! isset($_POST['token']) || ! isset($_FILES['filetoupload'])) {
1475 if (!isset($_POST['token']) || (!isset($_FILES)) || (isset($_FILES['filetoupload']['size']) && $_FILES['filetoupload']['size']==0)) 1478 // Show import dialog
1476 { 1479 $PAGE->assign('maxfilesize', getMaxFileSize());
1477 $returnurl = ( empty($_SERVER['HTTP_REFERER']) ? '?' : $_SERVER['HTTP_REFERER'] ); 1480 $PAGE->renderPage('import');
1478 echo '<script>alert("The file you are trying to upload is probably bigger than what this webserver can accept ('.getMaxFileSize().' bytes). Please upload in smaller chunks.");document.location=\''.escape($returnurl).'\';</script>';
1479 exit; 1481 exit;
1480 } 1482 }
1481 if (!tokenOk($_POST['token'])) die('Wrong token.');
1482 importFile($LINKSDB);
1483 exit;
1484 }
1485 1483
1486 // -------- Show upload/import dialog: 1484 // Import bookmarks from an uploaded file
1487 if ($targetPage == Router::$PAGE_IMPORT) 1485 if (isset($_FILES['filetoupload']['size']) && $_FILES['filetoupload']['size'] == 0) {
1488 { 1486 // The file is too big or some form field may be missing.
1489 $PAGE->assign('maxfilesize',getMaxFileSize()); 1487 echo '<script>alert("The file you are trying to upload is probably'
1490 $PAGE->renderPage('import'); 1488 .' bigger than what this webserver can accept ('
1489 .getMaxFileSize().' bytes).'
1490 .' Please upload in smaller chunks.");document.location=\'?do='
1491 .Router::$PAGE_IMPORT .'\';</script>';
1492 exit;
1493 }
1494 if (! tokenOk($_POST['token'])) {
1495 die('Wrong token.');
1496 }
1497 $status = NetscapeBookmarkUtils::import(
1498 $_POST,
1499 $_FILES,
1500 $LINKSDB,
1501 $conf->get('resource.page_cache')
1502 );
1503 echo '<script>alert("'.$status.'");document.location=\'?do='
1504 .Router::$PAGE_IMPORT .'\';</script>';
1491 exit; 1505 exit;
1492 } 1506 }
1493 1507
@@ -1545,95 +1559,6 @@ function renderPage($conf, $pluginManager)
1545} 1559}
1546 1560
1547/** 1561/**
1548 * Process the import file form.
1549 *
1550 * @param LinkDB $LINKSDB Loaded LinkDB instance.
1551 * @param ConfigManager $conf Configuration Manager instance.
1552 */
1553function importFile($LINKSDB, $conf)
1554{
1555 if (!isLoggedIn()) { die('Not allowed.'); }
1556
1557 $filename=$_FILES['filetoupload']['name'];
1558 $filesize=$_FILES['filetoupload']['size'];
1559 $data=file_get_contents($_FILES['filetoupload']['tmp_name']);
1560 $private = (empty($_POST['private']) ? 0 : 1); // Should the links be imported as private?
1561 $overwrite = !empty($_POST['overwrite']) ; // Should the imported links overwrite existing ones?
1562 $import_count=0;
1563
1564 // Sniff file type:
1565 $type='unknown';
1566 if (startsWith($data,'<!DOCTYPE NETSCAPE-Bookmark-file-1>')) $type='netscape'; // Netscape bookmark file (aka Firefox).
1567
1568 // Then import the bookmarks.
1569 if ($type=='netscape')
1570 {
1571 // This is a standard Netscape-style bookmark file.
1572 // This format is supported by all browsers (except IE, of course), also Delicious, Diigo and others.
1573 foreach(explode('<DT>',$data) as $html) // explode is very fast
1574 {
1575 $link = array('linkdate'=>'','title'=>'','url'=>'','description'=>'','tags'=>'','private'=>0);
1576 $d = explode('<DD>',$html);
1577 if (startsWith($d[0], '<A '))
1578 {
1579 $link['description'] = (isset($d[1]) ? html_entity_decode(trim($d[1]),ENT_QUOTES,'UTF-8') : ''); // Get description (optional)
1580 preg_match('!<A .*?>(.*?)</A>!i',$d[0],$matches); $link['title'] = (isset($matches[1]) ? trim($matches[1]) : ''); // Get title
1581 $link['title'] = html_entity_decode($link['title'],ENT_QUOTES,'UTF-8');
1582 preg_match_all('! ([A-Z_]+)=\"(.*?)"!i',$html,$matches,PREG_SET_ORDER); // Get all other attributes
1583 $raw_add_date=0;
1584 foreach($matches as $m)
1585 {
1586 $attr=$m[1]; $value=$m[2];
1587 if ($attr=='HREF') $link['url']=html_entity_decode($value,ENT_QUOTES,'UTF-8');
1588 elseif ($attr=='ADD_DATE')
1589 {
1590 $raw_add_date=intval($value);
1591 if ($raw_add_date>30000000000) $raw_add_date/=1000; //If larger than year 2920, then was likely stored in milliseconds instead of seconds
1592 }
1593 elseif ($attr=='PRIVATE') $link['private']=($value=='0'?0:1);
1594 elseif ($attr=='TAGS') $link['tags']=html_entity_decode(str_replace(',',' ',$value),ENT_QUOTES,'UTF-8');
1595 }
1596 if ($link['url']!='')
1597 {
1598 if ($private==1) $link['private']=1;
1599 $dblink = $LINKSDB->getLinkFromUrl($link['url']); // See if the link is already in database.
1600 if ($dblink==false)
1601 { // Link not in database, let's import it...
1602 if (empty($raw_add_date)) $raw_add_date=time(); // In case of shitty bookmark file with no ADD_DATE
1603
1604 // Make sure date/time is not already used by another link.
1605 // (Some bookmark files have several different links with the same ADD_DATE)
1606 // We increment date by 1 second until we find a date which is not used in DB.
1607 // (so that links that have the same date/time are more or less kept grouped by date, but do not conflict.)
1608 while (!empty($LINKSDB[date('Ymd_His',$raw_add_date)])) { $raw_add_date++; }// Yes, I know it's ugly.
1609 $link['linkdate']=date('Ymd_His',$raw_add_date);
1610 $LINKSDB[$link['linkdate']] = $link;
1611 $import_count++;
1612 }
1613 else // Link already present in database.
1614 {
1615 if ($overwrite)
1616 { // If overwrite is required, we import link data, except date/time.
1617 $link['linkdate']=$dblink['linkdate'];
1618 $LINKSDB[$link['linkdate']] = $link;
1619 $import_count++;
1620 }
1621 }
1622
1623 }
1624 }
1625 }
1626 $LINKSDB->savedb($conf->get('resource.page_cache'));
1627
1628 echo '<script>alert("File '.json_encode($filename).' ('.$filesize.' bytes) was successfully processed: '.$import_count.' links imported.");document.location=\'?\';</script>';
1629 }
1630 else
1631 {
1632 echo '<script>alert("File '.json_encode($filename).' ('.$filesize.' bytes) has an unknown file format. Nothing was imported.");document.location=\'?\';</script>';
1633 }
1634}
1635
1636/**
1637 * Template for the list of links (<div id="linklist">) 1562 * Template for the list of links (<div id="linklist">)
1638 * This function fills all the necessary fields in the $PAGE for the template 'linklist.html' 1563 * This function fills all the necessary fields in the $PAGE for the template 'linklist.html'
1639 * 1564 *
diff --git a/tests/NetscapeBookmarkUtilsTest.php b/tests/NetscapeBookmarkUtils/BookmarkExportTest.php
index 41e6d84c..cc54ab9f 100644
--- a/tests/NetscapeBookmarkUtilsTest.php
+++ b/tests/NetscapeBookmarkUtils/BookmarkExportTest.php
@@ -3,9 +3,9 @@
3require_once 'application/NetscapeBookmarkUtils.php'; 3require_once 'application/NetscapeBookmarkUtils.php';
4 4
5/** 5/**
6 * Netscape bookmark import and export 6 * Netscape bookmark export
7 */ 7 */
8class NetscapeBookmarkUtilsTest extends PHPUnit_Framework_TestCase 8class BookmarkExportTest extends PHPUnit_Framework_TestCase
9{ 9{
10 /** 10 /**
11 * @var string datastore to test write operations 11 * @var string datastore to test write operations
diff --git a/tests/NetscapeBookmarkUtils/BookmarkImportTest.php b/tests/NetscapeBookmarkUtils/BookmarkImportTest.php
new file mode 100644
index 00000000..2d4e7557
--- /dev/null
+++ b/tests/NetscapeBookmarkUtils/BookmarkImportTest.php
@@ -0,0 +1,518 @@
1<?php
2
3require_once 'application/NetscapeBookmarkUtils.php';
4
5
6/**
7 * Utility function to load a file's metadata in a $_FILES-like array
8 *
9 * @param string $filename Basename of the file
10 *
11 * @return array A $_FILES-like array
12 */
13function file2array($filename)
14{
15 return array(
16 'filetoupload' => array(
17 'name' => $filename,
18 'tmp_name' => __DIR__ . '/input/' . $filename,
19 'size' => filesize(__DIR__ . '/input/' . $filename)
20 )
21 );
22}
23
24
25/**
26 * Netscape bookmark import
27 */
28class BookmarkImportTest extends PHPUnit_Framework_TestCase
29{
30 /**
31 * @var string datastore to test write operations
32 */
33 protected static $testDatastore = 'sandbox/datastore.php';
34
35 /**
36 * @var LinkDB private LinkDB instance
37 */
38 protected $linkDb = null;
39
40 /**
41 * @var string Dummy page cache
42 */
43 protected $pagecache = 'tests';
44
45 /**
46 * Resets test data before each test
47 */
48 protected function setUp()
49 {
50 if (file_exists(self::$testDatastore)) {
51 unlink(self::$testDatastore);
52 }
53 // start with an empty datastore
54 file_put_contents(self::$testDatastore, '<?php /* S7QysKquBQA= */ ?>');
55 $this->linkDb = new LinkDB(self::$testDatastore, true, false);
56 }
57
58 /**
59 * Attempt to import bookmarks from an empty file
60 */
61 public function testImportEmptyData()
62 {
63 $files = file2array('empty.htm');
64 $this->assertEquals(
65 'File empty.htm (0 bytes) has an unknown file format.'
66 .' Nothing was imported.',
67 NetscapeBookmarkUtils::import(NULL, $files, NULL, NULL)
68 );
69 $this->assertEquals(0, count($this->linkDb));
70 }
71
72 /**
73 * Attempt to import bookmarks from a file with no Doctype
74 */
75 public function testImportNoDoctype()
76 {
77 $files = file2array('no_doctype.htm');
78 $this->assertEquals(
79 'File no_doctype.htm (350 bytes) has an unknown file format. Nothing was imported.',
80 NetscapeBookmarkUtils::import(NULL, $files, NULL, NULL)
81 );
82 $this->assertEquals(0, count($this->linkDb));
83 }
84
85 /**
86 * Import bookmarks nested in a folder hierarchy
87 */
88 public function testImportNested()
89 {
90 $files = file2array('netscape_nested.htm');
91 $this->assertEquals(
92 'File netscape_nested.htm (1337 bytes) was successfully processed:'
93 .' 8 links imported, 0 links overwritten, 0 links skipped.',
94 NetscapeBookmarkUtils::import(array(), $files, $this->linkDb, $this->pagecache)
95 );
96 $this->assertEquals(8, count($this->linkDb));
97 $this->assertEquals(2, count_private($this->linkDb));
98
99 $this->assertEquals(
100 array(
101 'linkdate' => '20160225_205541',
102 'title' => 'Nested 1',
103 'url' => 'http://nest.ed/1',
104 'description' => '',
105 'private' => 0,
106 'tags' => 'tag1 tag2'
107 ),
108 $this->linkDb->getLinkFromUrl('http://nest.ed/1')
109 );
110 $this->assertEquals(
111 array(
112 'linkdate' => '20160225_205542',
113 'title' => 'Nested 1-1',
114 'url' => 'http://nest.ed/1-1',
115 'description' => '',
116 'private' => 0,
117 'tags' => 'folder1 tag1 tag2'
118 ),
119 $this->linkDb->getLinkFromUrl('http://nest.ed/1-1')
120 );
121 $this->assertEquals(
122 array(
123 'linkdate' => '20160225_205547',
124 'title' => 'Nested 1-2',
125 'url' => 'http://nest.ed/1-2',
126 'description' => '',
127 'private' => 0,
128 'tags' => 'folder1 tag3 tag4'
129 ),
130 $this->linkDb->getLinkFromUrl('http://nest.ed/1-2')
131 );
132 $this->assertEquals(
133 array(
134 'linkdate' => '20160202_172222',
135 'title' => 'Nested 2-1',
136 'url' => 'http://nest.ed/2-1',
137 'description' => 'First link of the second section',
138 'private' => 1,
139 'tags' => 'folder2'
140 ),
141 $this->linkDb->getLinkFromUrl('http://nest.ed/2-1')
142 );
143 $this->assertEquals(
144 array(
145 'linkdate' => '20160119_200227',
146 'title' => 'Nested 2-2',
147 'url' => 'http://nest.ed/2-2',
148 'description' => 'Second link of the second section',
149 'private' => 1,
150 'tags' => 'folder2'
151 ),
152 $this->linkDb->getLinkFromUrl('http://nest.ed/2-2')
153 );
154 $this->assertEquals(
155 array(
156 'linkdate' => '20160202_172223',
157 'title' => 'Nested 3-1',
158 'url' => 'http://nest.ed/3-1',
159 'description' => '',
160 'private' => 0,
161 'tags' => 'folder3 folder3-1 tag3'
162 ),
163 $this->linkDb->getLinkFromUrl('http://nest.ed/3-1')
164 );
165 $this->assertEquals(
166 array(
167 'linkdate' => '20160119_200228',
168 'title' => 'Nested 3-2',
169 'url' => 'http://nest.ed/3-2',
170 'description' => '',
171 'private' => 0,
172 'tags' => 'folder3 folder3-1'
173 ),
174 $this->linkDb->getLinkFromUrl('http://nest.ed/3-2')
175 );
176 $this->assertEquals(
177 array(
178 'linkdate' => '20160229_081541',
179 'title' => 'Nested 2',
180 'url' => 'http://nest.ed/2',
181 'description' => '',
182 'private' => 0,
183 'tags' => 'tag4'
184 ),
185 $this->linkDb->getLinkFromUrl('http://nest.ed/2')
186 );
187 }
188
189 /**
190 * Import bookmarks with the default privacy setting (reuse from file)
191 *
192 * The $_POST array is not set.
193 */
194 public function testImportDefaultPrivacyNoPost()
195 {
196 $files = file2array('netscape_basic.htm');
197 $this->assertEquals(
198 'File netscape_basic.htm (482 bytes) was successfully processed:'
199 .' 2 links imported, 0 links overwritten, 0 links skipped.',
200 NetscapeBookmarkUtils::import(array(), $files, $this->linkDb, $this->pagecache)
201 );
202 $this->assertEquals(2, count($this->linkDb));
203 $this->assertEquals(1, count_private($this->linkDb));
204
205 $this->assertEquals(
206 array(
207 'linkdate' => '20001010_105536',
208 'title' => 'Secret stuff',
209 'url' => 'https://private.tld',
210 'description' => "Super-secret stuff you're not supposed to know about",
211 'private' => 1,
212 'tags' => 'private secret'
213 ),
214 $this->linkDb->getLinkFromUrl('https://private.tld')
215 );
216 $this->assertEquals(
217 array(
218 'linkdate' => '20160225_205548',
219 'title' => 'Public stuff',
220 'url' => 'http://public.tld',
221 'description' => '',
222 'private' => 0,
223 'tags' => 'public hello world'
224 ),
225 $this->linkDb->getLinkFromUrl('http://public.tld')
226 );
227 }
228
229 /**
230 * Import bookmarks with the default privacy setting (reuse from file)
231 */
232 public function testImportKeepPrivacy()
233 {
234 $post = array('privacy' => 'default');
235 $files = file2array('netscape_basic.htm');
236 $this->assertEquals(
237 'File netscape_basic.htm (482 bytes) was successfully processed:'
238 .' 2 links imported, 0 links overwritten, 0 links skipped.',
239 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->pagecache)
240 );
241 $this->assertEquals(2, count($this->linkDb));
242 $this->assertEquals(1, count_private($this->linkDb));
243
244 $this->assertEquals(
245 array(
246 'linkdate' => '20001010_105536',
247 'title' => 'Secret stuff',
248 'url' => 'https://private.tld',
249 'description' => "Super-secret stuff you're not supposed to know about",
250 'private' => 1,
251 'tags' => 'private secret'
252 ),
253 $this->linkDb->getLinkFromUrl('https://private.tld')
254 );
255 $this->assertEquals(
256 array(
257 'linkdate' => '20160225_205548',
258 'title' => 'Public stuff',
259 'url' => 'http://public.tld',
260 'description' => '',
261 'private' => 0,
262 'tags' => 'public hello world'
263 ),
264 $this->linkDb->getLinkFromUrl('http://public.tld')
265 );
266 }
267
268 /**
269 * Import links as public
270 */
271 public function testImportAsPublic()
272 {
273 $post = array('privacy' => 'public');
274 $files = file2array('netscape_basic.htm');
275 $this->assertEquals(
276 'File netscape_basic.htm (482 bytes) was successfully processed:'
277 .' 2 links imported, 0 links overwritten, 0 links skipped.',
278 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->pagecache)
279 );
280 $this->assertEquals(2, count($this->linkDb));
281 $this->assertEquals(0, count_private($this->linkDb));
282 $this->assertEquals(
283 0,
284 $this->linkDb['20001010_105536']['private']
285 );
286 $this->assertEquals(
287 0,
288 $this->linkDb['20160225_205548']['private']
289 );
290 }
291
292 /**
293 * Import links as private
294 */
295 public function testImportAsPrivate()
296 {
297 $post = array('privacy' => 'private');
298 $files = file2array('netscape_basic.htm');
299 $this->assertEquals(
300 'File netscape_basic.htm (482 bytes) was successfully processed:'
301 .' 2 links imported, 0 links overwritten, 0 links skipped.',
302 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->pagecache)
303 );
304 $this->assertEquals(2, count($this->linkDb));
305 $this->assertEquals(2, count_private($this->linkDb));
306 $this->assertEquals(
307 1,
308 $this->linkDb['20001010_105536']['private']
309 );
310 $this->assertEquals(
311 1,
312 $this->linkDb['20160225_205548']['private']
313 );
314 }
315
316 /**
317 * Overwrite private links so they become public
318 */
319 public function testOverwriteAsPublic()
320 {
321 $files = file2array('netscape_basic.htm');
322
323 // import links as private
324 $post = array('privacy' => 'private');
325 $this->assertEquals(
326 'File netscape_basic.htm (482 bytes) was successfully processed:'
327 .' 2 links imported, 0 links overwritten, 0 links skipped.',
328 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->pagecache)
329 );
330 $this->assertEquals(2, count($this->linkDb));
331 $this->assertEquals(2, count_private($this->linkDb));
332 $this->assertEquals(
333 1,
334 $this->linkDb['20001010_105536']['private']
335 );
336 $this->assertEquals(
337 1,
338 $this->linkDb['20160225_205548']['private']
339 );
340
341 // re-import as public, enable overwriting
342 $post = array(
343 'privacy' => 'public',
344 'overwrite' => 'true'
345 );
346 $this->assertEquals(
347 'File netscape_basic.htm (482 bytes) was successfully processed:'
348 .' 2 links imported, 2 links overwritten, 0 links skipped.',
349 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->pagecache)
350 );
351 $this->assertEquals(2, count($this->linkDb));
352 $this->assertEquals(0, count_private($this->linkDb));
353 $this->assertEquals(
354 0,
355 $this->linkDb['20001010_105536']['private']
356 );
357 $this->assertEquals(
358 0,
359 $this->linkDb['20160225_205548']['private']
360 );
361 }
362
363 /**
364 * Overwrite public links so they become private
365 */
366 public function testOverwriteAsPrivate()
367 {
368 $files = file2array('netscape_basic.htm');
369
370 // import links as public
371 $post = array('privacy' => 'public');
372 $this->assertEquals(
373 'File netscape_basic.htm (482 bytes) was successfully processed:'
374 .' 2 links imported, 0 links overwritten, 0 links skipped.',
375 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->pagecache)
376 );
377 $this->assertEquals(2, count($this->linkDb));
378 $this->assertEquals(0, count_private($this->linkDb));
379 $this->assertEquals(
380 0,
381 $this->linkDb['20001010_105536']['private']
382 );
383 $this->assertEquals(
384 0,
385 $this->linkDb['20160225_205548']['private']
386 );
387
388 // re-import as private, enable overwriting
389 $post = array(
390 'privacy' => 'private',
391 'overwrite' => 'true'
392 );
393 $this->assertEquals(
394 'File netscape_basic.htm (482 bytes) was successfully processed:'
395 .' 2 links imported, 2 links overwritten, 0 links skipped.',
396 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->pagecache)
397 );
398 $this->assertEquals(2, count($this->linkDb));
399 $this->assertEquals(2, count_private($this->linkDb));
400 $this->assertEquals(
401 1,
402 $this->linkDb['20001010_105536']['private']
403 );
404 $this->assertEquals(
405 1,
406 $this->linkDb['20160225_205548']['private']
407 );
408 }
409
410 /**
411 * Attept to import the same links twice without enabling overwriting
412 */
413 public function testSkipOverwrite()
414 {
415 $post = array('privacy' => 'public');
416 $files = file2array('netscape_basic.htm');
417 $this->assertEquals(
418 'File netscape_basic.htm (482 bytes) was successfully processed:'
419 .' 2 links imported, 0 links overwritten, 0 links skipped.',
420 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->pagecache)
421 );
422 $this->assertEquals(2, count($this->linkDb));
423 $this->assertEquals(0, count_private($this->linkDb));
424
425 // re-import as private, DO NOT enable overwriting
426 $post = array('privacy' => 'private');
427 $this->assertEquals(
428 'File netscape_basic.htm (482 bytes) was successfully processed:'
429 .' 0 links imported, 0 links overwritten, 2 links skipped.',
430 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->pagecache)
431 );
432 $this->assertEquals(2, count($this->linkDb));
433 $this->assertEquals(0, count_private($this->linkDb));
434 }
435
436 /**
437 * Add user-specified tags to all imported bookmarks
438 */
439 public function testSetDefaultTags()
440 {
441 $post = array(
442 'privacy' => 'public',
443 'default_tags' => 'tag1,tag2 tag3'
444 );
445 $files = file2array('netscape_basic.htm');
446 $this->assertEquals(
447 'File netscape_basic.htm (482 bytes) was successfully processed:'
448 .' 2 links imported, 0 links overwritten, 0 links skipped.',
449 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->pagecache)
450 );
451 $this->assertEquals(2, count($this->linkDb));
452 $this->assertEquals(0, count_private($this->linkDb));
453 $this->assertEquals(
454 'tag1 tag2 tag3 private secret',
455 $this->linkDb['20001010_105536']['tags']
456 );
457 $this->assertEquals(
458 'tag1 tag2 tag3 public hello world',
459 $this->linkDb['20160225_205548']['tags']
460 );
461 }
462
463 /**
464 * The user-specified tags contain characters to be escaped
465 */
466 public function testSanitizeDefaultTags()
467 {
468 $post = array(
469 'privacy' => 'public',
470 'default_tags' => 'tag1&,tag2 "tag3"'
471 );
472 $files = file2array('netscape_basic.htm');
473 $this->assertEquals(
474 'File netscape_basic.htm (482 bytes) was successfully processed:'
475 .' 2 links imported, 0 links overwritten, 0 links skipped.',
476 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->pagecache)
477 );
478 $this->assertEquals(2, count($this->linkDb));
479 $this->assertEquals(0, count_private($this->linkDb));
480 $this->assertEquals(
481 'tag1&amp; tag2 &quot;tag3&quot; private secret',
482 $this->linkDb['20001010_105536']['tags']
483 );
484 $this->assertEquals(
485 'tag1&amp; tag2 &quot;tag3&quot; public hello world',
486 $this->linkDb['20160225_205548']['tags']
487 );
488 }
489
490 /**
491 * Ensure each imported bookmark has a unique linkdate
492 *
493 * See https://github.com/shaarli/Shaarli/issues/351
494 */
495 public function testImportSameDate()
496 {
497 $files = file2array('same_date.htm');
498 $this->assertEquals(
499 'File same_date.htm (453 bytes) was successfully processed:'
500 .' 3 links imported, 0 links overwritten, 0 links skipped.',
501 NetscapeBookmarkUtils::import(array(), $files, $this->linkDb, $this->pagecache)
502 );
503 $this->assertEquals(3, count($this->linkDb));
504 $this->assertEquals(0, count_private($this->linkDb));
505 $this->assertEquals(
506 '20160225_205548',
507 $this->linkDb['20160225_205548']['linkdate']
508 );
509 $this->assertEquals(
510 '20160225_205549',
511 $this->linkDb['20160225_205549']['linkdate']
512 );
513 $this->assertEquals(
514 '20160225_205550',
515 $this->linkDb['20160225_205550']['linkdate']
516 );
517 }
518}
diff --git a/tests/NetscapeBookmarkUtils/input/empty.htm b/tests/NetscapeBookmarkUtils/input/empty.htm
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/tests/NetscapeBookmarkUtils/input/empty.htm
diff --git a/tests/NetscapeBookmarkUtils/input/netscape_basic.htm b/tests/NetscapeBookmarkUtils/input/netscape_basic.htm
new file mode 100644
index 00000000..affe0cf8
--- /dev/null
+++ b/tests/NetscapeBookmarkUtils/input/netscape_basic.htm
@@ -0,0 +1,11 @@
1<!DOCTYPE NETSCAPE-Bookmark-file-1>
2<!-- This is an automatically generated file.
3It will be read and overwritten.
4Do Not Edit! -->
5<TITLE>Bookmarks</TITLE>
6<H1>Bookmarks</H1>
7<DL><p>
8<DT><A HREF="https://private.tld" ADD_DATE="10/Oct/2000:13:55:36 +0300" PRIVATE="1" TAGS="private secret">Secret stuff</A>
9<DD>Super-secret stuff you're not supposed to know about
10<DT><A HREF="http://public.tld" ADD_DATE="1456433748" PRIVATE="0" TAGS="public hello world">Public stuff</A>
11</DL><p>
diff --git a/tests/NetscapeBookmarkUtils/input/netscape_nested.htm b/tests/NetscapeBookmarkUtils/input/netscape_nested.htm
new file mode 100644
index 00000000..b486fe18
--- /dev/null
+++ b/tests/NetscapeBookmarkUtils/input/netscape_nested.htm
@@ -0,0 +1,31 @@
1<!DOCTYPE NETSCAPE-Bookmark-file-1>
2<!-- This is an automatically generated file.
3It will be read and overwritten.
4Do Not Edit! -->
5<TITLE>Bookmarks</TITLE>
6<H1>Bookmarks</H1>
7<DL><p>
8 <DT><A HREF="http://nest.ed/1" ADD_DATE="1456433741" PRIVATE="0" TAGS="tag1,tag2">Nested 1</A>
9 <DT><H3 ADD_DATE="1456433722" LAST_MODIFIED="1456433739">Folder1</H3>
10 <DL><p>
11 <DT><A HREF="http://nest.ed/1-1" ADD_DATE="1456433742" PRIVATE="0" TAGS="tag1,tag2">Nested 1-1</A>
12 <DT><A HREF="http://nest.ed/1-2" ADD_DATE="1456433747" PRIVATE="0" TAGS="tag3,tag4">Nested 1-2</A>
13 </DL><p>
14 <DT><H3 ADD_DATE="1456433722">Folder2</H3>
15 <DD>This second folder contains wonderful links!
16 <DL><p>
17 <DT><A HREF="http://nest.ed/2-1" ADD_DATE="1454433742" PRIVATE="1">Nested 2-1</A>
18 <DD>First link of the second section
19 <DT><A HREF="http://nest.ed/2-2" ADD_DATE="1453233747" PRIVATE="1">Nested 2-2</A>
20 <DD>Second link of the second section
21 </DL><p>
22 <DT><H3>Folder3</H3>
23 <DL><p>
24 <DT><H3>Folder3-1</H3>
25 <DL><p>
26 <DT><A HREF="http://nest.ed/3-1" ADD_DATE="1454433742" PRIVATE="0" TAGS="tag3">Nested 3-1</A>
27 <DT><A HREF="http://nest.ed/3-2" ADD_DATE="1453233747" PRIVATE="0">Nested 3-2</A>
28 </DL><p>
29 </DL><p>
30 <DT><A HREF="http://nest.ed/2" ADD_DATE="1456733741" PRIVATE="0" TAGS="tag4">Nested 2</A>
31</DL><p>
diff --git a/tests/NetscapeBookmarkUtils/input/no_doctype.htm b/tests/NetscapeBookmarkUtils/input/no_doctype.htm
new file mode 100644
index 00000000..766d398b
--- /dev/null
+++ b/tests/NetscapeBookmarkUtils/input/no_doctype.htm
@@ -0,0 +1,7 @@
1<TITLE>Bookmarks</TITLE>
2<H1>Bookmarks</H1>
3<DL><p>
4<DT><A HREF="https://private.tld" ADD_DATE="10/Oct/2000:13:55:36 +0300" PRIVATE="1" TAGS="private secret">Secret stuff</A>
5<DD>Super-secret stuff you're not supposed to know about
6<DT><A HREF="http://public.tld" ADD_DATE="1456433748" PRIVATE="0" TAGS="public hello world">Public stuff</A>
7</DL><p>
diff --git a/tests/NetscapeBookmarkUtils/input/same_date.htm b/tests/NetscapeBookmarkUtils/input/same_date.htm
new file mode 100644
index 00000000..9d58a582
--- /dev/null
+++ b/tests/NetscapeBookmarkUtils/input/same_date.htm
@@ -0,0 +1,11 @@
1<!DOCTYPE NETSCAPE-Bookmark-file-1>
2<!-- This is an automatically generated file.
3It will be read and overwritten.
4Do Not Edit! -->
5<TITLE>Bookmarks</TITLE>
6<H1>Bookmarks</H1>
7<DL><p>
8<DT><A HREF="https://fir.st" ADD_DATE="1456433748" PRIVATE="0">Today's first link</A>
9<DT><A HREF="https://seco.nd" ADD_DATE="1456433748" PRIVATE="0">Today's second link</A>
10<DT><A HREF="https://thi.rd" ADD_DATE="1456433748" PRIVATE="0">Today's third link</A>
11</DL><p>
diff --git a/tpl/import.html b/tpl/import.html
index 6c4f9421..071e1160 100644
--- a/tpl/import.html
+++ b/tpl/import.html
@@ -3,19 +3,31 @@
3<head>{include="includes"}</head> 3<head>{include="includes"}</head>
4<body onload="document.uploadform.filetoupload.focus();"> 4<body onload="document.uploadform.filetoupload.focus();">
5<div id="pageheader"> 5<div id="pageheader">
6 {include="page.header"} 6 {include="page.header"}
7 <div id="uploaddiv"> 7 <div id="uploaddiv">
8 Import Netscape HTML bookmarks (as exported from Firefox/Chrome/Opera/Delicious/Diigo...) (Max: {$maxfilesize} bytes). 8 Import Netscape HTML bookmarks (as exported from Firefox/Chrome/Opera/Delicious/Diigo...) (Max: {$maxfilesize} bytes).
9 <form method="POST" action="?do=upload" enctype="multipart/form-data" name="uploadform" id="uploadform"> 9 <form method="POST" action="?do=import" enctype="multipart/form-data"
10 <input type="hidden" name="token" value="{$token}"> 10 name="uploadform" id="uploadform">
11 <input type="file" name="filetoupload"> 11 <input type="hidden" name="token" value="{$token}">
12 <input type="hidden" name="MAX_FILE_SIZE" value="{$maxfilesize}"> 12 <input type="hidden" name="MAX_FILE_SIZE" value="{$maxfilesize}">
13 <input type="submit" name="import_file" value="Import" class="bigbutton"><br> 13 <input type="file" name="filetoupload">
14 <input type="checkbox" name="private" id="private"><label for="private">&nbsp;Import all links as private</label><br> 14 <input type="submit" name="import_file" value="Import" class="bigbutton"><br>
15 <input type="checkbox" name="overwrite" id="overwrite"><label for="overwrite">&nbsp;Overwrite existing links</label> 15
16 </form> 16 <label for="privacy">&nbsp;Visibility:</label><br>
17 </div> 17 <input type="radio" name="privacy" value="default" checked="true">
18 &nbsp;Use values from the imported file, default to public<br>
19 <input type="radio" name="privacy" value="private">
20 &nbsp;Import all bookmarks as private<br>
21 <input type="radio" name="privacy" value="public">
22 &nbsp;Import all bookmarks as public<br>
23
24 <input type="checkbox" name="overwrite" id="overwrite">
25 <label for="overwrite">&nbsp;Overwrite existing bookmarks</label><br>
26 <label for="default_tags">&nbsp;Add default tags</label>
27 <input type="text" name="default_tags" id="default_tags">
28 </form>
29 </div>
18</div> 30</div>
19{include="page.footer"} 31{include="page.footer"}
20</body> 32</body>
21</html> \ No newline at end of file 33</html>