aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorArthurHoaro <arthur@hoa.ro>2020-01-18 10:01:06 +0100
committerGitHub <noreply@github.com>2020-01-18 10:01:06 +0100
commit3fb29fdda04ca86e04422d49b86cf646d53c4f9d (patch)
treeadf8512f93f5559ba87d0c9931969ae4ebea7133
parent796c4c57d085ae4589b53dfe8369ae9ba30ffdaf (diff)
parente26e2060f5470ce8bf4c5973284bae07b8af170a (diff)
downloadShaarli-3fb29fdda04ca86e04422d49b86cf646d53c4f9d.tar.gz
Shaarli-3fb29fdda04ca86e04422d49b86cf646d53c4f9d.tar.zst
Shaarli-3fb29fdda04ca86e04422d49b86cf646d53c4f9d.zip
Store bookmarks as PHP objects and add a service layer to retriā€¦ (#1307)
Store bookmarks as PHP objects and add a service layer to retrieve them
-rw-r--r--Makefile3
-rw-r--r--application/History.php17
-rw-r--r--application/Utils.php2
-rw-r--r--application/api/ApiMiddleware.php11
-rw-r--r--application/api/ApiUtils.php79
-rw-r--r--application/api/controllers/ApiController.php8
-rw-r--r--application/api/controllers/HistoryController.php2
-rw-r--r--application/api/controllers/Info.php5
-rw-r--r--application/api/controllers/Links.php79
-rw-r--r--application/api/controllers/Tags.php44
-rw-r--r--application/bookmark/Bookmark.php461
-rw-r--r--application/bookmark/BookmarkArray.php259
-rw-r--r--application/bookmark/BookmarkFileService.php373
-rw-r--r--application/bookmark/BookmarkFilter.php468
-rw-r--r--application/bookmark/BookmarkIO.php108
-rw-r--r--application/bookmark/BookmarkInitializer.php59
-rw-r--r--application/bookmark/BookmarkServiceInterface.php180
-rw-r--r--application/bookmark/LinkUtils.php27
-rw-r--r--application/bookmark/exception/BookmarkNotFoundException.php (renamed from application/bookmark/exception/LinkNotFoundException.php)2
-rw-r--r--application/bookmark/exception/EmptyDataStoreException.php7
-rw-r--r--application/bookmark/exception/InvalidBookmarkException.php30
-rw-r--r--application/bookmark/exception/NotWritableDataStoreException.php19
-rw-r--r--application/config/ConfigManager.php2
-rw-r--r--application/feed/FeedBuilder.php78
-rw-r--r--application/formatter/BookmarkDefaultFormatter.php81
-rw-r--r--application/formatter/BookmarkFormatter.php256
-rw-r--r--application/formatter/BookmarkMarkdownFormatter.php204
-rw-r--r--application/formatter/BookmarkRawFormatter.php13
-rw-r--r--application/formatter/FormatterFactory.php46
-rw-r--r--application/legacy/LegacyLinkDB.php (renamed from application/bookmark/LinkDB.php)68
-rw-r--r--application/legacy/LegacyLinkFilter.php (renamed from application/bookmark/LinkFilter.php)20
-rw-r--r--application/legacy/LegacyUpdater.php617
-rw-r--r--application/netscape/NetscapeBookmarkUtils.php117
-rw-r--r--application/render/PageBuilder.php24
-rw-r--r--application/updater/Updater.php480
-rw-r--r--application/updater/UpdaterUtils.php65
-rw-r--r--assets/common/css/markdown.css (renamed from plugins/markdown/markdown.css)4
-rw-r--r--composer.json2
-rw-r--r--composer.lock20
-rw-r--r--doc/md/guides/various-hacks.md8
-rw-r--r--index.php555
-rw-r--r--plugins/markdown/README.md102
-rw-r--r--plugins/markdown/help.html5
-rw-r--r--plugins/markdown/markdown.meta4
-rw-r--r--plugins/markdown/markdown.php365
-rw-r--r--tests/HistoryTest.php240
-rw-r--r--tests/api/ApiMiddlewareTest.php8
-rw-r--r--tests/api/ApiUtilsTest.php67
-rw-r--r--tests/api/controllers/history/HistoryTest.php4
-rw-r--r--tests/api/controllers/info/InfoTest.php20
-rw-r--r--tests/api/controllers/links/DeleteLinkTest.php22
-rw-r--r--tests/api/controllers/links/GetLinkIdTest.php11
-rw-r--r--tests/api/controllers/links/GetLinksTest.php15
-rw-r--r--tests/api/controllers/links/PostLinkTest.php30
-rw-r--r--tests/api/controllers/links/PutLinkTest.php24
-rw-r--r--tests/api/controllers/tags/DeleteTagTest.php29
-rw-r--r--tests/api/controllers/tags/GetTagNameTest.php8
-rw-r--r--tests/api/controllers/tags/GetTagsTest.php23
-rw-r--r--tests/api/controllers/tags/PutTagTest.php25
-rw-r--r--tests/bookmark/BookmarkArrayTest.php239
-rw-r--r--tests/bookmark/BookmarkFileServiceTest.php1042
-rw-r--r--tests/bookmark/BookmarkFilterTest.php514
-rw-r--r--tests/bookmark/BookmarkInitializerTest.php120
-rw-r--r--tests/bookmark/BookmarkTest.php388
-rw-r--r--tests/bookmark/LinkUtilsTest.php9
-rw-r--r--tests/bootstrap.php18
-rw-r--r--tests/config/ConfigJsonTest.php2
-rw-r--r--tests/feed/FeedBuilderTest.php103
-rw-r--r--tests/formatter/BookmarkDefaultFormatterTest.php156
-rw-r--r--tests/formatter/BookmarkMarkdownFormatterTest.php160
-rw-r--r--tests/formatter/BookmarkRawFormatterTest.php97
-rw-r--r--tests/formatter/FormatterFactoryTest.php101
-rw-r--r--tests/legacy/LegacyDummyUpdater.php74
-rw-r--r--tests/legacy/LegacyLinkDBTest.php (renamed from tests/bookmark/LinkDBTest.php)72
-rw-r--r--tests/legacy/LegacyLinkFilterTest.php (renamed from tests/bookmark/LinkFilterTest.php)128
-rw-r--r--tests/legacy/LegacyUpdaterTest.php886
-rw-r--r--tests/netscape/BookmarkExportTest.php70
-rw-r--r--tests/netscape/BookmarkImportTest.php570
-rw-r--r--tests/plugins/PluginArchiveorgTest.php4
-rw-r--r--tests/plugins/PluginIssoTest.php12
-rw-r--r--tests/plugins/PluginMarkdownTest.php316
-rw-r--r--tests/updater/DummyUpdater.php13
-rw-r--r--tests/updater/UpdaterTest.php684
-rw-r--r--tests/utils/FakeBookmarkService.php18
-rw-r--r--tests/utils/ReferenceHistory.php2
-rw-r--r--tests/utils/ReferenceLinkDB.php111
-rw-r--r--tests/utils/config/configJson.json.php6
-rw-r--r--tpl/default/configure.html22
-rw-r--r--tpl/default/editlink.html12
-rw-r--r--tpl/default/includes.html3
-rw-r--r--tpl/vintage/configure.html13
-rw-r--r--tpl/vintage/editlink.html12
-rw-r--r--tpl/vintage/includes.html3
-rw-r--r--webpack.config.js4
94 files changed, 8575 insertions, 3314 deletions
diff --git a/Makefile b/Makefile
index 286d2c90..917fab7d 100644
--- a/Makefile
+++ b/Makefile
@@ -80,7 +80,8 @@ locale_test_%:
80 --testsuite language-$(firstword $(subst _, ,$*)) 80 --testsuite language-$(firstword $(subst _, ,$*))
81 81
82all_tests: test locale_test_de_DE locale_test_en_US locale_test_fr_FR 82all_tests: test locale_test_de_DE locale_test_en_US locale_test_fr_FR
83 @$(BIN)/phpcov merge --html coverage coverage 83 @# --The current version is not compatible with PHP 7.2
84 @#$(BIN)/phpcov merge --html coverage coverage
84 @# --text doesn't work with phpunit 4.* (v5 requires PHP 5.6) 85 @# --text doesn't work with phpunit 4.* (v5 requires PHP 5.6)
85 @#$(BIN)/phpcov merge --text coverage/txt coverage 86 @#$(BIN)/phpcov merge --text coverage/txt coverage
86 87
diff --git a/application/History.php b/application/History.php
index a5846652..4fd2f294 100644
--- a/application/History.php
+++ b/application/History.php
@@ -3,6 +3,7 @@ namespace Shaarli;
3 3
4use DateTime; 4use DateTime;
5use Exception; 5use Exception;
6use Shaarli\Bookmark\Bookmark;
6 7
7/** 8/**
8 * Class History 9 * Class History
@@ -20,7 +21,7 @@ use Exception;
20 * - UPDATED: link updated 21 * - UPDATED: link updated
21 * - DELETED: link deleted 22 * - DELETED: link deleted
22 * - SETTINGS: the settings have been updated through the UI. 23 * - SETTINGS: the settings have been updated through the UI.
23 * - IMPORT: bulk links import 24 * - IMPORT: bulk bookmarks import
24 * 25 *
25 * Note: new events are put at the beginning of the file and history array. 26 * Note: new events are put at the beginning of the file and history array.
26 */ 27 */
@@ -96,31 +97,31 @@ class History
96 /** 97 /**
97 * Add Event: new link. 98 * Add Event: new link.
98 * 99 *
99 * @param array $link Link data. 100 * @param Bookmark $link Link data.
100 */ 101 */
101 public function addLink($link) 102 public function addLink($link)
102 { 103 {
103 $this->addEvent(self::CREATED, $link['id']); 104 $this->addEvent(self::CREATED, $link->getId());
104 } 105 }
105 106
106 /** 107 /**
107 * Add Event: update existing link. 108 * Add Event: update existing link.
108 * 109 *
109 * @param array $link Link data. 110 * @param Bookmark $link Link data.
110 */ 111 */
111 public function updateLink($link) 112 public function updateLink($link)
112 { 113 {
113 $this->addEvent(self::UPDATED, $link['id']); 114 $this->addEvent(self::UPDATED, $link->getId());
114 } 115 }
115 116
116 /** 117 /**
117 * Add Event: delete existing link. 118 * Add Event: delete existing link.
118 * 119 *
119 * @param array $link Link data. 120 * @param Bookmark $link Link data.
120 */ 121 */
121 public function deleteLink($link) 122 public function deleteLink($link)
122 { 123 {
123 $this->addEvent(self::DELETED, $link['id']); 124 $this->addEvent(self::DELETED, $link->getId());
124 } 125 }
125 126
126 /** 127 /**
@@ -134,7 +135,7 @@ class History
134 /** 135 /**
135 * Add Event: bulk import. 136 * Add Event: bulk import.
136 * 137 *
137 * Note: we don't store links add/update one by one since it can have a huge impact on performances. 138 * Note: we don't store bookmarks add/update one by one since it can have a huge impact on performances.
138 */ 139 */
139 public function importLinks() 140 public function importLinks()
140 { 141 {
diff --git a/application/Utils.php b/application/Utils.php
index 925e1a22..56f5b9a2 100644
--- a/application/Utils.php
+++ b/application/Utils.php
@@ -162,7 +162,7 @@ function generateLocation($referer, $host, $loopTerms = array())
162 $finalReferer = '?'; 162 $finalReferer = '?';
163 163
164 // No referer if it contains any value in $loopCriteria. 164 // No referer if it contains any value in $loopCriteria.
165 foreach ($loopTerms as $value) { 165 foreach (array_filter($loopTerms) as $value) {
166 if (strpos($referer, $value) !== false) { 166 if (strpos($referer, $value) !== false) {
167 return $finalReferer; 167 return $finalReferer;
168 } 168 }
diff --git a/application/api/ApiMiddleware.php b/application/api/ApiMiddleware.php
index 2d55bda6..4745ac94 100644
--- a/application/api/ApiMiddleware.php
+++ b/application/api/ApiMiddleware.php
@@ -3,6 +3,7 @@ namespace Shaarli\Api;
3 3
4use Shaarli\Api\Exceptions\ApiAuthorizationException; 4use Shaarli\Api\Exceptions\ApiAuthorizationException;
5use Shaarli\Api\Exceptions\ApiException; 5use Shaarli\Api\Exceptions\ApiException;
6use Shaarli\Bookmark\BookmarkFileService;
6use Shaarli\Config\ConfigManager; 7use Shaarli\Config\ConfigManager;
7use Slim\Container; 8use Slim\Container;
8use Slim\Http\Request; 9use Slim\Http\Request;
@@ -117,7 +118,7 @@ class ApiMiddleware
117 } 118 }
118 119
119 /** 120 /**
120 * Instantiate a new LinkDB including private links, 121 * Instantiate a new LinkDB including private bookmarks,
121 * and load in the Slim container. 122 * and load in the Slim container.
122 * 123 *
123 * FIXME! LinkDB could use a refactoring to avoid this trick. 124 * FIXME! LinkDB could use a refactoring to avoid this trick.
@@ -126,10 +127,10 @@ class ApiMiddleware
126 */ 127 */
127 protected function setLinkDb($conf) 128 protected function setLinkDb($conf)
128 { 129 {
129 $linkDb = new \Shaarli\Bookmark\LinkDB( 130 $linkDb = new BookmarkFileService(
130 $conf->get('resource.datastore'), 131 $conf,
131 true, 132 $this->container->get('history'),
132 $conf->get('privacy.hide_public_links') 133 true
133 ); 134 );
134 $this->container['db'] = $linkDb; 135 $this->container['db'] = $linkDb;
135 } 136 }
diff --git a/application/api/ApiUtils.php b/application/api/ApiUtils.php
index 5ac07c4d..5156a5f7 100644
--- a/application/api/ApiUtils.php
+++ b/application/api/ApiUtils.php
@@ -2,6 +2,7 @@
2namespace Shaarli\Api; 2namespace Shaarli\Api;
3 3
4use Shaarli\Api\Exceptions\ApiAuthorizationException; 4use Shaarli\Api\Exceptions\ApiAuthorizationException;
5use Shaarli\Bookmark\Bookmark;
5use Shaarli\Http\Base64Url; 6use Shaarli\Http\Base64Url;
6 7
7/** 8/**
@@ -54,28 +55,28 @@ class ApiUtils
54 /** 55 /**
55 * Format a Link for the REST API. 56 * Format a Link for the REST API.
56 * 57 *
57 * @param array $link Link data read from the datastore. 58 * @param Bookmark $bookmark Bookmark data read from the datastore.
58 * @param string $indexUrl Shaarli's index URL (used for relative URL). 59 * @param string $indexUrl Shaarli's index URL (used for relative URL).
59 * 60 *
60 * @return array Link data formatted for the REST API. 61 * @return array Link data formatted for the REST API.
61 */ 62 */
62 public static function formatLink($link, $indexUrl) 63 public static function formatLink($bookmark, $indexUrl)
63 { 64 {
64 $out['id'] = $link['id']; 65 $out['id'] = $bookmark->getId();
65 // Not an internal link 66 // Not an internal link
66 if (! is_note($link['url'])) { 67 if (! $bookmark->isNote()) {
67 $out['url'] = $link['url']; 68 $out['url'] = $bookmark->getUrl();
68 } else { 69 } else {
69 $out['url'] = $indexUrl . $link['url']; 70 $out['url'] = $indexUrl . $bookmark->getUrl();
70 } 71 }
71 $out['shorturl'] = $link['shorturl']; 72 $out['shorturl'] = $bookmark->getShortUrl();
72 $out['title'] = $link['title']; 73 $out['title'] = $bookmark->getTitle();
73 $out['description'] = $link['description']; 74 $out['description'] = $bookmark->getDescription();
74 $out['tags'] = preg_split('/\s+/', $link['tags'], -1, PREG_SPLIT_NO_EMPTY); 75 $out['tags'] = $bookmark->getTags();
75 $out['private'] = $link['private'] == true; 76 $out['private'] = $bookmark->isPrivate();
76 $out['created'] = $link['created']->format(\DateTime::ATOM); 77 $out['created'] = $bookmark->getCreated()->format(\DateTime::ATOM);
77 if (! empty($link['updated'])) { 78 if (! empty($bookmark->getUpdated())) {
78 $out['updated'] = $link['updated']->format(\DateTime::ATOM); 79 $out['updated'] = $bookmark->getUpdated()->format(\DateTime::ATOM);
79 } else { 80 } else {
80 $out['updated'] = ''; 81 $out['updated'] = '';
81 } 82 }
@@ -83,7 +84,7 @@ class ApiUtils
83 } 84 }
84 85
85 /** 86 /**
86 * Convert a link given through a request, to a valid link for LinkDB. 87 * Convert a link given through a request, to a valid Bookmark for the datastore.
87 * 88 *
88 * If no URL is provided, it will generate a local note URL. 89 * If no URL is provided, it will generate a local note URL.
89 * If no title is provided, it will use the URL as title. 90 * If no title is provided, it will use the URL as title.
@@ -91,50 +92,42 @@ class ApiUtils
91 * @param array $input Request Link. 92 * @param array $input Request Link.
92 * @param bool $defaultPrivate Request Link. 93 * @param bool $defaultPrivate Request Link.
93 * 94 *
94 * @return array Formatted link. 95 * @return Bookmark instance.
95 */ 96 */
96 public static function buildLinkFromRequest($input, $defaultPrivate) 97 public static function buildLinkFromRequest($input, $defaultPrivate)
97 { 98 {
98 $input['url'] = ! empty($input['url']) ? cleanup_url($input['url']) : ''; 99 $bookmark = new Bookmark();
100 $url = ! empty($input['url']) ? cleanup_url($input['url']) : '';
99 if (isset($input['private'])) { 101 if (isset($input['private'])) {
100 $private = filter_var($input['private'], FILTER_VALIDATE_BOOLEAN); 102 $private = filter_var($input['private'], FILTER_VALIDATE_BOOLEAN);
101 } else { 103 } else {
102 $private = $defaultPrivate; 104 $private = $defaultPrivate;
103 } 105 }
104 106
105 $link = [ 107 $bookmark->setTitle(! empty($input['title']) ? $input['title'] : '');
106 'title' => ! empty($input['title']) ? $input['title'] : $input['url'], 108 $bookmark->setUrl($url);
107 'url' => $input['url'], 109 $bookmark->setDescription(! empty($input['description']) ? $input['description'] : '');
108 'description' => ! empty($input['description']) ? $input['description'] : '', 110 $bookmark->setTags(! empty($input['tags']) ? $input['tags'] : []);
109 'tags' => ! empty($input['tags']) ? implode(' ', $input['tags']) : '', 111 $bookmark->setPrivate($private);
110 'private' => $private, 112
111 'created' => new \DateTime(), 113 return $bookmark;
112 ];
113 return $link;
114 } 114 }
115 115
116 /** 116 /**
117 * Update link fields using an updated link object. 117 * Update link fields using an updated link object.
118 * 118 *
119 * @param array $oldLink data 119 * @param Bookmark $oldLink data
120 * @param array $newLink data 120 * @param Bookmark $newLink data
121 * 121 *
122 * @return array $oldLink updated with $newLink values 122 * @return Bookmark $oldLink updated with $newLink values
123 */ 123 */
124 public static function updateLink($oldLink, $newLink) 124 public static function updateLink($oldLink, $newLink)
125 { 125 {
126 foreach (['title', 'url', 'description', 'tags', 'private'] as $field) { 126 $oldLink->setTitle($newLink->getTitle());
127 $oldLink[$field] = $newLink[$field]; 127 $oldLink->setUrl($newLink->getUrl());
128 } 128 $oldLink->setDescription($newLink->getDescription());
129 $oldLink['updated'] = new \DateTime(); 129 $oldLink->setTags($newLink->getTags());
130 130 $oldLink->setPrivate($newLink->isPrivate());
131 if (empty($oldLink['url'])) {
132 $oldLink['url'] = '?' . $oldLink['shorturl'];
133 }
134
135 if (empty($oldLink['title'])) {
136 $oldLink['title'] = $oldLink['url'];
137 }
138 131
139 return $oldLink; 132 return $oldLink;
140 } 133 }
@@ -143,7 +136,7 @@ class ApiUtils
143 * Format a Tag for the REST API. 136 * Format a Tag for the REST API.
144 * 137 *
145 * @param string $tag Tag name 138 * @param string $tag Tag name
146 * @param int $occurrences Number of links using this tag 139 * @param int $occurrences Number of bookmarks using this tag
147 * 140 *
148 * @return array Link data formatted for the REST API. 141 * @return array Link data formatted for the REST API.
149 */ 142 */
diff --git a/application/api/controllers/ApiController.php b/application/api/controllers/ApiController.php
index a6e7cbab..c4b3d0c3 100644
--- a/application/api/controllers/ApiController.php
+++ b/application/api/controllers/ApiController.php
@@ -2,7 +2,7 @@
2 2
3namespace Shaarli\Api\Controllers; 3namespace Shaarli\Api\Controllers;
4 4
5use Shaarli\Bookmark\LinkDB; 5use Shaarli\Bookmark\BookmarkServiceInterface;
6use Shaarli\Config\ConfigManager; 6use Shaarli\Config\ConfigManager;
7use Slim\Container; 7use Slim\Container;
8 8
@@ -26,9 +26,9 @@ abstract class ApiController
26 protected $conf; 26 protected $conf;
27 27
28 /** 28 /**
29 * @var LinkDB 29 * @var BookmarkServiceInterface
30 */ 30 */
31 protected $linkDb; 31 protected $bookmarkService;
32 32
33 /** 33 /**
34 * @var HistoryController 34 * @var HistoryController
@@ -51,7 +51,7 @@ abstract class ApiController
51 { 51 {
52 $this->ci = $ci; 52 $this->ci = $ci;
53 $this->conf = $ci->get('conf'); 53 $this->conf = $ci->get('conf');
54 $this->linkDb = $ci->get('db'); 54 $this->bookmarkService = $ci->get('db');
55 $this->history = $ci->get('history'); 55 $this->history = $ci->get('history');
56 if ($this->conf->get('dev.debug', false)) { 56 if ($this->conf->get('dev.debug', false)) {
57 $this->jsonStyle = JSON_PRETTY_PRINT; 57 $this->jsonStyle = JSON_PRETTY_PRINT;
diff --git a/application/api/controllers/HistoryController.php b/application/api/controllers/HistoryController.php
index 9afcfa26..505647a9 100644
--- a/application/api/controllers/HistoryController.php
+++ b/application/api/controllers/HistoryController.php
@@ -41,7 +41,7 @@ class HistoryController extends ApiController
41 throw new ApiBadParametersException('Invalid offset'); 41 throw new ApiBadParametersException('Invalid offset');
42 } 42 }
43 43
44 // limit parameter is either a number of links or 'all' for everything. 44 // limit parameter is either a number of bookmarks or 'all' for everything.
45 $limit = $request->getParam('limit'); 45 $limit = $request->getParam('limit');
46 if (empty($limit)) { 46 if (empty($limit)) {
47 $limit = count($history); 47 $limit = count($history);
diff --git a/application/api/controllers/Info.php b/application/api/controllers/Info.php
index f37dcae5..12f6b2f0 100644
--- a/application/api/controllers/Info.php
+++ b/application/api/controllers/Info.php
@@ -2,6 +2,7 @@
2 2
3namespace Shaarli\Api\Controllers; 3namespace Shaarli\Api\Controllers;
4 4
5use Shaarli\Bookmark\BookmarkFilter;
5use Slim\Http\Request; 6use Slim\Http\Request;
6use Slim\Http\Response; 7use Slim\Http\Response;
7 8
@@ -26,8 +27,8 @@ class Info extends ApiController
26 public function getInfo($request, $response) 27 public function getInfo($request, $response)
27 { 28 {
28 $info = [ 29 $info = [
29 'global_counter' => count($this->linkDb), 30 'global_counter' => $this->bookmarkService->count(),
30 'private_counter' => count_private($this->linkDb), 31 'private_counter' => $this->bookmarkService->count(BookmarkFilter::$PRIVATE),
31 'settings' => array( 32 'settings' => array(
32 'title' => $this->conf->get('general.title', 'Shaarli'), 33 'title' => $this->conf->get('general.title', 'Shaarli'),
33 'header_link' => $this->conf->get('general.header_link', '?'), 34 'header_link' => $this->conf->get('general.header_link', '?'),
diff --git a/application/api/controllers/Links.php b/application/api/controllers/Links.php
index ffcfd4c7..29247950 100644
--- a/application/api/controllers/Links.php
+++ b/application/api/controllers/Links.php
@@ -11,7 +11,7 @@ use Slim\Http\Response;
11/** 11/**
12 * Class Links 12 * Class Links
13 * 13 *
14 * REST API Controller: all services related to links collection. 14 * REST API Controller: all services related to bookmarks collection.
15 * 15 *
16 * @package Api\Controllers 16 * @package Api\Controllers
17 * @see http://shaarli.github.io/api-documentation/#links-links-collection 17 * @see http://shaarli.github.io/api-documentation/#links-links-collection
@@ -19,12 +19,12 @@ use Slim\Http\Response;
19class Links extends ApiController 19class Links extends ApiController
20{ 20{
21 /** 21 /**
22 * @var int Number of links returned if no limit is provided. 22 * @var int Number of bookmarks returned if no limit is provided.
23 */ 23 */
24 public static $DEFAULT_LIMIT = 20; 24 public static $DEFAULT_LIMIT = 20;
25 25
26 /** 26 /**
27 * Retrieve a list of links, allowing different filters. 27 * Retrieve a list of bookmarks, allowing different filters.
28 * 28 *
29 * @param Request $request Slim request. 29 * @param Request $request Slim request.
30 * @param Response $response Slim response. 30 * @param Response $response Slim response.
@@ -36,33 +36,32 @@ class Links extends ApiController
36 public function getLinks($request, $response) 36 public function getLinks($request, $response)
37 { 37 {
38 $private = $request->getParam('visibility'); 38 $private = $request->getParam('visibility');
39 $links = $this->linkDb->filterSearch( 39 $bookmarks = $this->bookmarkService->search(
40 [ 40 [
41 'searchtags' => $request->getParam('searchtags', ''), 41 'searchtags' => $request->getParam('searchtags', ''),
42 'searchterm' => $request->getParam('searchterm', ''), 42 'searchterm' => $request->getParam('searchterm', ''),
43 ], 43 ],
44 false,
45 $private 44 $private
46 ); 45 );
47 46
48 // Return links from the {offset}th link, starting from 0. 47 // Return bookmarks from the {offset}th link, starting from 0.
49 $offset = $request->getParam('offset'); 48 $offset = $request->getParam('offset');
50 if (! empty($offset) && ! ctype_digit($offset)) { 49 if (! empty($offset) && ! ctype_digit($offset)) {
51 throw new ApiBadParametersException('Invalid offset'); 50 throw new ApiBadParametersException('Invalid offset');
52 } 51 }
53 $offset = ! empty($offset) ? intval($offset) : 0; 52 $offset = ! empty($offset) ? intval($offset) : 0;
54 if ($offset > count($links)) { 53 if ($offset > count($bookmarks)) {
55 return $response->withJson([], 200, $this->jsonStyle); 54 return $response->withJson([], 200, $this->jsonStyle);
56 } 55 }
57 56
58 // limit parameter is either a number of links or 'all' for everything. 57 // limit parameter is either a number of bookmarks or 'all' for everything.
59 $limit = $request->getParam('limit'); 58 $limit = $request->getParam('limit');
60 if (empty($limit)) { 59 if (empty($limit)) {
61 $limit = self::$DEFAULT_LIMIT; 60 $limit = self::$DEFAULT_LIMIT;
62 } elseif (ctype_digit($limit)) { 61 } elseif (ctype_digit($limit)) {
63 $limit = intval($limit); 62 $limit = intval($limit);
64 } elseif ($limit === 'all') { 63 } elseif ($limit === 'all') {
65 $limit = count($links); 64 $limit = count($bookmarks);
66 } else { 65 } else {
67 throw new ApiBadParametersException('Invalid limit'); 66 throw new ApiBadParametersException('Invalid limit');
68 } 67 }
@@ -72,12 +71,12 @@ class Links extends ApiController
72 71
73 $out = []; 72 $out = [];
74 $index = 0; 73 $index = 0;
75 foreach ($links as $link) { 74 foreach ($bookmarks as $bookmark) {
76 if (count($out) >= $limit) { 75 if (count($out) >= $limit) {
77 break; 76 break;
78 } 77 }
79 if ($index++ >= $offset) { 78 if ($index++ >= $offset) {
80 $out[] = ApiUtils::formatLink($link, $indexUrl); 79 $out[] = ApiUtils::formatLink($bookmark, $indexUrl);
81 } 80 }
82 } 81 }
83 82
@@ -97,11 +96,11 @@ class Links extends ApiController
97 */ 96 */
98 public function getLink($request, $response, $args) 97 public function getLink($request, $response, $args)
99 { 98 {
100 if (!isset($this->linkDb[$args['id']])) { 99 if (!$this->bookmarkService->exists($args['id'])) {
101 throw new ApiLinkNotFoundException(); 100 throw new ApiLinkNotFoundException();
102 } 101 }
103 $index = index_url($this->ci['environment']); 102 $index = index_url($this->ci['environment']);
104 $out = ApiUtils::formatLink($this->linkDb[$args['id']], $index); 103 $out = ApiUtils::formatLink($this->bookmarkService->get($args['id']), $index);
105 104
106 return $response->withJson($out, 200, $this->jsonStyle); 105 return $response->withJson($out, 200, $this->jsonStyle);
107 } 106 }
@@ -117,9 +116,11 @@ class Links extends ApiController
117 public function postLink($request, $response) 116 public function postLink($request, $response)
118 { 117 {
119 $data = $request->getParsedBody(); 118 $data = $request->getParsedBody();
120 $link = ApiUtils::buildLinkFromRequest($data, $this->conf->get('privacy.default_private_links')); 119 $bookmark = ApiUtils::buildLinkFromRequest($data, $this->conf->get('privacy.default_private_links'));
121 // duplicate by URL, return 409 Conflict 120 // duplicate by URL, return 409 Conflict
122 if (! empty($link['url']) && ! empty($dup = $this->linkDb->getLinkFromUrl($link['url']))) { 121 if (! empty($bookmark->getUrl())
122 && ! empty($dup = $this->bookmarkService->findByUrl($bookmark->getUrl()))
123 ) {
123 return $response->withJson( 124 return $response->withJson(
124 ApiUtils::formatLink($dup, index_url($this->ci['environment'])), 125 ApiUtils::formatLink($dup, index_url($this->ci['environment'])),
125 409, 126 409,
@@ -127,23 +128,9 @@ class Links extends ApiController
127 ); 128 );
128 } 129 }
129 130
130 $link['id'] = $this->linkDb->getNextId(); 131 $this->bookmarkService->add($bookmark);
131 $link['shorturl'] = link_small_hash($link['created'], $link['id']); 132 $out = ApiUtils::formatLink($bookmark, index_url($this->ci['environment']));
132 133 $redirect = $this->ci->router->relativePathFor('getLink', ['id' => $bookmark->getId()]);
133 // note: general relative URL
134 if (empty($link['url'])) {
135 $link['url'] = '?' . $link['shorturl'];
136 }
137
138 if (empty($link['title'])) {
139 $link['title'] = $link['url'];
140 }
141
142 $this->linkDb[$link['id']] = $link;
143 $this->linkDb->save($this->conf->get('resource.page_cache'));
144 $this->history->addLink($link);
145 $out = ApiUtils::formatLink($link, index_url($this->ci['environment']));
146 $redirect = $this->ci->router->relativePathFor('getLink', ['id' => $link['id']]);
147 return $response->withAddedHeader('Location', $redirect) 134 return $response->withAddedHeader('Location', $redirect)
148 ->withJson($out, 201, $this->jsonStyle); 135 ->withJson($out, 201, $this->jsonStyle);
149 } 136 }
@@ -161,18 +148,18 @@ class Links extends ApiController
161 */ 148 */
162 public function putLink($request, $response, $args) 149 public function putLink($request, $response, $args)
163 { 150 {
164 if (! isset($this->linkDb[$args['id']])) { 151 if (! $this->bookmarkService->exists($args['id'])) {
165 throw new ApiLinkNotFoundException(); 152 throw new ApiLinkNotFoundException();
166 } 153 }
167 154
168 $index = index_url($this->ci['environment']); 155 $index = index_url($this->ci['environment']);
169 $data = $request->getParsedBody(); 156 $data = $request->getParsedBody();
170 157
171 $requestLink = ApiUtils::buildLinkFromRequest($data, $this->conf->get('privacy.default_private_links')); 158 $requestBookmark = ApiUtils::buildLinkFromRequest($data, $this->conf->get('privacy.default_private_links'));
172 // duplicate URL on a different link, return 409 Conflict 159 // duplicate URL on a different link, return 409 Conflict
173 if (! empty($requestLink['url']) 160 if (! empty($requestBookmark->getUrl())
174 && ! empty($dup = $this->linkDb->getLinkFromUrl($requestLink['url'])) 161 && ! empty($dup = $this->bookmarkService->findByUrl($requestBookmark->getUrl()))
175 && $dup['id'] != $args['id'] 162 && $dup->getId() != $args['id']
176 ) { 163 ) {
177 return $response->withJson( 164 return $response->withJson(
178 ApiUtils::formatLink($dup, $index), 165 ApiUtils::formatLink($dup, $index),
@@ -181,13 +168,11 @@ class Links extends ApiController
181 ); 168 );
182 } 169 }
183 170
184 $responseLink = $this->linkDb[$args['id']]; 171 $responseBookmark = $this->bookmarkService->get($args['id']);
185 $responseLink = ApiUtils::updateLink($responseLink, $requestLink); 172 $responseBookmark = ApiUtils::updateLink($responseBookmark, $requestBookmark);
186 $this->linkDb[$responseLink['id']] = $responseLink; 173 $this->bookmarkService->set($responseBookmark);
187 $this->linkDb->save($this->conf->get('resource.page_cache'));
188 $this->history->updateLink($responseLink);
189 174
190 $out = ApiUtils::formatLink($responseLink, $index); 175 $out = ApiUtils::formatLink($responseBookmark, $index);
191 return $response->withJson($out, 200, $this->jsonStyle); 176 return $response->withJson($out, 200, $this->jsonStyle);
192 } 177 }
193 178
@@ -204,13 +189,11 @@ class Links extends ApiController
204 */ 189 */
205 public function deleteLink($request, $response, $args) 190 public function deleteLink($request, $response, $args)
206 { 191 {
207 if (! isset($this->linkDb[$args['id']])) { 192 if (! $this->bookmarkService->exists($args['id'])) {
208 throw new ApiLinkNotFoundException(); 193 throw new ApiLinkNotFoundException();
209 } 194 }
210 $link = $this->linkDb[$args['id']]; 195 $bookmark = $this->bookmarkService->get($args['id']);
211 unset($this->linkDb[(int) $args['id']]); 196 $this->bookmarkService->remove($bookmark);
212 $this->linkDb->save($this->conf->get('resource.page_cache'));
213 $this->history->deleteLink($link);
214 197
215 return $response->withStatus(204); 198 return $response->withStatus(204);
216 } 199 }
diff --git a/application/api/controllers/Tags.php b/application/api/controllers/Tags.php
index 82f3ef74..e60e00a7 100644
--- a/application/api/controllers/Tags.php
+++ b/application/api/controllers/Tags.php
@@ -5,6 +5,7 @@ namespace Shaarli\Api\Controllers;
5use Shaarli\Api\ApiUtils; 5use Shaarli\Api\ApiUtils;
6use Shaarli\Api\Exceptions\ApiBadParametersException; 6use Shaarli\Api\Exceptions\ApiBadParametersException;
7use Shaarli\Api\Exceptions\ApiTagNotFoundException; 7use Shaarli\Api\Exceptions\ApiTagNotFoundException;
8use Shaarli\Bookmark\BookmarkFilter;
8use Slim\Http\Request; 9use Slim\Http\Request;
9use Slim\Http\Response; 10use Slim\Http\Response;
10 11
@@ -18,7 +19,7 @@ use Slim\Http\Response;
18class Tags extends ApiController 19class Tags extends ApiController
19{ 20{
20 /** 21 /**
21 * @var int Number of links returned if no limit is provided. 22 * @var int Number of bookmarks returned if no limit is provided.
22 */ 23 */
23 public static $DEFAULT_LIMIT = 'all'; 24 public static $DEFAULT_LIMIT = 'all';
24 25
@@ -35,7 +36,7 @@ class Tags extends ApiController
35 public function getTags($request, $response) 36 public function getTags($request, $response)
36 { 37 {
37 $visibility = $request->getParam('visibility'); 38 $visibility = $request->getParam('visibility');
38 $tags = $this->linkDb->linksCountPerTag([], $visibility); 39 $tags = $this->bookmarkService->bookmarksCountPerTag([], $visibility);
39 40
40 // Return tags from the {offset}th tag, starting from 0. 41 // Return tags from the {offset}th tag, starting from 0.
41 $offset = $request->getParam('offset'); 42 $offset = $request->getParam('offset');
@@ -47,7 +48,7 @@ class Tags extends ApiController
47 return $response->withJson([], 200, $this->jsonStyle); 48 return $response->withJson([], 200, $this->jsonStyle);
48 } 49 }
49 50
50 // limit parameter is either a number of links or 'all' for everything. 51 // limit parameter is either a number of bookmarks or 'all' for everything.
51 $limit = $request->getParam('limit'); 52 $limit = $request->getParam('limit');
52 if (empty($limit)) { 53 if (empty($limit)) {
53 $limit = self::$DEFAULT_LIMIT; 54 $limit = self::$DEFAULT_LIMIT;
@@ -87,7 +88,7 @@ class Tags extends ApiController
87 */ 88 */
88 public function getTag($request, $response, $args) 89 public function getTag($request, $response, $args)
89 { 90 {
90 $tags = $this->linkDb->linksCountPerTag(); 91 $tags = $this->bookmarkService->bookmarksCountPerTag();
91 if (!isset($tags[$args['tagName']])) { 92 if (!isset($tags[$args['tagName']])) {
92 throw new ApiTagNotFoundException(); 93 throw new ApiTagNotFoundException();
93 } 94 }
@@ -111,7 +112,7 @@ class Tags extends ApiController
111 */ 112 */
112 public function putTag($request, $response, $args) 113 public function putTag($request, $response, $args)
113 { 114 {
114 $tags = $this->linkDb->linksCountPerTag(); 115 $tags = $this->bookmarkService->bookmarksCountPerTag();
115 if (! isset($tags[$args['tagName']])) { 116 if (! isset($tags[$args['tagName']])) {
116 throw new ApiTagNotFoundException(); 117 throw new ApiTagNotFoundException();
117 } 118 }
@@ -121,13 +122,19 @@ class Tags extends ApiController
121 throw new ApiBadParametersException('New tag name is required in the request body'); 122 throw new ApiBadParametersException('New tag name is required in the request body');
122 } 123 }
123 124
124 $updated = $this->linkDb->renameTag($args['tagName'], $data['name']); 125 $bookmarks = $this->bookmarkService->search(
125 $this->linkDb->save($this->conf->get('resource.page_cache')); 126 ['searchtags' => $args['tagName']],
126 foreach ($updated as $link) { 127 BookmarkFilter::$ALL,
127 $this->history->updateLink($link); 128 true
129 );
130 foreach ($bookmarks as $bookmark) {
131 $bookmark->renameTag($args['tagName'], $data['name']);
132 $this->bookmarkService->set($bookmark, false);
133 $this->history->updateLink($bookmark);
128 } 134 }
135 $this->bookmarkService->save();
129 136
130 $tags = $this->linkDb->linksCountPerTag(); 137 $tags = $this->bookmarkService->bookmarksCountPerTag();
131 $out = ApiUtils::formatTag($data['name'], $tags[$data['name']]); 138 $out = ApiUtils::formatTag($data['name'], $tags[$data['name']]);
132 return $response->withJson($out, 200, $this->jsonStyle); 139 return $response->withJson($out, 200, $this->jsonStyle);
133 } 140 }
@@ -145,15 +152,22 @@ class Tags extends ApiController
145 */ 152 */
146 public function deleteTag($request, $response, $args) 153 public function deleteTag($request, $response, $args)
147 { 154 {
148 $tags = $this->linkDb->linksCountPerTag(); 155 $tags = $this->bookmarkService->bookmarksCountPerTag();
149 if (! isset($tags[$args['tagName']])) { 156 if (! isset($tags[$args['tagName']])) {
150 throw new ApiTagNotFoundException(); 157 throw new ApiTagNotFoundException();
151 } 158 }
152 $updated = $this->linkDb->renameTag($args['tagName'], null); 159
153 $this->linkDb->save($this->conf->get('resource.page_cache')); 160 $bookmarks = $this->bookmarkService->search(
154 foreach ($updated as $link) { 161 ['searchtags' => $args['tagName']],
155 $this->history->updateLink($link); 162 BookmarkFilter::$ALL,
163 true
164 );
165 foreach ($bookmarks as $bookmark) {
166 $bookmark->deleteTag($args['tagName']);
167 $this->bookmarkService->set($bookmark, false);
168 $this->history->updateLink($bookmark);
156 } 169 }
170 $this->bookmarkService->save();
157 171
158 return $response->withStatus(204); 172 return $response->withStatus(204);
159 } 173 }
diff --git a/application/bookmark/Bookmark.php b/application/bookmark/Bookmark.php
new file mode 100644
index 00000000..f9b21d3d
--- /dev/null
+++ b/application/bookmark/Bookmark.php
@@ -0,0 +1,461 @@
1<?php
2
3namespace Shaarli\Bookmark;
4
5use DateTime;
6use Shaarli\Bookmark\Exception\InvalidBookmarkException;
7
8/**
9 * Class Bookmark
10 *
11 * This class represent a single Bookmark with all its attributes.
12 * Every bookmark should manipulated using this, before being formatted.
13 *
14 * @package Shaarli\Bookmark
15 */
16class Bookmark
17{
18 /** @var string Date format used in string (former ID format) */
19 const LINK_DATE_FORMAT = 'Ymd_His';
20
21 /** @var int Bookmark ID */
22 protected $id;
23
24 /** @var string Permalink identifier */
25 protected $shortUrl;
26
27 /** @var string Bookmark's URL - $shortUrl prefixed with `?` for notes */
28 protected $url;
29
30 /** @var string Bookmark's title */
31 protected $title;
32
33 /** @var string Raw bookmark's description */
34 protected $description;
35
36 /** @var array List of bookmark's tags */
37 protected $tags;
38
39 /** @var string Thumbnail's URL - false if no thumbnail could be found */
40 protected $thumbnail;
41
42 /** @var bool Set to true if the bookmark is set as sticky */
43 protected $sticky;
44
45 /** @var DateTime Creation datetime */
46 protected $created;
47
48 /** @var DateTime Update datetime */
49 protected $updated;
50
51 /** @var bool True if the bookmark can only be seen while logged in */
52 protected $private;
53
54 /**
55 * Initialize a link from array data. Especially useful to create a Bookmark from former link storage format.
56 *
57 * @param array $data
58 *
59 * @return $this
60 */
61 public function fromArray($data)
62 {
63 $this->id = $data['id'];
64 $this->shortUrl = $data['shorturl'];
65 $this->url = $data['url'];
66 $this->title = $data['title'];
67 $this->description = $data['description'];
68 $this->thumbnail = isset($data['thumbnail']) ? $data['thumbnail'] : null;
69 $this->sticky = isset($data['sticky']) ? $data['sticky'] : false;
70 $this->created = $data['created'];
71 if (is_array($data['tags'])) {
72 $this->tags = $data['tags'];
73 } else {
74 $this->tags = preg_split('/\s+/', $data['tags'], -1, PREG_SPLIT_NO_EMPTY);
75 }
76 if (! empty($data['updated'])) {
77 $this->updated = $data['updated'];
78 }
79 $this->private = $data['private'] ? true : false;
80
81 return $this;
82 }
83
84 /**
85 * Make sure that the current instance of Bookmark is valid and can be saved into the data store.
86 * A valid link requires:
87 * - an integer ID
88 * - a short URL (for permalinks)
89 * - a creation date
90 *
91 * This function also initialize optional empty fields:
92 * - the URL with the permalink
93 * - the title with the URL
94 *
95 * @throws InvalidBookmarkException
96 */
97 public function validate()
98 {
99 if ($this->id === null
100 || ! is_int($this->id)
101 || empty($this->shortUrl)
102 || empty($this->created)
103 || ! $this->created instanceof DateTime
104 ) {
105 throw new InvalidBookmarkException($this);
106 }
107 if (empty($this->url)) {
108 $this->url = '?'. $this->shortUrl;
109 }
110 if (empty($this->title)) {
111 $this->title = $this->url;
112 }
113 }
114
115 /**
116 * Set the Id.
117 * If they're not already initialized, this function also set:
118 * - created: with the current datetime
119 * - shortUrl: with a generated small hash from the date and the given ID
120 *
121 * @param int $id
122 *
123 * @return Bookmark
124 */
125 public function setId($id)
126 {
127 $this->id = $id;
128 if (empty($this->created)) {
129 $this->created = new DateTime();
130 }
131 if (empty($this->shortUrl)) {
132 $this->shortUrl = link_small_hash($this->created, $this->id);
133 }
134
135 return $this;
136 }
137
138 /**
139 * Get the Id.
140 *
141 * @return int
142 */
143 public function getId()
144 {
145 return $this->id;
146 }
147
148 /**
149 * Get the ShortUrl.
150 *
151 * @return string
152 */
153 public function getShortUrl()
154 {
155 return $this->shortUrl;
156 }
157
158 /**
159 * Get the Url.
160 *
161 * @return string
162 */
163 public function getUrl()
164 {
165 return $this->url;
166 }
167
168 /**
169 * Get the Title.
170 *
171 * @return string
172 */
173 public function getTitle()
174 {
175 return $this->title;
176 }
177
178 /**
179 * Get the Description.
180 *
181 * @return string
182 */
183 public function getDescription()
184 {
185 return ! empty($this->description) ? $this->description : '';
186 }
187
188 /**
189 * Get the Created.
190 *
191 * @return DateTime
192 */
193 public function getCreated()
194 {
195 return $this->created;
196 }
197
198 /**
199 * Get the Updated.
200 *
201 * @return DateTime
202 */
203 public function getUpdated()
204 {
205 return $this->updated;
206 }
207
208 /**
209 * Set the ShortUrl.
210 *
211 * @param string $shortUrl
212 *
213 * @return Bookmark
214 */
215 public function setShortUrl($shortUrl)
216 {
217 $this->shortUrl = $shortUrl;
218
219 return $this;
220 }
221
222 /**
223 * Set the Url.
224 *
225 * @param string $url
226 * @param array $allowedProtocols
227 *
228 * @return Bookmark
229 */
230 public function setUrl($url, $allowedProtocols = [])
231 {
232 $url = trim($url);
233 if (! empty($url)) {
234 $url = whitelist_protocols($url, $allowedProtocols);
235 }
236 $this->url = $url;
237
238 return $this;
239 }
240
241 /**
242 * Set the Title.
243 *
244 * @param string $title
245 *
246 * @return Bookmark
247 */
248 public function setTitle($title)
249 {
250 $this->title = trim($title);
251
252 return $this;
253 }
254
255 /**
256 * Set the Description.
257 *
258 * @param string $description
259 *
260 * @return Bookmark
261 */
262 public function setDescription($description)
263 {
264 $this->description = $description;
265
266 return $this;
267 }
268
269 /**
270 * Set the Created.
271 * Note: you shouldn't set this manually except for special cases (like bookmark import)
272 *
273 * @param DateTime $created
274 *
275 * @return Bookmark
276 */
277 public function setCreated($created)
278 {
279 $this->created = $created;
280
281 return $this;
282 }
283
284 /**
285 * Set the Updated.
286 *
287 * @param DateTime $updated
288 *
289 * @return Bookmark
290 */
291 public function setUpdated($updated)
292 {
293 $this->updated = $updated;
294
295 return $this;
296 }
297
298 /**
299 * Get the Private.
300 *
301 * @return bool
302 */
303 public function isPrivate()
304 {
305 return $this->private ? true : false;
306 }
307
308 /**
309 * Set the Private.
310 *
311 * @param bool $private
312 *
313 * @return Bookmark
314 */
315 public function setPrivate($private)
316 {
317 $this->private = $private ? true : false;
318
319 return $this;
320 }
321
322 /**
323 * Get the Tags.
324 *
325 * @return array
326 */
327 public function getTags()
328 {
329 return is_array($this->tags) ? $this->tags : [];
330 }
331
332 /**
333 * Set the Tags.
334 *
335 * @param array $tags
336 *
337 * @return Bookmark
338 */
339 public function setTags($tags)
340 {
341 $this->setTagsString(implode(' ', $tags));
342
343 return $this;
344 }
345
346 /**
347 * Get the Thumbnail.
348 *
349 * @return string|bool
350 */
351 public function getThumbnail()
352 {
353 return !$this->isNote() ? $this->thumbnail : false;
354 }
355
356 /**
357 * Set the Thumbnail.
358 *
359 * @param string|bool $thumbnail
360 *
361 * @return Bookmark
362 */
363 public function setThumbnail($thumbnail)
364 {
365 $this->thumbnail = $thumbnail;
366
367 return $this;
368 }
369
370 /**
371 * Get the Sticky.
372 *
373 * @return bool
374 */
375 public function isSticky()
376 {
377 return $this->sticky ? true : false;
378 }
379
380 /**
381 * Set the Sticky.
382 *
383 * @param bool $sticky
384 *
385 * @return Bookmark
386 */
387 public function setSticky($sticky)
388 {
389 $this->sticky = $sticky ? true : false;
390
391 return $this;
392 }
393
394 /**
395 * @return string Bookmark's tags as a string, separated by a space
396 */
397 public function getTagsString()
398 {
399 return implode(' ', $this->getTags());
400 }
401
402 /**
403 * @return bool
404 */
405 public function isNote()
406 {
407 // We check empty value to get a valid result if the link has not been saved yet
408 return empty($this->url) || $this->url[0] === '?';
409 }
410
411 /**
412 * Set tags from a string.
413 * Note:
414 * - tags must be separated whether by a space or a comma
415 * - multiple spaces will be removed
416 * - trailing dash in tags will be removed
417 *
418 * @param string $tags
419 *
420 * @return $this
421 */
422 public function setTagsString($tags)
423 {
424 // Remove first '-' char in tags.
425 $tags = preg_replace('/(^| )\-/', '$1', $tags);
426 // Explode all tags separted by spaces or commas
427 $tags = preg_split('/[\s,]+/', $tags);
428 // Remove eventual empty values
429 $tags = array_values(array_filter($tags));
430
431 $this->tags = $tags;
432
433 return $this;
434 }
435
436 /**
437 * Rename a tag in tags list.
438 *
439 * @param string $fromTag
440 * @param string $toTag
441 */
442 public function renameTag($fromTag, $toTag)
443 {
444 if (($pos = array_search($fromTag, $this->tags)) !== false) {
445 $this->tags[$pos] = trim($toTag);
446 }
447 }
448
449 /**
450 * Delete a tag from tags list.
451 *
452 * @param string $tag
453 */
454 public function deleteTag($tag)
455 {
456 if (($pos = array_search($tag, $this->tags)) !== false) {
457 unset($this->tags[$pos]);
458 $this->tags = array_values($this->tags);
459 }
460 }
461}
diff --git a/application/bookmark/BookmarkArray.php b/application/bookmark/BookmarkArray.php
new file mode 100644
index 00000000..d87d43b4
--- /dev/null
+++ b/application/bookmark/BookmarkArray.php
@@ -0,0 +1,259 @@
1<?php
2
3namespace Shaarli\Bookmark;
4
5use Shaarli\Bookmark\Exception\InvalidBookmarkException;
6
7/**
8 * Class BookmarkArray
9 *
10 * Implementing ArrayAccess, this allows us to use the bookmark list
11 * as an array and iterate over it.
12 *
13 * @package Shaarli\Bookmark
14 */
15class BookmarkArray implements \Iterator, \Countable, \ArrayAccess
16{
17 /**
18 * @var Bookmark[]
19 */
20 protected $bookmarks;
21
22 /**
23 * @var array List of all bookmarks IDS mapped with their array offset.
24 * Map: id->offset.
25 */
26 protected $ids;
27
28 /**
29 * @var int Position in the $this->keys array (for the Iterator interface)
30 */
31 protected $position;
32
33 /**
34 * @var array List of offset keys (for the Iterator interface implementation)
35 */
36 protected $keys;
37
38 /**
39 * @var array List of all recorded URLs (key=url, value=bookmark offset)
40 * for fast reserve search (url-->bookmark offset)
41 */
42 protected $urls;
43
44 public function __construct()
45 {
46 $this->ids = [];
47 $this->bookmarks = [];
48 $this->keys = [];
49 $this->urls = [];
50 $this->position = 0;
51 }
52
53 /**
54 * Countable - Counts elements of an object
55 *
56 * @return int Number of bookmarks
57 */
58 public function count()
59 {
60 return count($this->bookmarks);
61 }
62
63 /**
64 * ArrayAccess - Assigns a value to the specified offset
65 *
66 * @param int $offset Bookmark ID
67 * @param Bookmark $value instance
68 *
69 * @throws InvalidBookmarkException
70 */
71 public function offsetSet($offset, $value)
72 {
73 if (! $value instanceof Bookmark
74 || $value->getId() === null || empty($value->getUrl())
75 || ($offset !== null && ! is_int($offset)) || ! is_int($value->getId())
76 || $offset !== null && $offset !== $value->getId()
77 ) {
78 throw new InvalidBookmarkException($value);
79 }
80
81 // If the bookmark exists, we reuse the real offset, otherwise new entry
82 if ($offset !== null) {
83 $existing = $this->getBookmarkOffset($offset);
84 } else {
85 $existing = $this->getBookmarkOffset($value->getId());
86 }
87
88 if ($existing !== null) {
89 $offset = $existing;
90 } else {
91 $offset = count($this->bookmarks);
92 }
93
94 $this->bookmarks[$offset] = $value;
95 $this->urls[$value->getUrl()] = $offset;
96 $this->ids[$value->getId()] = $offset;
97 }
98
99 /**
100 * ArrayAccess - Whether or not an offset exists
101 *
102 * @param int $offset Bookmark ID
103 *
104 * @return bool true if it exists, false otherwise
105 */
106 public function offsetExists($offset)
107 {
108 return array_key_exists($this->getBookmarkOffset($offset), $this->bookmarks);
109 }
110
111 /**
112 * ArrayAccess - Unsets an offset
113 *
114 * @param int $offset Bookmark ID
115 */
116 public function offsetUnset($offset)
117 {
118 $realOffset = $this->getBookmarkOffset($offset);
119 $url = $this->bookmarks[$realOffset]->getUrl();
120 unset($this->urls[$url]);
121 unset($this->ids[$offset]);
122 unset($this->bookmarks[$realOffset]);
123 }
124
125 /**
126 * ArrayAccess - Returns the value at specified offset
127 *
128 * @param int $offset Bookmark ID
129 *
130 * @return Bookmark|null The Bookmark if found, null otherwise
131 */
132 public function offsetGet($offset)
133 {
134 $realOffset = $this->getBookmarkOffset($offset);
135 return isset($this->bookmarks[$realOffset]) ? $this->bookmarks[$realOffset] : null;
136 }
137
138 /**
139 * Iterator - Returns the current element
140 *
141 * @return Bookmark corresponding to the current position
142 */
143 public function current()
144 {
145 return $this[$this->keys[$this->position]];
146 }
147
148 /**
149 * Iterator - Returns the key of the current element
150 *
151 * @return int Bookmark ID corresponding to the current position
152 */
153 public function key()
154 {
155 return $this->keys[$this->position];
156 }
157
158 /**
159 * Iterator - Moves forward to next element
160 */
161 public function next()
162 {
163 ++$this->position;
164 }
165
166 /**
167 * Iterator - Rewinds the Iterator to the first element
168 *
169 * Entries are sorted by date (latest first)
170 */
171 public function rewind()
172 {
173 $this->keys = array_keys($this->ids);
174 $this->position = 0;
175 }
176
177 /**
178 * Iterator - Checks if current position is valid
179 *
180 * @return bool true if the current Bookmark ID exists, false otherwise
181 */
182 public function valid()
183 {
184 return isset($this->keys[$this->position]);
185 }
186
187 /**
188 * Returns a bookmark offset in bookmarks array from its unique ID.
189 *
190 * @param int $id Persistent ID of a bookmark.
191 *
192 * @return int Real offset in local array, or null if doesn't exist.
193 */
194 protected function getBookmarkOffset($id)
195 {
196 if (isset($this->ids[$id])) {
197 return $this->ids[$id];
198 }
199 return null;
200 }
201
202 /**
203 * Return the next key for bookmark creation.
204 * E.g. If the last ID is 597, the next will be 598.
205 *
206 * @return int next ID.
207 */
208 public function getNextId()
209 {
210 if (!empty($this->ids)) {
211 return max(array_keys($this->ids)) + 1;
212 }
213 return 0;
214 }
215
216 /**
217 * @param $url
218 *
219 * @return Bookmark|null
220 */
221 public function getByUrl($url)
222 {
223 if (! empty($url)
224 && isset($this->urls[$url])
225 && isset($this->bookmarks[$this->urls[$url]])
226 ) {
227 return $this->bookmarks[$this->urls[$url]];
228 }
229 return null;
230 }
231
232 /**
233 * Reorder links by creation date (newest first).
234 *
235 * Also update the urls and ids mapping arrays.
236 *
237 * @param string $order ASC|DESC
238 */
239 public function reorder($order = 'DESC')
240 {
241 $order = $order === 'ASC' ? -1 : 1;
242 // Reorder array by dates.
243 usort($this->bookmarks, function ($a, $b) use ($order) {
244 /** @var $a Bookmark */
245 /** @var $b Bookmark */
246 if ($a->isSticky() !== $b->isSticky()) {
247 return $a->isSticky() ? -1 : 1;
248 }
249 return $a->getCreated() < $b->getCreated() ? 1 * $order : -1 * $order;
250 });
251
252 $this->urls = [];
253 $this->ids = [];
254 foreach ($this->bookmarks as $key => $bookmark) {
255 $this->urls[$bookmark->getUrl()] = $key;
256 $this->ids[$bookmark->getId()] = $key;
257 }
258 }
259}
diff --git a/application/bookmark/BookmarkFileService.php b/application/bookmark/BookmarkFileService.php
new file mode 100644
index 00000000..a56cc92b
--- /dev/null
+++ b/application/bookmark/BookmarkFileService.php
@@ -0,0 +1,373 @@
1<?php
2
3
4namespace Shaarli\Bookmark;
5
6
7use Exception;
8use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
9use Shaarli\Bookmark\Exception\EmptyDataStoreException;
10use Shaarli\Config\ConfigManager;
11use Shaarli\History;
12use Shaarli\Legacy\LegacyLinkDB;
13use Shaarli\Legacy\LegacyUpdater;
14use Shaarli\Updater\UpdaterUtils;
15
16/**
17 * Class BookmarksService
18 *
19 * This is the entry point to manipulate the bookmark DB.
20 * It manipulates loads links from a file data store containing all bookmarks.
21 *
22 * It also triggers the legacy format (bookmarks as arrays) migration.
23 */
24class BookmarkFileService implements BookmarkServiceInterface
25{
26 /** @var Bookmark[] instance */
27 protected $bookmarks;
28
29 /** @var BookmarkIO instance */
30 protected $bookmarksIO;
31
32 /** @var BookmarkFilter */
33 protected $bookmarkFilter;
34
35 /** @var ConfigManager instance */
36 protected $conf;
37
38 /** @var History instance */
39 protected $history;
40
41 /** @var bool true for logged in users. Default value to retrieve private bookmarks. */
42 protected $isLoggedIn;
43
44 /**
45 * @inheritDoc
46 */
47 public function __construct(ConfigManager $conf, History $history, $isLoggedIn)
48 {
49 $this->conf = $conf;
50 $this->history = $history;
51 $this->bookmarksIO = new BookmarkIO($this->conf);
52 $this->isLoggedIn = $isLoggedIn;
53
54 if (!$this->isLoggedIn && $this->conf->get('privacy.hide_public_links', false)) {
55 $this->bookmarks = [];
56 } else {
57 try {
58 $this->bookmarks = $this->bookmarksIO->read();
59 } catch (EmptyDataStoreException $e) {
60 $this->bookmarks = new BookmarkArray();
61 if ($isLoggedIn) {
62 $this->save();
63 }
64 }
65
66 if (! $this->bookmarks instanceof BookmarkArray) {
67 $this->migrate();
68 exit(
69 'Your data store has been migrated, please reload the page.'. PHP_EOL .
70 'If this message keeps showing up, please delete data/updates.txt file.'
71 );
72 }
73 }
74
75 $this->bookmarkFilter = new BookmarkFilter($this->bookmarks);
76 }
77
78 /**
79 * @inheritDoc
80 */
81 public function findByHash($hash)
82 {
83 $bookmark = $this->bookmarkFilter->filter(BookmarkFilter::$FILTER_HASH, $hash);
84 // PHP 7.3 introduced array_key_first() to avoid this hack
85 $first = reset($bookmark);
86 if (! $this->isLoggedIn && $first->isPrivate()) {
87 throw new Exception('Not authorized');
88 }
89
90 return $bookmark;
91 }
92
93 /**
94 * @inheritDoc
95 */
96 public function findByUrl($url)
97 {
98 return $this->bookmarks->getByUrl($url);
99 }
100
101 /**
102 * @inheritDoc
103 */
104 public function search($request = [], $visibility = null, $caseSensitive = false, $untaggedOnly = false)
105 {
106 if ($visibility === null) {
107 $visibility = $this->isLoggedIn ? BookmarkFilter::$ALL : BookmarkFilter::$PUBLIC;
108 }
109
110 // Filter bookmark database according to parameters.
111 $searchtags = isset($request['searchtags']) ? $request['searchtags'] : '';
112 $searchterm = isset($request['searchterm']) ? $request['searchterm'] : '';
113
114 return $this->bookmarkFilter->filter(
115 BookmarkFilter::$FILTER_TAG | BookmarkFilter::$FILTER_TEXT,
116 [$searchtags, $searchterm],
117 $caseSensitive,
118 $visibility,
119 $untaggedOnly
120 );
121 }
122
123 /**
124 * @inheritDoc
125 */
126 public function get($id, $visibility = null)
127 {
128 if (! isset($this->bookmarks[$id])) {
129 throw new BookmarkNotFoundException();
130 }
131
132 if ($visibility === null) {
133 $visibility = $this->isLoggedIn ? 'all' : 'public';
134 }
135
136 $bookmark = $this->bookmarks[$id];
137 if (($bookmark->isPrivate() && $visibility != 'all' && $visibility != 'private')
138 || (! $bookmark->isPrivate() && $visibility != 'all' && $visibility != 'public')
139 ) {
140 throw new Exception('Unauthorized');
141 }
142
143 return $bookmark;
144 }
145
146 /**
147 * @inheritDoc
148 */
149 public function set($bookmark, $save = true)
150 {
151 if ($this->isLoggedIn !== true) {
152 throw new Exception(t('You\'re not authorized to alter the datastore'));
153 }
154 if (! $bookmark instanceof Bookmark) {
155 throw new Exception(t('Provided data is invalid'));
156 }
157 if (! isset($this->bookmarks[$bookmark->getId()])) {
158 throw new BookmarkNotFoundException();
159 }
160 $bookmark->validate();
161
162 $bookmark->setUpdated(new \DateTime());
163 $this->bookmarks[$bookmark->getId()] = $bookmark;
164 if ($save === true) {
165 $this->save();
166 $this->history->updateLink($bookmark);
167 }
168 return $this->bookmarks[$bookmark->getId()];
169 }
170
171 /**
172 * @inheritDoc
173 */
174 public function add($bookmark, $save = true)
175 {
176 if ($this->isLoggedIn !== true) {
177 throw new Exception(t('You\'re not authorized to alter the datastore'));
178 }
179 if (! $bookmark instanceof Bookmark) {
180 throw new Exception(t('Provided data is invalid'));
181 }
182 if (! empty($bookmark->getId())) {
183 throw new Exception(t('This bookmarks already exists'));
184 }
185 $bookmark->setId($this->bookmarks->getNextId());
186 $bookmark->validate();
187
188 $this->bookmarks[$bookmark->getId()] = $bookmark;
189 if ($save === true) {
190 $this->save();
191 $this->history->addLink($bookmark);
192 }
193 return $this->bookmarks[$bookmark->getId()];
194 }
195
196 /**
197 * @inheritDoc
198 */
199 public function addOrSet($bookmark, $save = true)
200 {
201 if ($this->isLoggedIn !== true) {
202 throw new Exception(t('You\'re not authorized to alter the datastore'));
203 }
204 if (! $bookmark instanceof Bookmark) {
205 throw new Exception('Provided data is invalid');
206 }
207 if ($bookmark->getId() === null) {
208 return $this->add($bookmark, $save);
209 }
210 return $this->set($bookmark, $save);
211 }
212
213 /**
214 * @inheritDoc
215 */
216 public function remove($bookmark, $save = true)
217 {
218 if ($this->isLoggedIn !== true) {
219 throw new Exception(t('You\'re not authorized to alter the datastore'));
220 }
221 if (! $bookmark instanceof Bookmark) {
222 throw new Exception(t('Provided data is invalid'));
223 }
224 if (! isset($this->bookmarks[$bookmark->getId()])) {
225 throw new BookmarkNotFoundException();
226 }
227
228 unset($this->bookmarks[$bookmark->getId()]);
229 if ($save === true) {
230 $this->save();
231 $this->history->deleteLink($bookmark);
232 }
233 }
234
235 /**
236 * @inheritDoc
237 */
238 public function exists($id, $visibility = null)
239 {
240 if (! isset($this->bookmarks[$id])) {
241 return false;
242 }
243
244 if ($visibility === null) {
245 $visibility = $this->isLoggedIn ? 'all' : 'public';
246 }
247
248 $bookmark = $this->bookmarks[$id];
249 if (($bookmark->isPrivate() && $visibility != 'all' && $visibility != 'private')
250 || (! $bookmark->isPrivate() && $visibility != 'all' && $visibility != 'public')
251 ) {
252 return false;
253 }
254
255 return true;
256 }
257
258 /**
259 * @inheritDoc
260 */
261 public function count($visibility = null)
262 {
263 return count($this->search([], $visibility));
264 }
265
266 /**
267 * @inheritDoc
268 */
269 public function save()
270 {
271 if (!$this->isLoggedIn) {
272 // TODO: raise an Exception instead
273 die('You are not authorized to change the database.');
274 }
275 $this->bookmarks->reorder();
276 $this->bookmarksIO->write($this->bookmarks);
277 invalidateCaches($this->conf->get('resource.page_cache'));
278 }
279
280 /**
281 * @inheritDoc
282 */
283 public function bookmarksCountPerTag($filteringTags = [], $visibility = null)
284 {
285 $bookmarks = $this->search(['searchtags' => $filteringTags], $visibility);
286 $tags = [];
287 $caseMapping = [];
288 foreach ($bookmarks as $bookmark) {
289 foreach ($bookmark->getTags() as $tag) {
290 if (empty($tag) || (! $this->isLoggedIn && startsWith($tag, '.'))) {
291 continue;
292 }
293 // The first case found will be displayed.
294 if (!isset($caseMapping[strtolower($tag)])) {
295 $caseMapping[strtolower($tag)] = $tag;
296 $tags[$caseMapping[strtolower($tag)]] = 0;
297 }
298 $tags[$caseMapping[strtolower($tag)]]++;
299 }
300 }
301
302 /*
303 * Formerly used arsort(), which doesn't define the sort behaviour for equal values.
304 * Also, this function doesn't produce the same result between PHP 5.6 and 7.
305 *
306 * So we now use array_multisort() to sort tags by DESC occurrences,
307 * then ASC alphabetically for equal values.
308 *
309 * @see https://github.com/shaarli/Shaarli/issues/1142
310 */
311 $keys = array_keys($tags);
312 $tmpTags = array_combine($keys, $keys);
313 array_multisort($tags, SORT_DESC, $tmpTags, SORT_ASC, $tags);
314 return $tags;
315 }
316
317 /**
318 * @inheritDoc
319 */
320 public function days()
321 {
322 $bookmarkDays = [];
323 foreach ($this->search() as $bookmark) {
324 $bookmarkDays[$bookmark->getCreated()->format('Ymd')] = 0;
325 }
326 $bookmarkDays = array_keys($bookmarkDays);
327 sort($bookmarkDays);
328
329 return $bookmarkDays;
330 }
331
332 /**
333 * @inheritDoc
334 */
335 public function filterDay($request)
336 {
337 return $this->bookmarkFilter->filter(BookmarkFilter::$FILTER_DAY, $request);
338 }
339
340 /**
341 * @inheritDoc
342 */
343 public function initialize()
344 {
345 $initializer = new BookmarkInitializer($this);
346 $initializer->initialize();
347 }
348
349 /**
350 * Handles migration to the new database format (BookmarksArray).
351 */
352 protected function migrate()
353 {
354 $bookmarkDb = new LegacyLinkDB(
355 $this->conf->get('resource.datastore'),
356 true,
357 false
358 );
359 $updater = new LegacyUpdater(
360 UpdaterUtils::read_updates_file($this->conf->get('resource.updates')),
361 $bookmarkDb,
362 $this->conf,
363 true
364 );
365 $newUpdates = $updater->update();
366 if (! empty($newUpdates)) {
367 UpdaterUtils::write_updates_file(
368 $this->conf->get('resource.updates'),
369 $updater->getDoneUpdates()
370 );
371 }
372 }
373}
diff --git a/application/bookmark/BookmarkFilter.php b/application/bookmark/BookmarkFilter.php
new file mode 100644
index 00000000..fd556679
--- /dev/null
+++ b/application/bookmark/BookmarkFilter.php
@@ -0,0 +1,468 @@
1<?php
2
3namespace Shaarli\Bookmark;
4
5use Exception;
6use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
7
8/**
9 * Class LinkFilter.
10 *
11 * Perform search and filter operation on link data list.
12 */
13class BookmarkFilter
14{
15 /**
16 * @var string permalinks.
17 */
18 public static $FILTER_HASH = 'permalink';
19
20 /**
21 * @var string text search.
22 */
23 public static $FILTER_TEXT = 'fulltext';
24
25 /**
26 * @var string tag filter.
27 */
28 public static $FILTER_TAG = 'tags';
29
30 /**
31 * @var string filter by day.
32 */
33 public static $FILTER_DAY = 'FILTER_DAY';
34
35 /**
36 * @var string filter by day.
37 */
38 public static $DEFAULT = 'NO_FILTER';
39
40 /** @var string Visibility: all */
41 public static $ALL = 'all';
42
43 /** @var string Visibility: public */
44 public static $PUBLIC = 'public';
45
46 /** @var string Visibility: private */
47 public static $PRIVATE = 'private';
48
49 /**
50 * @var string Allowed characters for hashtags (regex syntax).
51 */
52 public static $HASHTAG_CHARS = '\p{Pc}\p{N}\p{L}\p{Mn}';
53
54 /**
55 * @var Bookmark[] all available bookmarks.
56 */
57 private $bookmarks;
58
59 /**
60 * @param Bookmark[] $bookmarks initialization.
61 */
62 public function __construct($bookmarks)
63 {
64 $this->bookmarks = $bookmarks;
65 }
66
67 /**
68 * Filter bookmarks according to parameters.
69 *
70 * @param string $type Type of filter (eg. tags, permalink, etc.).
71 * @param mixed $request Filter content.
72 * @param bool $casesensitive Optional: Perform case sensitive filter if true.
73 * @param string $visibility Optional: return only all/private/public bookmarks
74 * @param bool $untaggedonly Optional: return only untagged bookmarks. Applies only if $type includes FILTER_TAG
75 *
76 * @return Bookmark[] filtered bookmark list.
77 *
78 * @throws BookmarkNotFoundException
79 */
80 public function filter($type, $request, $casesensitive = false, $visibility = 'all', $untaggedonly = false)
81 {
82 if (!in_array($visibility, ['all', 'public', 'private'])) {
83 $visibility = 'all';
84 }
85
86 switch ($type) {
87 case self::$FILTER_HASH:
88 return $this->filterSmallHash($request);
89 case self::$FILTER_TAG | self::$FILTER_TEXT: // == "vuotext"
90 $noRequest = empty($request) || (empty($request[0]) && empty($request[1]));
91 if ($noRequest) {
92 if ($untaggedonly) {
93 return $this->filterUntagged($visibility);
94 }
95 return $this->noFilter($visibility);
96 }
97 if ($untaggedonly) {
98 $filtered = $this->filterUntagged($visibility);
99 } else {
100 $filtered = $this->bookmarks;
101 }
102 if (!empty($request[0])) {
103 $filtered = (new BookmarkFilter($filtered))->filterTags($request[0], $casesensitive, $visibility);
104 }
105 if (!empty($request[1])) {
106 $filtered = (new BookmarkFilter($filtered))->filterFulltext($request[1], $visibility);
107 }
108 return $filtered;
109 case self::$FILTER_TEXT:
110 return $this->filterFulltext($request, $visibility);
111 case self::$FILTER_TAG:
112 if ($untaggedonly) {
113 return $this->filterUntagged($visibility);
114 } else {
115 return $this->filterTags($request, $casesensitive, $visibility);
116 }
117 case self::$FILTER_DAY:
118 return $this->filterDay($request);
119 default:
120 return $this->noFilter($visibility);
121 }
122 }
123
124 /**
125 * Unknown filter, but handle private only.
126 *
127 * @param string $visibility Optional: return only all/private/public bookmarks
128 *
129 * @return Bookmark[] filtered bookmarks.
130 */
131 private function noFilter($visibility = 'all')
132 {
133 if ($visibility === 'all') {
134 return $this->bookmarks;
135 }
136
137 $out = array();
138 foreach ($this->bookmarks as $key => $value) {
139 if ($value->isPrivate() && $visibility === 'private') {
140 $out[$key] = $value;
141 } elseif (!$value->isPrivate() && $visibility === 'public') {
142 $out[$key] = $value;
143 }
144 }
145
146 return $out;
147 }
148
149 /**
150 * Returns the shaare corresponding to a smallHash.
151 *
152 * @param string $smallHash permalink hash.
153 *
154 * @return array $filtered array containing permalink data.
155 *
156 * @throws \Shaarli\Bookmark\Exception\BookmarkNotFoundException if the smallhash doesn't match any link.
157 */
158 private function filterSmallHash($smallHash)
159 {
160 foreach ($this->bookmarks as $key => $l) {
161 if ($smallHash == $l->getShortUrl()) {
162 // Yes, this is ugly and slow
163 return [$key => $l];
164 }
165 }
166
167 throw new BookmarkNotFoundException();
168 }
169
170 /**
171 * Returns the list of bookmarks corresponding to a full-text search
172 *
173 * Searches:
174 * - in the URLs, title and description;
175 * - are case-insensitive;
176 * - terms surrounded by quotes " are exact terms search.
177 * - terms starting with a dash - are excluded (except exact terms).
178 *
179 * Example:
180 * print_r($mydb->filterFulltext('hollandais'));
181 *
182 * mb_convert_case($val, MB_CASE_LOWER, 'UTF-8')
183 * - allows to perform searches on Unicode text
184 * - see https://github.com/shaarli/Shaarli/issues/75 for examples
185 *
186 * @param string $searchterms search query.
187 * @param string $visibility Optional: return only all/private/public bookmarks.
188 *
189 * @return array search results.
190 */
191 private function filterFulltext($searchterms, $visibility = 'all')
192 {
193 if (empty($searchterms)) {
194 return $this->noFilter($visibility);
195 }
196
197 $filtered = array();
198 $search = mb_convert_case(html_entity_decode($searchterms), MB_CASE_LOWER, 'UTF-8');
199 $exactRegex = '/"([^"]+)"/';
200 // Retrieve exact search terms.
201 preg_match_all($exactRegex, $search, $exactSearch);
202 $exactSearch = array_values(array_filter($exactSearch[1]));
203
204 // Remove exact search terms to get AND terms search.
205 $explodedSearchAnd = explode(' ', trim(preg_replace($exactRegex, '', $search)));
206 $explodedSearchAnd = array_values(array_filter($explodedSearchAnd));
207
208 // Filter excluding terms and update andSearch.
209 $excludeSearch = array();
210 $andSearch = array();
211 foreach ($explodedSearchAnd as $needle) {
212 if ($needle[0] == '-' && strlen($needle) > 1) {
213 $excludeSearch[] = substr($needle, 1);
214 } else {
215 $andSearch[] = $needle;
216 }
217 }
218
219 // Iterate over every stored link.
220 foreach ($this->bookmarks as $id => $link) {
221 // ignore non private bookmarks when 'privatonly' is on.
222 if ($visibility !== 'all') {
223 if (!$link->isPrivate() && $visibility === 'private') {
224 continue;
225 } elseif ($link->isPrivate() && $visibility === 'public') {
226 continue;
227 }
228 }
229
230 // Concatenate link fields to search across fields.
231 // Adds a '\' separator for exact search terms.
232 $content = mb_convert_case($link->getTitle(), MB_CASE_LOWER, 'UTF-8') .'\\';
233 $content .= mb_convert_case($link->getDescription(), MB_CASE_LOWER, 'UTF-8') .'\\';
234 $content .= mb_convert_case($link->getUrl(), MB_CASE_LOWER, 'UTF-8') .'\\';
235 $content .= mb_convert_case($link->getTagsString(), MB_CASE_LOWER, 'UTF-8') .'\\';
236
237 // Be optimistic
238 $found = true;
239
240 // First, we look for exact term search
241 for ($i = 0; $i < count($exactSearch) && $found; $i++) {
242 $found = strpos($content, $exactSearch[$i]) !== false;
243 }
244
245 // Iterate over keywords, if keyword is not found,
246 // no need to check for the others. We want all or nothing.
247 for ($i = 0; $i < count($andSearch) && $found; $i++) {
248 $found = strpos($content, $andSearch[$i]) !== false;
249 }
250
251 // Exclude terms.
252 for ($i = 0; $i < count($excludeSearch) && $found; $i++) {
253 $found = strpos($content, $excludeSearch[$i]) === false;
254 }
255
256 if ($found) {
257 $filtered[$id] = $link;
258 }
259 }
260
261 return $filtered;
262 }
263
264 /**
265 * generate a regex fragment out of a tag
266 *
267 * @param string $tag to to generate regexs from. may start with '-' to negate, contain '*' as wildcard
268 *
269 * @return string generated regex fragment
270 */
271 private static function tag2regex($tag)
272 {
273 $len = strlen($tag);
274 if (!$len || $tag === "-" || $tag === "*") {
275 // nothing to search, return empty regex
276 return '';
277 }
278 if ($tag[0] === "-") {
279 // query is negated
280 $i = 1; // use offset to start after '-' character
281 $regex = '(?!'; // create negative lookahead
282 } else {
283 $i = 0; // start at first character
284 $regex = '(?='; // use positive lookahead
285 }
286 $regex .= '.*(?:^| )'; // before tag may only be a space or the beginning
287 // iterate over string, separating it into placeholder and content
288 for (; $i < $len; $i++) {
289 if ($tag[$i] === '*') {
290 // placeholder found
291 $regex .= '[^ ]*?';
292 } else {
293 // regular characters
294 $offset = strpos($tag, '*', $i);
295 if ($offset === false) {
296 // no placeholder found, set offset to end of string
297 $offset = $len;
298 }
299 // subtract one, as we want to get before the placeholder or end of string
300 $offset -= 1;
301 // we got a tag name that we want to search for. escape any regex characters to prevent conflicts.
302 $regex .= preg_quote(substr($tag, $i, $offset - $i + 1), '/');
303 // move $i on
304 $i = $offset;
305 }
306 }
307 $regex .= '(?:$| ))'; // after the tag may only be a space or the end
308 return $regex;
309 }
310
311 /**
312 * Returns the list of bookmarks associated with a given list of tags
313 *
314 * You can specify one or more tags, separated by space or a comma, e.g.
315 * print_r($mydb->filterTags('linux programming'));
316 *
317 * @param string $tags list of tags separated by commas or blank spaces.
318 * @param bool $casesensitive ignore case if false.
319 * @param string $visibility Optional: return only all/private/public bookmarks.
320 *
321 * @return array filtered bookmarks.
322 */
323 public function filterTags($tags, $casesensitive = false, $visibility = 'all')
324 {
325 // get single tags (we may get passed an array, even though the docs say different)
326 $inputTags = $tags;
327 if (!is_array($tags)) {
328 // we got an input string, split tags
329 $inputTags = preg_split('/(?:\s+)|,/', $inputTags, -1, PREG_SPLIT_NO_EMPTY);
330 }
331
332 if (!count($inputTags)) {
333 // no input tags
334 return $this->noFilter($visibility);
335 }
336
337 // If we only have public visibility, we can't look for hidden tags
338 if ($visibility === self::$PUBLIC) {
339 $inputTags = array_values(array_filter($inputTags, function ($tag) {
340 return ! startsWith($tag, '.');
341 }));
342
343 if (empty($inputTags)) {
344 return [];
345 }
346 }
347
348 // build regex from all tags
349 $re = '/^' . implode(array_map("self::tag2regex", $inputTags)) . '.*$/';
350 if (!$casesensitive) {
351 // make regex case insensitive
352 $re .= 'i';
353 }
354
355 // create resulting array
356 $filtered = [];
357
358 // iterate over each link
359 foreach ($this->bookmarks as $key => $link) {
360 // check level of visibility
361 // ignore non private bookmarks when 'privateonly' is on.
362 if ($visibility !== 'all') {
363 if (!$link->isPrivate() && $visibility === 'private') {
364 continue;
365 } elseif ($link->isPrivate() && $visibility === 'public') {
366 continue;
367 }
368 }
369 $search = $link->getTagsString(); // build search string, start with tags of current link
370 if (strlen(trim($link->getDescription())) && strpos($link->getDescription(), '#') !== false) {
371 // description given and at least one possible tag found
372 $descTags = array();
373 // find all tags in the form of #tag in the description
374 preg_match_all(
375 '/(?<![' . self::$HASHTAG_CHARS . '])#([' . self::$HASHTAG_CHARS . ']+?)\b/sm',
376 $link->getDescription(),
377 $descTags
378 );
379 if (count($descTags[1])) {
380 // there were some tags in the description, add them to the search string
381 $search .= ' ' . implode(' ', $descTags[1]);
382 }
383 };
384 // match regular expression with search string
385 if (!preg_match($re, $search)) {
386 // this entry does _not_ match our regex
387 continue;
388 }
389 $filtered[$key] = $link;
390 }
391 return $filtered;
392 }
393
394 /**
395 * Return only bookmarks without any tag.
396 *
397 * @param string $visibility return only all/private/public bookmarks.
398 *
399 * @return array filtered bookmarks.
400 */
401 public function filterUntagged($visibility)
402 {
403 $filtered = [];
404 foreach ($this->bookmarks as $key => $link) {
405 if ($visibility !== 'all') {
406 if (!$link->isPrivate() && $visibility === 'private') {
407 continue;
408 } elseif ($link->isPrivate() && $visibility === 'public') {
409 continue;
410 }
411 }
412
413 if (empty(trim($link->getTagsString()))) {
414 $filtered[$key] = $link;
415 }
416 }
417
418 return $filtered;
419 }
420
421 /**
422 * Returns the list of articles for a given day, chronologically sorted
423 *
424 * Day must be in the form 'YYYYMMDD' (e.g. '20120125'), e.g.
425 * print_r($mydb->filterDay('20120125'));
426 *
427 * @param string $day day to filter.
428 *
429 * @return array all link matching given day.
430 *
431 * @throws Exception if date format is invalid.
432 */
433 public function filterDay($day)
434 {
435 if (!checkDateFormat('Ymd', $day)) {
436 throw new Exception('Invalid date format');
437 }
438
439 $filtered = array();
440 foreach ($this->bookmarks as $key => $l) {
441 if ($l->getCreated()->format('Ymd') == $day) {
442 $filtered[$key] = $l;
443 }
444 }
445
446 // sort by date ASC
447 return array_reverse($filtered, true);
448 }
449
450 /**
451 * Convert a list of tags (str) to an array. Also
452 * - handle case sensitivity.
453 * - accepts spaces commas as separator.
454 *
455 * @param string $tags string containing a list of tags.
456 * @param bool $casesensitive will convert everything to lowercase if false.
457 *
458 * @return array filtered tags string.
459 */
460 public static function tagsStrToArray($tags, $casesensitive)
461 {
462 // We use UTF-8 conversion to handle various graphemes (i.e. cyrillic, or greek)
463 $tagsOut = $casesensitive ? $tags : mb_convert_case($tags, MB_CASE_LOWER, 'UTF-8');
464 $tagsOut = str_replace(',', ' ', $tagsOut);
465
466 return preg_split('/\s+/', $tagsOut, -1, PREG_SPLIT_NO_EMPTY);
467 }
468}
diff --git a/application/bookmark/BookmarkIO.php b/application/bookmark/BookmarkIO.php
new file mode 100644
index 00000000..ae9ffcb4
--- /dev/null
+++ b/application/bookmark/BookmarkIO.php
@@ -0,0 +1,108 @@
1<?php
2
3namespace Shaarli\Bookmark;
4
5use Shaarli\Bookmark\Exception\EmptyDataStoreException;
6use Shaarli\Bookmark\Exception\NotWritableDataStoreException;
7use Shaarli\Config\ConfigManager;
8
9/**
10 * Class BookmarkIO
11 *
12 * This class performs read/write operation to the file data store.
13 * Used by BookmarkFileService.
14 *
15 * @package Shaarli\Bookmark
16 */
17class BookmarkIO
18{
19 /**
20 * @var string Datastore file path
21 */
22 protected $datastore;
23
24 /**
25 * @var ConfigManager instance
26 */
27 protected $conf;
28
29 /**
30 * string Datastore PHP prefix
31 */
32 protected static $phpPrefix = '<?php /* ';
33
34 /**
35 * string Datastore PHP suffix
36 */
37 protected static $phpSuffix = ' */ ?>';
38
39 /**
40 * LinksIO constructor.
41 *
42 * @param ConfigManager $conf instance
43 */
44 public function __construct($conf)
45 {
46 $this->conf = $conf;
47 $this->datastore = $conf->get('resource.datastore');
48 }
49
50 /**
51 * Reads database from disk to memory
52 *
53 * @return BookmarkArray instance
54 *
55 * @throws NotWritableDataStoreException Data couldn't be loaded
56 * @throws EmptyDataStoreException Datastore doesn't exist
57 */
58 public function read()
59 {
60 if (! file_exists($this->datastore)) {
61 throw new EmptyDataStoreException();
62 }
63
64 if (!is_writable($this->datastore)) {
65 throw new NotWritableDataStoreException($this->datastore);
66 }
67
68 // Note that gzinflate is faster than gzuncompress.
69 // See: http://www.php.net/manual/en/function.gzdeflate.php#96439
70 $links = unserialize(gzinflate(base64_decode(
71 substr(file_get_contents($this->datastore),
72 strlen(self::$phpPrefix), -strlen(self::$phpSuffix)))));
73
74 if (empty($links)) {
75 if (filesize($this->datastore) > 100) {
76 throw new NotWritableDataStoreException($this->datastore);
77 }
78 throw new EmptyDataStoreException();
79 }
80
81 return $links;
82 }
83
84 /**
85 * Saves the database from memory to disk
86 *
87 * @param BookmarkArray $links instance.
88 *
89 * @throws NotWritableDataStoreException the datastore is not writable
90 */
91 public function write($links)
92 {
93 if (is_file($this->datastore) && !is_writeable($this->datastore)) {
94 // The datastore exists but is not writeable
95 throw new NotWritableDataStoreException($this->datastore);
96 } else if (!is_file($this->datastore) && !is_writeable(dirname($this->datastore))) {
97 // The datastore does not exist and its parent directory is not writeable
98 throw new NotWritableDataStoreException(dirname($this->datastore));
99 }
100
101 file_put_contents(
102 $this->datastore,
103 self::$phpPrefix.base64_encode(gzdeflate(serialize($links))).self::$phpSuffix
104 );
105
106 invalidateCaches($this->conf->get('resource.page_cache'));
107 }
108}
diff --git a/application/bookmark/BookmarkInitializer.php b/application/bookmark/BookmarkInitializer.php
new file mode 100644
index 00000000..9eee9a35
--- /dev/null
+++ b/application/bookmark/BookmarkInitializer.php
@@ -0,0 +1,59 @@
1<?php
2
3namespace Shaarli\Bookmark;
4
5/**
6 * Class BookmarkInitializer
7 *
8 * This class is used to initialized default bookmarks after a fresh install of Shaarli.
9 * It is no longer call when the data store is empty,
10 * because user might want to delete default bookmarks after the install.
11 *
12 * To prevent data corruption, it does not overwrite existing bookmarks,
13 * even though there should not be any.
14 *
15 * @package Shaarli\Bookmark
16 */
17class BookmarkInitializer
18{
19 /** @var BookmarkServiceInterface */
20 protected $bookmarkService;
21
22 /**
23 * BookmarkInitializer constructor.
24 *
25 * @param BookmarkServiceInterface $bookmarkService
26 */
27 public function __construct($bookmarkService)
28 {
29 $this->bookmarkService = $bookmarkService;
30 }
31
32 /**
33 * Initialize the data store with default bookmarks
34 */
35 public function initialize()
36 {
37 $bookmark = new Bookmark();
38 $bookmark->setTitle(t('My secret stuff... - Pastebin.com'));
39 $bookmark->setUrl('http://sebsauvage.net/paste/?8434b27936c09649#bR7XsXhoTiLcqCpQbmOpBi3rq2zzQUC5hBI7ZT1O3x8=', []);
40 $bookmark->setDescription(t('Shhhh! I\'m a private link only YOU can see. You can delete me too.'));
41 $bookmark->setTagsString('secretstuff');
42 $bookmark->setPrivate(true);
43 $this->bookmarkService->add($bookmark);
44
45 $bookmark = new Bookmark();
46 $bookmark->setTitle(t('The personal, minimalist, super-fast, database free, bookmarking service'));
47 $bookmark->setUrl('https://shaarli.readthedocs.io', []);
48 $bookmark->setDescription(t(
49 'Welcome to Shaarli! This is your first public bookmark. '
50 . 'To edit or delete me, you must first login.
51
52To learn how to use Shaarli, consult the link "Documentation" at the bottom of this page.
53
54You use the community supported version of the original Shaarli project, by Sebastien Sauvage.'
55 ));
56 $bookmark->setTagsString('opensource software');
57 $this->bookmarkService->add($bookmark);
58 }
59}
diff --git a/application/bookmark/BookmarkServiceInterface.php b/application/bookmark/BookmarkServiceInterface.php
new file mode 100644
index 00000000..7b7a4f09
--- /dev/null
+++ b/application/bookmark/BookmarkServiceInterface.php
@@ -0,0 +1,180 @@
1<?php
2
3namespace Shaarli\Bookmark;
4
5
6use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
7use Shaarli\Bookmark\Exception\NotWritableDataStoreException;
8use Shaarli\Config\ConfigManager;
9use Shaarli\Exceptions\IOException;
10use Shaarli\History;
11
12/**
13 * Class BookmarksService
14 *
15 * This is the entry point to manipulate the bookmark DB.
16 */
17interface BookmarkServiceInterface
18{
19 /**
20 * BookmarksService constructor.
21 *
22 * @param ConfigManager $conf instance
23 * @param History $history instance
24 * @param bool $isLoggedIn true if the current user is logged in
25 */
26 public function __construct(ConfigManager $conf, History $history, $isLoggedIn);
27
28 /**
29 * Find a bookmark by hash
30 *
31 * @param string $hash
32 *
33 * @return mixed
34 *
35 * @throws \Exception
36 */
37 public function findByHash($hash);
38
39 /**
40 * @param $url
41 *
42 * @return Bookmark|null
43 */
44 public function findByUrl($url);
45
46 /**
47 * Search bookmarks
48 *
49 * @param mixed $request
50 * @param string $visibility
51 * @param bool $caseSensitive
52 * @param bool $untaggedOnly
53 *
54 * @return Bookmark[]
55 */
56 public function search($request = [], $visibility = null, $caseSensitive = false, $untaggedOnly = false);
57
58 /**
59 * Get a single bookmark by its ID.
60 *
61 * @param int $id Bookmark ID
62 * @param string $visibility all|public|private e.g. with public, accessing a private bookmark will throw an
63 * exception
64 *
65 * @return Bookmark
66 *
67 * @throws BookmarkNotFoundException
68 * @throws \Exception
69 */
70 public function get($id, $visibility = null);
71
72 /**
73 * Updates an existing bookmark (depending on its ID).
74 *
75 * @param Bookmark $bookmark
76 * @param bool $save Writes to the datastore if set to true
77 *
78 * @return Bookmark Updated bookmark
79 *
80 * @throws BookmarkNotFoundException
81 * @throws \Exception
82 */
83 public function set($bookmark, $save = true);
84
85 /**
86 * Adds a new bookmark (the ID must be empty).
87 *
88 * @param Bookmark $bookmark
89 * @param bool $save Writes to the datastore if set to true
90 *
91 * @return Bookmark new bookmark
92 *
93 * @throws \Exception
94 */
95 public function add($bookmark, $save = true);
96
97 /**
98 * Adds or updates a bookmark depending on its ID:
99 * - a Bookmark without ID will be added
100 * - a Bookmark with an existing ID will be updated
101 *
102 * @param Bookmark $bookmark
103 * @param bool $save
104 *
105 * @return Bookmark
106 *
107 * @throws \Exception
108 */
109 public function addOrSet($bookmark, $save = true);
110
111 /**
112 * Deletes a bookmark.
113 *
114 * @param Bookmark $bookmark
115 * @param bool $save
116 *
117 * @throws \Exception
118 */
119 public function remove($bookmark, $save = true);
120
121 /**
122 * Get a single bookmark by its ID.
123 *
124 * @param int $id Bookmark ID
125 * @param string $visibility all|public|private e.g. with public, accessing a private bookmark will throw an
126 * exception
127 *
128 * @return bool
129 */
130 public function exists($id, $visibility = null);
131
132 /**
133 * Return the number of available bookmarks for given visibility.
134 *
135 * @param string $visibility public|private|all
136 *
137 * @return int Number of bookmarks
138 */
139 public function count($visibility = null);
140
141 /**
142 * Write the datastore.
143 *
144 * @throws NotWritableDataStoreException
145 */
146 public function save();
147
148 /**
149 * Returns the list tags appearing in the bookmarks with the given tags
150 *
151 * @param array $filteringTags tags selecting the bookmarks to consider
152 * @param string $visibility process only all/private/public bookmarks
153 *
154 * @return array tag => bookmarksCount
155 */
156 public function bookmarksCountPerTag($filteringTags = [], $visibility = 'all');
157
158 /**
159 * Returns the list of days containing articles (oldest first)
160 *
161 * @return array containing days (in format YYYYMMDD).
162 */
163 public function days();
164
165 /**
166 * Returns the list of articles for a given day.
167 *
168 * @param string $request day to filter. Format: YYYYMMDD.
169 *
170 * @return Bookmark[] list of shaare found.
171 *
172 * @throws BookmarkNotFoundException
173 */
174 public function filterDay($request);
175
176 /**
177 * Creates the default database after a fresh install.
178 */
179 public function initialize();
180}
diff --git a/application/bookmark/LinkUtils.php b/application/bookmark/LinkUtils.php
index 77eb2d95..88379430 100644
--- a/application/bookmark/LinkUtils.php
+++ b/application/bookmark/LinkUtils.php
@@ -1,6 +1,6 @@
1<?php 1<?php
2 2
3use Shaarli\Bookmark\LinkDB; 3use Shaarli\Bookmark\Bookmark;
4 4
5/** 5/**
6 * Get cURL callback function for CURLOPT_WRITEFUNCTION 6 * Get cURL callback function for CURLOPT_WRITEFUNCTION
@@ -188,30 +188,11 @@ function html_extract_tag($tag, $html)
188} 188}
189 189
190/** 190/**
191 * Count private links in given linklist. 191 * In a string, converts URLs to clickable bookmarks.
192 *
193 * @param array|Countable $links Linklist.
194 *
195 * @return int Number of private links.
196 */
197function count_private($links)
198{
199 $cpt = 0;
200 foreach ($links as $link) {
201 if ($link['private']) {
202 $cpt += 1;
203 }
204 }
205
206 return $cpt;
207}
208
209/**
210 * In a string, converts URLs to clickable links.
211 * 192 *
212 * @param string $text input string. 193 * @param string $text input string.
213 * 194 *
214 * @return string returns $text with all links converted to HTML links. 195 * @return string returns $text with all bookmarks converted to HTML bookmarks.
215 * 196 *
216 * @see Function inspired from http://www.php.net/manual/en/function.preg-replace.php#85722 197 * @see Function inspired from http://www.php.net/manual/en/function.preg-replace.php#85722
217 */ 198 */
@@ -279,7 +260,7 @@ function format_description($description, $indexUrl = '')
279 */ 260 */
280function link_small_hash($date, $id) 261function link_small_hash($date, $id)
281{ 262{
282 return smallHash($date->format(LinkDB::LINK_DATE_FORMAT) . $id); 263 return smallHash($date->format(Bookmark::LINK_DATE_FORMAT) . $id);
283} 264}
284 265
285/** 266/**
diff --git a/application/bookmark/exception/LinkNotFoundException.php b/application/bookmark/exception/BookmarkNotFoundException.php
index f9414428..827a3d35 100644
--- a/application/bookmark/exception/LinkNotFoundException.php
+++ b/application/bookmark/exception/BookmarkNotFoundException.php
@@ -3,7 +3,7 @@ namespace Shaarli\Bookmark\Exception;
3 3
4use Exception; 4use Exception;
5 5
6class LinkNotFoundException extends Exception 6class BookmarkNotFoundException extends Exception
7{ 7{
8 /** 8 /**
9 * LinkNotFoundException constructor. 9 * LinkNotFoundException constructor.
diff --git a/application/bookmark/exception/EmptyDataStoreException.php b/application/bookmark/exception/EmptyDataStoreException.php
new file mode 100644
index 00000000..cd48c1e6
--- /dev/null
+++ b/application/bookmark/exception/EmptyDataStoreException.php
@@ -0,0 +1,7 @@
1<?php
2
3
4namespace Shaarli\Bookmark\Exception;
5
6
7class EmptyDataStoreException extends \Exception {}
diff --git a/application/bookmark/exception/InvalidBookmarkException.php b/application/bookmark/exception/InvalidBookmarkException.php
new file mode 100644
index 00000000..10c84a6d
--- /dev/null
+++ b/application/bookmark/exception/InvalidBookmarkException.php
@@ -0,0 +1,30 @@
1<?php
2
3namespace Shaarli\Bookmark\Exception;
4
5use Shaarli\Bookmark\Bookmark;
6
7class InvalidBookmarkException extends \Exception
8{
9 public function __construct($bookmark)
10 {
11 if ($bookmark instanceof Bookmark) {
12 if ($bookmark->getCreated() instanceof \DateTime) {
13 $created = $bookmark->getCreated()->format(\DateTime::ATOM);
14 } elseif (empty($bookmark->getCreated())) {
15 $created = '';
16 } else {
17 $created = 'Not a DateTime object';
18 }
19 $this->message = 'This bookmark is not valid'. PHP_EOL;
20 $this->message .= ' - ID: '. $bookmark->getId() . PHP_EOL;
21 $this->message .= ' - Title: '. $bookmark->getTitle() . PHP_EOL;
22 $this->message .= ' - Url: '. $bookmark->getUrl() . PHP_EOL;
23 $this->message .= ' - ShortUrl: '. $bookmark->getShortUrl() . PHP_EOL;
24 $this->message .= ' - Created: '. $created . PHP_EOL;
25 } else {
26 $this->message = 'The provided data is not a bookmark'. PHP_EOL;
27 $this->message .= var_export($bookmark, true);
28 }
29 }
30}
diff --git a/application/bookmark/exception/NotWritableDataStoreException.php b/application/bookmark/exception/NotWritableDataStoreException.php
new file mode 100644
index 00000000..95f34b50
--- /dev/null
+++ b/application/bookmark/exception/NotWritableDataStoreException.php
@@ -0,0 +1,19 @@
1<?php
2
3
4namespace Shaarli\Bookmark\Exception;
5
6
7class NotWritableDataStoreException extends \Exception
8{
9 /**
10 * NotReadableDataStore constructor.
11 *
12 * @param string $dataStore file path
13 */
14 public function __construct($dataStore)
15 {
16 $this->message = 'Couldn\'t load data from the data store file "'. $dataStore .'". '.
17 'Your data might be corrupted, or your file isn\'t readable.';
18 }
19}
diff --git a/application/config/ConfigManager.php b/application/config/ConfigManager.php
index c95e6800..e45bb4c3 100644
--- a/application/config/ConfigManager.php
+++ b/application/config/ConfigManager.php
@@ -389,6 +389,8 @@ class ConfigManager
389 $this->setEmpty('translation.extensions', []); 389 $this->setEmpty('translation.extensions', []);
390 390
391 $this->setEmpty('plugins', array()); 391 $this->setEmpty('plugins', array());
392
393 $this->setEmpty('formatter', 'markdown');
392 } 394 }
393 395
394 /** 396 /**
diff --git a/application/feed/FeedBuilder.php b/application/feed/FeedBuilder.php
index 957c8273..40bd4f15 100644
--- a/application/feed/FeedBuilder.php
+++ b/application/feed/FeedBuilder.php
@@ -2,6 +2,9 @@
2namespace Shaarli\Feed; 2namespace Shaarli\Feed;
3 3
4use DateTime; 4use DateTime;
5use Shaarli\Bookmark\Bookmark;
6use Shaarli\Bookmark\BookmarkServiceInterface;
7use Shaarli\Formatter\BookmarkFormatter;
5 8
6/** 9/**
7 * FeedBuilder class. 10 * FeedBuilder class.
@@ -26,16 +29,21 @@ class FeedBuilder
26 public static $DEFAULT_LANGUAGE = 'en-en'; 29 public static $DEFAULT_LANGUAGE = 'en-en';
27 30
28 /** 31 /**
29 * @var int Number of links to display in a feed by default. 32 * @var int Number of bookmarks to display in a feed by default.
30 */ 33 */
31 public static $DEFAULT_NB_LINKS = 50; 34 public static $DEFAULT_NB_LINKS = 50;
32 35
33 /** 36 /**
34 * @var \Shaarli\Bookmark\LinkDB instance. 37 * @var BookmarkServiceInterface instance.
35 */ 38 */
36 protected $linkDB; 39 protected $linkDB;
37 40
38 /** 41 /**
42 * @var BookmarkFormatter instance.
43 */
44 protected $formatter;
45
46 /**
39 * @var string RSS or ATOM feed. 47 * @var string RSS or ATOM feed.
40 */ 48 */
41 protected $feedType; 49 protected $feedType;
@@ -56,7 +64,7 @@ class FeedBuilder
56 protected $isLoggedIn; 64 protected $isLoggedIn;
57 65
58 /** 66 /**
59 * @var boolean Use permalinks instead of direct links if true. 67 * @var boolean Use permalinks instead of direct bookmarks if true.
60 */ 68 */
61 protected $usePermalinks; 69 protected $usePermalinks;
62 70
@@ -78,16 +86,17 @@ class FeedBuilder
78 /** 86 /**
79 * Feed constructor. 87 * Feed constructor.
80 * 88 *
81 * @param \Shaarli\Bookmark\LinkDB $linkDB LinkDB instance. 89 * @param BookmarkServiceInterface $linkDB LinkDB instance.
90 * @param BookmarkFormatter $formatter instance.
82 * @param string $feedType Type of feed. 91 * @param string $feedType Type of feed.
83 * @param array $serverInfo $_SERVER. 92 * @param array $serverInfo $_SERVER.
84 * @param array $userInput $_GET. 93 * @param array $userInput $_GET.
85 * @param boolean $isLoggedIn True if the user is currently logged in, 94 * @param boolean $isLoggedIn True if the user is currently logged in, false otherwise.
86 * false otherwise.
87 */ 95 */
88 public function __construct($linkDB, $feedType, $serverInfo, $userInput, $isLoggedIn) 96 public function __construct($linkDB, $formatter, $feedType, $serverInfo, $userInput, $isLoggedIn)
89 { 97 {
90 $this->linkDB = $linkDB; 98 $this->linkDB = $linkDB;
99 $this->formatter = $formatter;
91 $this->feedType = $feedType; 100 $this->feedType = $feedType;
92 $this->serverInfo = $serverInfo; 101 $this->serverInfo = $serverInfo;
93 $this->userInput = $userInput; 102 $this->userInput = $userInput;
@@ -101,13 +110,13 @@ class FeedBuilder
101 */ 110 */
102 public function buildData() 111 public function buildData()
103 { 112 {
104 // Search for untagged links 113 // Search for untagged bookmarks
105 if (isset($this->userInput['searchtags']) && empty($this->userInput['searchtags'])) { 114 if (isset($this->userInput['searchtags']) && empty($this->userInput['searchtags'])) {
106 $this->userInput['searchtags'] = false; 115 $this->userInput['searchtags'] = false;
107 } 116 }
108 117
109 // Optionally filter the results: 118 // Optionally filter the results:
110 $linksToDisplay = $this->linkDB->filterSearch($this->userInput); 119 $linksToDisplay = $this->linkDB->search($this->userInput);
111 120
112 $nblinksToDisplay = $this->getNbLinks(count($linksToDisplay)); 121 $nblinksToDisplay = $this->getNbLinks(count($linksToDisplay));
113 122
@@ -118,6 +127,7 @@ class FeedBuilder
118 } 127 }
119 128
120 $pageaddr = escape(index_url($this->serverInfo)); 129 $pageaddr = escape(index_url($this->serverInfo));
130 $this->formatter->addContextData('index_url', $pageaddr);
121 $linkDisplayed = array(); 131 $linkDisplayed = array();
122 for ($i = 0; $i < $nblinksToDisplay && $i < count($keys); $i++) { 132 for ($i = 0; $i < $nblinksToDisplay && $i < count($keys); $i++) {
123 $linkDisplayed[$keys[$i]] = $this->buildItem($linksToDisplay[$keys[$i]], $pageaddr); 133 $linkDisplayed[$keys[$i]] = $this->buildItem($linksToDisplay[$keys[$i]], $pageaddr);
@@ -139,54 +149,44 @@ class FeedBuilder
139 /** 149 /**
140 * Build a feed item (one per shaare). 150 * Build a feed item (one per shaare).
141 * 151 *
142 * @param array $link Single link array extracted from LinkDB. 152 * @param Bookmark $link Single link array extracted from LinkDB.
143 * @param string $pageaddr Index URL. 153 * @param string $pageaddr Index URL.
144 * 154 *
145 * @return array Link array with feed attributes. 155 * @return array Link array with feed attributes.
146 */ 156 */
147 protected function buildItem($link, $pageaddr) 157 protected function buildItem($link, $pageaddr)
148 { 158 {
149 $link['guid'] = $pageaddr . '?' . $link['shorturl']; 159 $data = $this->formatter->format($link);
150 // Prepend the root URL for notes 160 $data['guid'] = $pageaddr . '?' . $data['shorturl'];
151 if (is_note($link['url'])) {
152 $link['url'] = $pageaddr . $link['url'];
153 }
154 if ($this->usePermalinks === true) { 161 if ($this->usePermalinks === true) {
155 $permalink = '<a href="' . $link['url'] . '" title="' . t('Direct link') . '">' . t('Direct link') . '</a>'; 162 $permalink = '<a href="'. $data['url'] .'" title="'. t('Direct link') .'">'. t('Direct link') .'</a>';
156 } else { 163 } else {
157 $permalink = '<a href="' . $link['guid'] . '" title="' . t('Permalink') . '">' . t('Permalink') . '</a>'; 164 $permalink = '<a href="'. $data['guid'] .'" title="'. t('Permalink') .'">'. t('Permalink') .'</a>';
158 } 165 }
159 $link['description'] = format_description($link['description'], $pageaddr); 166 $data['description'] .= PHP_EOL . PHP_EOL . '<br>&#8212; ' . $permalink;
160 $link['description'] .= PHP_EOL . PHP_EOL . '<br>&#8212; ' . $permalink;
161 167
162 $pubDate = $link['created']; 168 $data['pub_iso_date'] = $this->getIsoDate($data['created']);
163 $link['pub_iso_date'] = $this->getIsoDate($pubDate);
164 169
165 // atom:entry elements MUST contain exactly one atom:updated element. 170 // atom:entry elements MUST contain exactly one atom:updated element.
166 if (!empty($link['updated'])) { 171 if (!empty($link->getUpdated())) {
167 $upDate = $link['updated']; 172 $data['up_iso_date'] = $this->getIsoDate($data['updated'], DateTime::ATOM);
168 $link['up_iso_date'] = $this->getIsoDate($upDate, DateTime::ATOM);
169 } else { 173 } else {
170 $link['up_iso_date'] = $this->getIsoDate($pubDate, DateTime::ATOM); 174 $data['up_iso_date'] = $this->getIsoDate($data['created'], DateTime::ATOM);
171 } 175 }
172 176
173 // Save the more recent item. 177 // Save the more recent item.
174 if (empty($this->latestDate) || $this->latestDate < $pubDate) { 178 if (empty($this->latestDate) || $this->latestDate < $data['created']) {
175 $this->latestDate = $pubDate; 179 $this->latestDate = $data['created'];
176 } 180 }
177 if (!empty($upDate) && $this->latestDate < $upDate) { 181 if (!empty($data['updated']) && $this->latestDate < $data['updated']) {
178 $this->latestDate = $upDate; 182 $this->latestDate = $data['updated'];
179 } 183 }
180 184
181 $taglist = array_filter(explode(' ', $link['tags']), 'strlen'); 185 return $data;
182 uasort($taglist, 'strcasecmp');
183 $link['taglist'] = $taglist;
184
185 return $link;
186 } 186 }
187 187
188 /** 188 /**
189 * Set this to true to use permalinks instead of direct links. 189 * Set this to true to use permalinks instead of direct bookmarks.
190 * 190 *
191 * @param boolean $usePermalinks true to force permalinks. 191 * @param boolean $usePermalinks true to force permalinks.
192 */ 192 */
@@ -273,11 +273,11 @@ class FeedBuilder
273 * Returns the number of link to display according to 'nb' user input parameter. 273 * Returns the number of link to display according to 'nb' user input parameter.
274 * 274 *
275 * If 'nb' not set or invalid, default value: $DEFAULT_NB_LINKS. 275 * If 'nb' not set or invalid, default value: $DEFAULT_NB_LINKS.
276 * If 'nb' is set to 'all', display all filtered links (max parameter). 276 * If 'nb' is set to 'all', display all filtered bookmarks (max parameter).
277 * 277 *
278 * @param int $max maximum number of links to display. 278 * @param int $max maximum number of bookmarks to display.
279 * 279 *
280 * @return int number of links to display. 280 * @return int number of bookmarks to display.
281 */ 281 */
282 public function getNbLinks($max) 282 public function getNbLinks($max)
283 { 283 {
diff --git a/application/formatter/BookmarkDefaultFormatter.php b/application/formatter/BookmarkDefaultFormatter.php
new file mode 100644
index 00000000..7550c556
--- /dev/null
+++ b/application/formatter/BookmarkDefaultFormatter.php
@@ -0,0 +1,81 @@
1<?php
2
3namespace Shaarli\Formatter;
4
5/**
6 * Class BookmarkDefaultFormatter
7 *
8 * Default bookmark formatter.
9 * Escape values for HTML display and automatically add link to URL and hashtags.
10 *
11 * @package Shaarli\Formatter
12 */
13class BookmarkDefaultFormatter extends BookmarkFormatter
14{
15 /**
16 * @inheritdoc
17 */
18 public function formatTitle($bookmark)
19 {
20 return escape($bookmark->getTitle());
21 }
22
23 /**
24 * @inheritdoc
25 */
26 public function formatDescription($bookmark)
27 {
28 $indexUrl = ! empty($this->contextData['index_url']) ? $this->contextData['index_url'] : '';
29 return format_description(escape($bookmark->getDescription()), $indexUrl);
30 }
31
32 /**
33 * @inheritdoc
34 */
35 protected function formatTagList($bookmark)
36 {
37 return escape($bookmark->getTags());
38 }
39
40 /**
41 * @inheritdoc
42 */
43 public function formatTagString($bookmark)
44 {
45 return implode(' ', $this->formatTagList($bookmark));
46 }
47
48 /**
49 * @inheritdoc
50 */
51 public function formatUrl($bookmark)
52 {
53 if (! empty($this->contextData['index_url']) && (
54 startsWith($bookmark->getUrl(), '?') || startsWith($bookmark->getUrl(), '/')
55 )) {
56 return $this->contextData['index_url'] . escape($bookmark->getUrl());
57 }
58 return escape($bookmark->getUrl());
59 }
60
61 /**
62 * @inheritdoc
63 */
64 protected function formatRealUrl($bookmark)
65 {
66 if (! empty($this->contextData['index_url']) && (
67 startsWith($bookmark->getUrl(), '?') || startsWith($bookmark->getUrl(), '/')
68 )) {
69 return $this->contextData['index_url'] . escape($bookmark->getUrl());
70 }
71 return escape($bookmark->getUrl());
72 }
73
74 /**
75 * @inheritdoc
76 */
77 protected function formatThumbnail($bookmark)
78 {
79 return escape($bookmark->getThumbnail());
80 }
81}
diff --git a/application/formatter/BookmarkFormatter.php b/application/formatter/BookmarkFormatter.php
new file mode 100644
index 00000000..c82c3452
--- /dev/null
+++ b/application/formatter/BookmarkFormatter.php
@@ -0,0 +1,256 @@
1<?php
2
3namespace Shaarli\Formatter;
4
5use DateTime;
6use Shaarli\Config\ConfigManager;
7use Shaarli\Bookmark\Bookmark;
8
9/**
10 * Class BookmarkFormatter
11 *
12 * Abstract class processing all bookmark attributes through methods designed to be overridden.
13 *
14 * @package Shaarli\Formatter
15 */
16abstract class BookmarkFormatter
17{
18 /**
19 * @var ConfigManager
20 */
21 protected $conf;
22
23 /**
24 * @var array Additional parameters than can be used for specific formatting
25 * e.g. index_url for Feed formatting
26 */
27 protected $contextData = [];
28
29 /**
30 * LinkDefaultFormatter constructor.
31 * @param ConfigManager $conf
32 */
33 public function __construct(ConfigManager $conf)
34 {
35 $this->conf = $conf;
36 }
37
38 /**
39 * Convert a Bookmark into an array usable by templates and plugins.
40 *
41 * All Bookmark attributes are formatted through a format method
42 * that can be overridden in a formatter extending this class.
43 *
44 * @param Bookmark $bookmark instance
45 *
46 * @return array formatted representation of a Bookmark
47 */
48 public function format($bookmark)
49 {
50 $out['id'] = $this->formatId($bookmark);
51 $out['shorturl'] = $this->formatShortUrl($bookmark);
52 $out['url'] = $this->formatUrl($bookmark);
53 $out['real_url'] = $this->formatRealUrl($bookmark);
54 $out['title'] = $this->formatTitle($bookmark);
55 $out['description'] = $this->formatDescription($bookmark);
56 $out['thumbnail'] = $this->formatThumbnail($bookmark);
57 $out['taglist'] = $this->formatTagList($bookmark);
58 $out['tags'] = $this->formatTagString($bookmark);
59 $out['sticky'] = $bookmark->isSticky();
60 $out['private'] = $bookmark->isPrivate();
61 $out['class'] = $this->formatClass($bookmark);
62 $out['created'] = $this->formatCreated($bookmark);
63 $out['updated'] = $this->formatUpdated($bookmark);
64 $out['timestamp'] = $this->formatCreatedTimestamp($bookmark);
65 $out['updated_timestamp'] = $this->formatUpdatedTimestamp($bookmark);
66 return $out;
67 }
68
69 /**
70 * Add additional data available to formatters.
71 * This is used for example to add `index_url` in description's links.
72 *
73 * @param string $key Context data key
74 * @param string $value Context data value
75 */
76 public function addContextData($key, $value)
77 {
78 $this->contextData[$key] = $value;
79 }
80
81 /**
82 * Format ID
83 *
84 * @param Bookmark $bookmark instance
85 *
86 * @return int formatted ID
87 */
88 protected function formatId($bookmark)
89 {
90 return $bookmark->getId();
91 }
92
93 /**
94 * Format ShortUrl
95 *
96 * @param Bookmark $bookmark instance
97 *
98 * @return string formatted ShortUrl
99 */
100 protected function formatShortUrl($bookmark)
101 {
102 return $bookmark->getShortUrl();
103 }
104
105 /**
106 * Format Url
107 *
108 * @param Bookmark $bookmark instance
109 *
110 * @return string formatted Url
111 */
112 protected function formatUrl($bookmark)
113 {
114 return $bookmark->getUrl();
115 }
116
117 /**
118 * Format RealUrl
119 * Legacy: identical to Url
120 *
121 * @param Bookmark $bookmark instance
122 *
123 * @return string formatted RealUrl
124 */
125 protected function formatRealUrl($bookmark)
126 {
127 return $bookmark->getUrl();
128 }
129
130 /**
131 * Format Title
132 *
133 * @param Bookmark $bookmark instance
134 *
135 * @return string formatted Title
136 */
137 protected function formatTitle($bookmark)
138 {
139 return $bookmark->getTitle();
140 }
141
142 /**
143 * Format Description
144 *
145 * @param Bookmark $bookmark instance
146 *
147 * @return string formatted Description
148 */
149 protected function formatDescription($bookmark)
150 {
151 return $bookmark->getDescription();
152 }
153
154 /**
155 * Format Thumbnail
156 *
157 * @param Bookmark $bookmark instance
158 *
159 * @return string formatted Thumbnail
160 */
161 protected function formatThumbnail($bookmark)
162 {
163 return $bookmark->getThumbnail();
164 }
165
166 /**
167 * Format Tags
168 *
169 * @param Bookmark $bookmark instance
170 *
171 * @return array formatted Tags
172 */
173 protected function formatTagList($bookmark)
174 {
175 return $bookmark->getTags();
176 }
177
178 /**
179 * Format TagString
180 *
181 * @param Bookmark $bookmark instance
182 *
183 * @return string formatted TagString
184 */
185 protected function formatTagString($bookmark)
186 {
187 return implode(' ', $bookmark->getTags());
188 }
189
190 /**
191 * Format Class
192 * Used to add specific CSS class for a link
193 *
194 * @param Bookmark $bookmark instance
195 *
196 * @return string formatted Class
197 */
198 protected function formatClass($bookmark)
199 {
200 return $bookmark->isPrivate() ? 'private' : '';
201 }
202
203 /**
204 * Format Created
205 *
206 * @param Bookmark $bookmark instance
207 *
208 * @return DateTime instance
209 */
210 protected function formatCreated(Bookmark $bookmark)
211 {
212 return $bookmark->getCreated();
213 }
214
215 /**
216 * Format Updated
217 *
218 * @param Bookmark $bookmark instance
219 *
220 * @return DateTime instance
221 */
222 protected function formatUpdated(Bookmark $bookmark)
223 {
224 return $bookmark->getUpdated();
225 }
226
227 /**
228 * Format CreatedTimestamp
229 *
230 * @param Bookmark $bookmark instance
231 *
232 * @return int formatted CreatedTimestamp
233 */
234 protected function formatCreatedTimestamp(Bookmark $bookmark)
235 {
236 if (! empty($bookmark->getCreated())) {
237 return $bookmark->getCreated()->getTimestamp();
238 }
239 return 0;
240 }
241
242 /**
243 * Format UpdatedTimestamp
244 *
245 * @param Bookmark $bookmark instance
246 *
247 * @return int formatted UpdatedTimestamp
248 */
249 protected function formatUpdatedTimestamp(Bookmark $bookmark)
250 {
251 if (! empty($bookmark->getUpdated())) {
252 return $bookmark->getUpdated()->getTimestamp();
253 }
254 return 0;
255 }
256}
diff --git a/application/formatter/BookmarkMarkdownFormatter.php b/application/formatter/BookmarkMarkdownFormatter.php
new file mode 100644
index 00000000..7797bfbf
--- /dev/null
+++ b/application/formatter/BookmarkMarkdownFormatter.php
@@ -0,0 +1,204 @@
1<?php
2
3namespace Shaarli\Formatter;
4
5use Shaarli\Config\ConfigManager;
6
7/**
8 * Class BookmarkMarkdownFormatter
9 *
10 * Format bookmark description into Markdown format.
11 *
12 * @package Shaarli\Formatter
13 */
14class BookmarkMarkdownFormatter extends BookmarkDefaultFormatter
15{
16 /**
17 * When this tag is present in a bookmark, its description should not be processed with Markdown
18 */
19 const NO_MD_TAG = 'nomarkdown';
20
21 /** @var \Parsedown instance */
22 protected $parsedown;
23
24 /** @var bool used to escape HTML in Markdown or not.
25 * It MUST be set to true for shared instance as HTML content can
26 * introduce XSS vulnerabilities.
27 */
28 protected $escape;
29
30 /**
31 * @var array List of allowed protocols for links inside bookmark's description.
32 */
33 protected $allowedProtocols;
34
35 /**
36 * LinkMarkdownFormatter constructor.
37 *
38 * @param ConfigManager $conf instance
39 */
40 public function __construct(ConfigManager $conf)
41 {
42 parent::__construct($conf);
43 $this->parsedown = new \Parsedown();
44 $this->escape = $conf->get('security.markdown_escape', true);
45 $this->allowedProtocols = $conf->get('security.allowed_protocols', []);
46 }
47
48 /**
49 * @inheritdoc
50 */
51 public function formatDescription($bookmark)
52 {
53 if (in_array(self::NO_MD_TAG, $bookmark->getTags())) {
54 return parent::formatDescription($bookmark);
55 }
56
57 $processedDescription = $bookmark->getDescription();
58 $processedDescription = $this->filterProtocols($processedDescription);
59 $processedDescription = $this->formatHashTags($processedDescription);
60 $processedDescription = $this->reverseEscapedHtml($processedDescription);
61 $processedDescription = $this->parsedown
62 ->setMarkupEscaped($this->escape)
63 ->setBreaksEnabled(true)
64 ->text($processedDescription);
65 $processedDescription = $this->sanitizeHtml($processedDescription);
66
67 if (!empty($processedDescription)) {
68 $processedDescription = '<div class="markdown">'. $processedDescription . '</div>';
69 }
70
71 return $processedDescription;
72 }
73
74 /**
75 * Remove the NO markdown tag if it is present
76 *
77 * @inheritdoc
78 */
79 protected function formatTagList($bookmark)
80 {
81 $out = parent::formatTagList($bookmark);
82 if (($pos = array_search(self::NO_MD_TAG, $out)) !== false) {
83 unset($out[$pos]);
84 return array_values($out);
85 }
86 return $out;
87 }
88
89 /**
90 * Replace not whitelisted protocols with http:// in given description.
91 * Also adds `index_url` to relative links if it's specified
92 *
93 * @param string $description input description text.
94 *
95 * @return string $description without malicious link.
96 */
97 protected function filterProtocols($description)
98 {
99 $allowedProtocols = $this->allowedProtocols;
100 $indexUrl = ! empty($this->contextData['index_url']) ? $this->contextData['index_url'] : '';
101
102 return preg_replace_callback(
103 '#]\((.*?)\)#is',
104 function ($match) use ($allowedProtocols, $indexUrl) {
105 $link = startsWith($match[1], '?') || startsWith($match[1], '/') ? $indexUrl : '';
106 $link .= whitelist_protocols($match[1], $allowedProtocols);
107 return ']('. $link.')';
108 },
109 $description
110 );
111 }
112
113 /**
114 * Replace hashtag in Markdown links format
115 * E.g. `#hashtag` becomes `[#hashtag](?addtag=hashtag)`
116 * It includes the index URL if specified.
117 *
118 * @param string $description
119 *
120 * @return string
121 */
122 protected function formatHashTags($description)
123 {
124 $indexUrl = ! empty($this->contextData['index_url']) ? $this->contextData['index_url'] : '';
125
126 /*
127 * To support unicode: http://stackoverflow.com/a/35498078/1484919
128 * \p{Pc} - to match underscore
129 * \p{N} - numeric character in any script
130 * \p{L} - letter from any language
131 * \p{Mn} - any non marking space (accents, umlauts, etc)
132 */
133 $regex = '/(^|\s)#([\p{Pc}\p{N}\p{L}\p{Mn}]+)/mui';
134 $replacement = '$1[#$2]('. $indexUrl .'?addtag=$2)';
135
136 $descriptionLines = explode(PHP_EOL, $description);
137 $descriptionOut = '';
138 $codeBlockOn = false;
139 $lineCount = 0;
140
141 foreach ($descriptionLines as $descriptionLine) {
142 // Detect line of code: starting with 4 spaces,
143 // except lists which can start with +/*/- or `2.` after spaces.
144 $codeLineOn = preg_match('/^ +(?=[^\+\*\-])(?=(?!\d\.).)/', $descriptionLine) > 0;
145 // Detect and toggle block of code
146 if (!$codeBlockOn) {
147 $codeBlockOn = preg_match('/^```/', $descriptionLine) > 0;
148 } elseif (preg_match('/^```/', $descriptionLine) > 0) {
149 $codeBlockOn = false;
150 }
151
152 if (!$codeBlockOn && !$codeLineOn) {
153 $descriptionLine = preg_replace($regex, $replacement, $descriptionLine);
154 }
155
156 $descriptionOut .= $descriptionLine;
157 if ($lineCount++ < count($descriptionLines) - 1) {
158 $descriptionOut .= PHP_EOL;
159 }
160 }
161
162 return $descriptionOut;
163 }
164
165 /**
166 * Remove dangerous HTML tags (tags, iframe, etc.).
167 * Doesn't affect <code> content (already escaped by Parsedown).
168 *
169 * @param string $description input description text.
170 *
171 * @return string given string escaped.
172 */
173 protected function sanitizeHtml($description)
174 {
175 $escapeTags = array(
176 'script',
177 'style',
178 'link',
179 'iframe',
180 'frameset',
181 'frame',
182 );
183 foreach ($escapeTags as $tag) {
184 $description = preg_replace_callback(
185 '#<\s*'. $tag .'[^>]*>(.*</\s*'. $tag .'[^>]*>)?#is',
186 function ($match) {
187 return escape($match[0]);
188 },
189 $description
190 );
191 }
192 $description = preg_replace(
193 '#(<[^>]+\s)on[a-z]*="?[^ "]*"?#is',
194 '$1',
195 $description
196 );
197 return $description;
198 }
199
200 protected function reverseEscapedHtml($description)
201 {
202 return unescape($description);
203 }
204}
diff --git a/application/formatter/BookmarkRawFormatter.php b/application/formatter/BookmarkRawFormatter.php
new file mode 100644
index 00000000..bc372273
--- /dev/null
+++ b/application/formatter/BookmarkRawFormatter.php
@@ -0,0 +1,13 @@
1<?php
2
3namespace Shaarli\Formatter;
4
5/**
6 * Class BookmarkRawFormatter
7 *
8 * Used to retrieve bookmarks as array with raw values.
9 * Warning: Do NOT use this for HTML content as it can introduce XSS vulnerabilities.
10 *
11 * @package Shaarli\Formatter
12 */
13class BookmarkRawFormatter extends BookmarkFormatter {}
diff --git a/application/formatter/FormatterFactory.php b/application/formatter/FormatterFactory.php
new file mode 100644
index 00000000..0d2c0466
--- /dev/null
+++ b/application/formatter/FormatterFactory.php
@@ -0,0 +1,46 @@
1<?php
2
3namespace Shaarli\Formatter;
4
5use Shaarli\Config\ConfigManager;
6
7/**
8 * Class FormatterFactory
9 *
10 * Helper class used to instantiate the proper BookmarkFormatter.
11 *
12 * @package Shaarli\Formatter
13 */
14class FormatterFactory
15{
16 /** @var ConfigManager instance */
17 protected $conf;
18
19 /**
20 * FormatterFactory constructor.
21 *
22 * @param ConfigManager $conf
23 */
24 public function __construct(ConfigManager $conf)
25 {
26 $this->conf = $conf;
27 }
28
29 /**
30 * Instanciate a BookmarkFormatter depending on the configuration or provided formatter type.
31 *
32 * @param string|null $type force a specific type regardless of the configuration
33 *
34 * @return BookmarkFormatter instance.
35 */
36 public function getFormatter($type = null)
37 {
38 $type = $type ? $type : $this->conf->get('formatter', 'default');
39 $className = '\\Shaarli\\Formatter\\Bookmark'. ucfirst($type) .'Formatter';
40 if (!class_exists($className)) {
41 $className = '\\Shaarli\\Formatter\\BookmarkDefaultFormatter';
42 }
43
44 return new $className($this->conf);
45 }
46}
diff --git a/application/bookmark/LinkDB.php b/application/legacy/LegacyLinkDB.php
index f01c7ee6..7ccf5e54 100644
--- a/application/bookmark/LinkDB.php
+++ b/application/legacy/LegacyLinkDB.php
@@ -1,17 +1,17 @@
1<?php 1<?php
2 2
3namespace Shaarli\Bookmark; 3namespace Shaarli\Legacy;
4 4
5use ArrayAccess; 5use ArrayAccess;
6use Countable; 6use Countable;
7use DateTime; 7use DateTime;
8use Iterator; 8use Iterator;
9use Shaarli\Bookmark\Exception\LinkNotFoundException; 9use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
10use Shaarli\Exceptions\IOException; 10use Shaarli\Exceptions\IOException;
11use Shaarli\FileUtils; 11use Shaarli\FileUtils;
12 12
13/** 13/**
14 * Data storage for links. 14 * Data storage for bookmarks.
15 * 15 *
16 * This object behaves like an associative array. 16 * This object behaves like an associative array.
17 * 17 *
@@ -29,8 +29,8 @@ use Shaarli\FileUtils;
29 * - private: Is this link private? 0=no, other value=yes 29 * - private: Is this link private? 0=no, other value=yes
30 * - tags: tags attached to this entry (separated by spaces) 30 * - tags: tags attached to this entry (separated by spaces)
31 * - title Title of the link 31 * - title Title of the link
32 * - url URL of the link. Used for displayable links. 32 * - url URL of the link. Used for displayable bookmarks.
33 * Can be absolute or relative in the database but the relative links 33 * Can be absolute or relative in the database but the relative bookmarks
34 * will be converted to absolute ones in templates. 34 * will be converted to absolute ones in templates.
35 * - real_url Raw URL in stored in the DB (absolute or relative). 35 * - real_url Raw URL in stored in the DB (absolute or relative).
36 * - shorturl Permalink smallhash 36 * - shorturl Permalink smallhash
@@ -49,11 +49,13 @@ use Shaarli\FileUtils;
49 * Example: 49 * Example:
50 * - DB: link #1 (2010-01-01) link #2 (2016-01-01) 50 * - DB: link #1 (2010-01-01) link #2 (2016-01-01)
51 * - Order: #2 #1 51 * - Order: #2 #1
52 * - Import links containing: link #3 (2013-01-01) 52 * - Import bookmarks containing: link #3 (2013-01-01)
53 * - New DB: link #1 (2010-01-01) link #2 (2016-01-01) link #3 (2013-01-01) 53 * - New DB: link #1 (2010-01-01) link #2 (2016-01-01) link #3 (2013-01-01)
54 * - Real order: #2 #3 #1 54 * - Real order: #2 #3 #1
55 *
56 * @deprecated
55 */ 57 */
56class LinkDB implements Iterator, Countable, ArrayAccess 58class LegacyLinkDB implements Iterator, Countable, ArrayAccess
57{ 59{
58 // Links are stored as a PHP serialized string 60 // Links are stored as a PHP serialized string
59 private $datastore; 61 private $datastore;
@@ -61,7 +63,7 @@ class LinkDB implements Iterator, Countable, ArrayAccess
61 // Link date storage format 63 // Link date storage format
62 const LINK_DATE_FORMAT = 'Ymd_His'; 64 const LINK_DATE_FORMAT = 'Ymd_His';
63 65
64 // List of links (associative array) 66 // List of bookmarks (associative array)
65 // - key: link date (e.g. "20110823_124546"), 67 // - key: link date (e.g. "20110823_124546"),
66 // - value: associative array (keys: title, description...) 68 // - value: associative array (keys: title, description...)
67 private $links; 69 private $links;
@@ -71,7 +73,7 @@ class LinkDB implements Iterator, Countable, ArrayAccess
71 private $urls; 73 private $urls;
72 74
73 /** 75 /**
74 * @var array List of all links IDS mapped with their array offset. 76 * @var array List of all bookmarks IDS mapped with their array offset.
75 * Map: id->offset. 77 * Map: id->offset.
76 */ 78 */
77 protected $ids; 79 protected $ids;
@@ -82,10 +84,10 @@ class LinkDB implements Iterator, Countable, ArrayAccess
82 // Position in the $this->keys array (for the Iterator interface) 84 // Position in the $this->keys array (for the Iterator interface)
83 private $position; 85 private $position;
84 86
85 // Is the user logged in? (used to filter private links) 87 // Is the user logged in? (used to filter private bookmarks)
86 private $loggedIn; 88 private $loggedIn;
87 89
88 // Hide public links 90 // Hide public bookmarks
89 private $hidePublicLinks; 91 private $hidePublicLinks;
90 92
91 /** 93 /**
@@ -95,7 +97,7 @@ class LinkDB implements Iterator, Countable, ArrayAccess
95 * 97 *
96 * @param string $datastore datastore file path. 98 * @param string $datastore datastore file path.
97 * @param boolean $isLoggedIn is the user logged in? 99 * @param boolean $isLoggedIn is the user logged in?
98 * @param boolean $hidePublicLinks if true all links are private. 100 * @param boolean $hidePublicLinks if true all bookmarks are private.
99 */ 101 */
100 public function __construct( 102 public function __construct(
101 $datastore, 103 $datastore,
@@ -280,7 +282,7 @@ You use the community supported version of the original Shaarli project, by Seba
280 */ 282 */
281 private function read() 283 private function read()
282 { 284 {
283 // Public links are hidden and user not logged in => nothing to show 285 // Public bookmarks are hidden and user not logged in => nothing to show
284 if ($this->hidePublicLinks && !$this->loggedIn) { 286 if ($this->hidePublicLinks && !$this->loggedIn) {
285 $this->links = array(); 287 $this->links = array();
286 return; 288 return;
@@ -310,7 +312,7 @@ You use the community supported version of the original Shaarli project, by Seba
310 312
311 $link['sticky'] = isset($link['sticky']) ? $link['sticky'] : false; 313 $link['sticky'] = isset($link['sticky']) ? $link['sticky'] : false;
312 314
313 // To be able to load links before running the update, and prepare the update 315 // To be able to load bookmarks before running the update, and prepare the update
314 if (!isset($link['created'])) { 316 if (!isset($link['created'])) {
315 $link['id'] = $link['linkdate']; 317 $link['id'] = $link['linkdate'];
316 $link['created'] = DateTime::createFromFormat(self::LINK_DATE_FORMAT, $link['linkdate']); 318 $link['created'] = DateTime::createFromFormat(self::LINK_DATE_FORMAT, $link['linkdate']);
@@ -375,13 +377,13 @@ You use the community supported version of the original Shaarli project, by Seba
375 * 377 *
376 * @return array $filtered array containing permalink data. 378 * @return array $filtered array containing permalink data.
377 * 379 *
378 * @throws LinkNotFoundException if the smallhash is malformed or doesn't match any link. 380 * @throws BookmarkNotFoundException if the smallhash is malformed or doesn't match any link.
379 */ 381 */
380 public function filterHash($request) 382 public function filterHash($request)
381 { 383 {
382 $request = substr($request, 0, 6); 384 $request = substr($request, 0, 6);
383 $linkFilter = new LinkFilter($this->links); 385 $linkFilter = new LegacyLinkFilter($this->links);
384 return $linkFilter->filter(LinkFilter::$FILTER_HASH, $request); 386 return $linkFilter->filter(LegacyLinkFilter::$FILTER_HASH, $request);
385 } 387 }
386 388
387 /** 389 /**
@@ -393,21 +395,21 @@ You use the community supported version of the original Shaarli project, by Seba
393 */ 395 */
394 public function filterDay($request) 396 public function filterDay($request)
395 { 397 {
396 $linkFilter = new LinkFilter($this->links); 398 $linkFilter = new LegacyLinkFilter($this->links);
397 return $linkFilter->filter(LinkFilter::$FILTER_DAY, $request); 399 return $linkFilter->filter(LegacyLinkFilter::$FILTER_DAY, $request);
398 } 400 }
399 401
400 /** 402 /**
401 * Filter links according to search parameters. 403 * Filter bookmarks according to search parameters.
402 * 404 *
403 * @param array $filterRequest Search request content. Supported keys: 405 * @param array $filterRequest Search request content. Supported keys:
404 * - searchtags: list of tags 406 * - searchtags: list of tags
405 * - searchterm: term search 407 * - searchterm: term search
406 * @param bool $casesensitive Optional: Perform case sensitive filter 408 * @param bool $casesensitive Optional: Perform case sensitive filter
407 * @param string $visibility return only all/private/public links 409 * @param string $visibility return only all/private/public bookmarks
408 * @param bool $untaggedonly return only untagged links 410 * @param bool $untaggedonly return only untagged bookmarks
409 * 411 *
410 * @return array filtered links, all links if no suitable filter was provided. 412 * @return array filtered bookmarks, all bookmarks if no suitable filter was provided.
411 */ 413 */
412 public function filterSearch( 414 public function filterSearch(
413 $filterRequest = array(), 415 $filterRequest = array(),
@@ -420,19 +422,19 @@ You use the community supported version of the original Shaarli project, by Seba
420 $searchtags = isset($filterRequest['searchtags']) ? escape($filterRequest['searchtags']) : ''; 422 $searchtags = isset($filterRequest['searchtags']) ? escape($filterRequest['searchtags']) : '';
421 $searchterm = isset($filterRequest['searchterm']) ? escape($filterRequest['searchterm']) : ''; 423 $searchterm = isset($filterRequest['searchterm']) ? escape($filterRequest['searchterm']) : '';
422 424
423 // Search tags + fullsearch - blank string parameter will return all links. 425 // Search tags + fullsearch - blank string parameter will return all bookmarks.
424 $type = LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT; // == "vuotext" 426 $type = LegacyLinkFilter::$FILTER_TAG | LegacyLinkFilter::$FILTER_TEXT; // == "vuotext"
425 $request = [$searchtags, $searchterm]; 427 $request = [$searchtags, $searchterm];
426 428
427 $linkFilter = new LinkFilter($this); 429 $linkFilter = new LegacyLinkFilter($this);
428 return $linkFilter->filter($type, $request, $casesensitive, $visibility, $untaggedonly); 430 return $linkFilter->filter($type, $request, $casesensitive, $visibility, $untaggedonly);
429 } 431 }
430 432
431 /** 433 /**
432 * Returns the list tags appearing in the links with the given tags 434 * Returns the list tags appearing in the bookmarks with the given tags
433 * 435 *
434 * @param array $filteringTags tags selecting the links to consider 436 * @param array $filteringTags tags selecting the bookmarks to consider
435 * @param string $visibility process only all/private/public links 437 * @param string $visibility process only all/private/public bookmarks
436 * 438 *
437 * @return array tag => linksCount 439 * @return array tag => linksCount
438 */ 440 */
@@ -471,12 +473,12 @@ You use the community supported version of the original Shaarli project, by Seba
471 } 473 }
472 474
473 /** 475 /**
474 * Rename or delete a tag across all links. 476 * Rename or delete a tag across all bookmarks.
475 * 477 *
476 * @param string $from Tag to rename 478 * @param string $from Tag to rename
477 * @param string $to New tag. If none is provided, the from tag will be deleted 479 * @param string $to New tag. If none is provided, the from tag will be deleted
478 * 480 *
479 * @return array|bool List of altered links or false on error 481 * @return array|bool List of altered bookmarks or false on error
480 */ 482 */
481 public function renameTag($from, $to) 483 public function renameTag($from, $to)
482 { 484 {
@@ -519,7 +521,7 @@ You use the community supported version of the original Shaarli project, by Seba
519 } 521 }
520 522
521 /** 523 /**
522 * Reorder links by creation date (newest first). 524 * Reorder bookmarks by creation date (newest first).
523 * 525 *
524 * Also update the urls and ids mapping arrays. 526 * Also update the urls and ids mapping arrays.
525 * 527 *
@@ -562,7 +564,7 @@ You use the community supported version of the original Shaarli project, by Seba
562 } 564 }
563 565
564 /** 566 /**
565 * Returns a link offset in links array from its unique ID. 567 * Returns a link offset in bookmarks array from its unique ID.
566 * 568 *
567 * @param int $id Persistent ID of a link. 569 * @param int $id Persistent ID of a link.
568 * 570 *
diff --git a/application/bookmark/LinkFilter.php b/application/legacy/LegacyLinkFilter.php
index 9b966307..7cf93d60 100644
--- a/application/bookmark/LinkFilter.php
+++ b/application/legacy/LegacyLinkFilter.php
@@ -1,16 +1,18 @@
1<?php 1<?php
2 2
3namespace Shaarli\Bookmark; 3namespace Shaarli\Legacy;
4 4
5use Exception; 5use Exception;
6use Shaarli\Bookmark\Exception\LinkNotFoundException; 6use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
7 7
8/** 8/**
9 * Class LinkFilter. 9 * Class LinkFilter.
10 * 10 *
11 * Perform search and filter operation on link data list. 11 * Perform search and filter operation on link data list.
12 *
13 * @deprecated
12 */ 14 */
13class LinkFilter 15class LegacyLinkFilter
14{ 16{
15 /** 17 /**
16 * @var string permalinks. 18 * @var string permalinks.
@@ -38,12 +40,12 @@ class LinkFilter
38 public static $HASHTAG_CHARS = '\p{Pc}\p{N}\p{L}\p{Mn}'; 40 public static $HASHTAG_CHARS = '\p{Pc}\p{N}\p{L}\p{Mn}';
39 41
40 /** 42 /**
41 * @var LinkDB all available links. 43 * @var LegacyLinkDB all available links.
42 */ 44 */
43 private $links; 45 private $links;
44 46
45 /** 47 /**
46 * @param LinkDB $links initialization. 48 * @param LegacyLinkDB $links initialization.
47 */ 49 */
48 public function __construct($links) 50 public function __construct($links)
49 { 51 {
@@ -84,10 +86,10 @@ class LinkFilter
84 $filtered = $this->links; 86 $filtered = $this->links;
85 } 87 }
86 if (!empty($request[0])) { 88 if (!empty($request[0])) {
87 $filtered = (new LinkFilter($filtered))->filterTags($request[0], $casesensitive, $visibility); 89 $filtered = (new LegacyLinkFilter($filtered))->filterTags($request[0], $casesensitive, $visibility);
88 } 90 }
89 if (!empty($request[1])) { 91 if (!empty($request[1])) {
90 $filtered = (new LinkFilter($filtered))->filterFulltext($request[1], $visibility); 92 $filtered = (new LegacyLinkFilter($filtered))->filterFulltext($request[1], $visibility);
91 } 93 }
92 return $filtered; 94 return $filtered;
93 case self::$FILTER_TEXT: 95 case self::$FILTER_TEXT:
@@ -137,7 +139,7 @@ class LinkFilter
137 * 139 *
138 * @return array $filtered array containing permalink data. 140 * @return array $filtered array containing permalink data.
139 * 141 *
140 * @throws \Shaarli\Bookmark\Exception\LinkNotFoundException if the smallhash doesn't match any link. 142 * @throws BookmarkNotFoundException if the smallhash doesn't match any link.
141 */ 143 */
142 private function filterSmallHash($smallHash) 144 private function filterSmallHash($smallHash)
143 { 145 {
@@ -151,7 +153,7 @@ class LinkFilter
151 } 153 }
152 154
153 if (empty($filtered)) { 155 if (empty($filtered)) {
154 throw new LinkNotFoundException(); 156 throw new BookmarkNotFoundException();
155 } 157 }
156 158
157 return $filtered; 159 return $filtered;
diff --git a/application/legacy/LegacyUpdater.php b/application/legacy/LegacyUpdater.php
new file mode 100644
index 00000000..3a5de79f
--- /dev/null
+++ b/application/legacy/LegacyUpdater.php
@@ -0,0 +1,617 @@
1<?php
2
3namespace Shaarli\Legacy;
4
5use Exception;
6use RainTPL;
7use ReflectionClass;
8use ReflectionException;
9use ReflectionMethod;
10use Shaarli\ApplicationUtils;
11use Shaarli\Bookmark\Bookmark;
12use Shaarli\Bookmark\BookmarkArray;
13use Shaarli\Bookmark\LinkDB;
14use Shaarli\Bookmark\BookmarkFilter;
15use Shaarli\Bookmark\BookmarkIO;
16use Shaarli\Config\ConfigJson;
17use Shaarli\Config\ConfigManager;
18use Shaarli\Config\ConfigPhp;
19use Shaarli\Exceptions\IOException;
20use Shaarli\Thumbnailer;
21use Shaarli\Updater\Exception\UpdaterException;
22
23/**
24 * Class updater.
25 * Used to update stuff when a new Shaarli's version is reached.
26 * Update methods are ran only once, and the stored in a JSON file.
27 *
28 * @deprecated
29 */
30class LegacyUpdater
31{
32 /**
33 * @var array Updates which are already done.
34 */
35 protected $doneUpdates;
36
37 /**
38 * @var LegacyLinkDB instance.
39 */
40 protected $linkDB;
41
42 /**
43 * @var ConfigManager $conf Configuration Manager instance.
44 */
45 protected $conf;
46
47 /**
48 * @var bool True if the user is logged in, false otherwise.
49 */
50 protected $isLoggedIn;
51
52 /**
53 * @var array $_SESSION
54 */
55 protected $session;
56
57 /**
58 * @var ReflectionMethod[] List of current class methods.
59 */
60 protected $methods;
61
62 /**
63 * Object constructor.
64 *
65 * @param array $doneUpdates Updates which are already done.
66 * @param LegacyLinkDB $linkDB LinkDB instance.
67 * @param ConfigManager $conf Configuration Manager instance.
68 * @param boolean $isLoggedIn True if the user is logged in.
69 * @param array $session $_SESSION (by reference)
70 *
71 * @throws ReflectionException
72 */
73 public function __construct($doneUpdates, $linkDB, $conf, $isLoggedIn, &$session = [])
74 {
75 $this->doneUpdates = $doneUpdates;
76 $this->linkDB = $linkDB;
77 $this->conf = $conf;
78 $this->isLoggedIn = $isLoggedIn;
79 $this->session = &$session;
80
81 // Retrieve all update methods.
82 $class = new ReflectionClass($this);
83 $this->methods = $class->getMethods();
84 }
85
86 /**
87 * Run all new updates.
88 * Update methods have to start with 'updateMethod' and return true (on success).
89 *
90 * @return array An array containing ran updates.
91 *
92 * @throws UpdaterException If something went wrong.
93 */
94 public function update()
95 {
96 $updatesRan = array();
97
98 // If the user isn't logged in, exit without updating.
99 if ($this->isLoggedIn !== true) {
100 return $updatesRan;
101 }
102
103 if ($this->methods === null) {
104 throw new UpdaterException(t('Couldn\'t retrieve updater class methods.'));
105 }
106
107 foreach ($this->methods as $method) {
108 // Not an update method or already done, pass.
109 if (!startsWith($method->getName(), 'updateMethod')
110 || in_array($method->getName(), $this->doneUpdates)
111 ) {
112 continue;
113 }
114
115 try {
116 $method->setAccessible(true);
117 $res = $method->invoke($this);
118 // Update method must return true to be considered processed.
119 if ($res === true) {
120 $updatesRan[] = $method->getName();
121 }
122 } catch (Exception $e) {
123 throw new UpdaterException($method, $e);
124 }
125 }
126
127 $this->doneUpdates = array_merge($this->doneUpdates, $updatesRan);
128
129 return $updatesRan;
130 }
131
132 /**
133 * @return array Updates methods already processed.
134 */
135 public function getDoneUpdates()
136 {
137 return $this->doneUpdates;
138 }
139
140 /**
141 * Move deprecated options.php to config.php.
142 *
143 * Milestone 0.9 (old versioning) - shaarli/Shaarli#41:
144 * options.php is not supported anymore.
145 */
146 public function updateMethodMergeDeprecatedConfigFile()
147 {
148 if (is_file($this->conf->get('resource.data_dir') . '/options.php')) {
149 include $this->conf->get('resource.data_dir') . '/options.php';
150
151 // Load GLOBALS into config
152 $allowedKeys = array_merge(ConfigPhp::$ROOT_KEYS);
153 $allowedKeys[] = 'config';
154 foreach ($GLOBALS as $key => $value) {
155 if (in_array($key, $allowedKeys)) {
156 $this->conf->set($key, $value);
157 }
158 }
159 $this->conf->write($this->isLoggedIn);
160 unlink($this->conf->get('resource.data_dir') . '/options.php');
161 }
162
163 return true;
164 }
165
166 /**
167 * Move old configuration in PHP to the new config system in JSON format.
168 *
169 * Will rename 'config.php' into 'config.save.php' and create 'config.json.php'.
170 * It will also convert legacy setting keys to the new ones.
171 */
172 public function updateMethodConfigToJson()
173 {
174 // JSON config already exists, nothing to do.
175 if ($this->conf->getConfigIO() instanceof ConfigJson) {
176 return true;
177 }
178
179 $configPhp = new ConfigPhp();
180 $configJson = new ConfigJson();
181 $oldConfig = $configPhp->read($this->conf->getConfigFile() . '.php');
182 rename($this->conf->getConfigFileExt(), $this->conf->getConfigFile() . '.save.php');
183 $this->conf->setConfigIO($configJson);
184 $this->conf->reload();
185
186 $legacyMap = array_flip(ConfigPhp::$LEGACY_KEYS_MAPPING);
187 foreach (ConfigPhp::$ROOT_KEYS as $key) {
188 $this->conf->set($legacyMap[$key], $oldConfig[$key]);
189 }
190
191 // Set sub config keys (config and plugins)
192 $subConfig = array('config', 'plugins');
193 foreach ($subConfig as $sub) {
194 foreach ($oldConfig[$sub] as $key => $value) {
195 if (isset($legacyMap[$sub . '.' . $key])) {
196 $configKey = $legacyMap[$sub . '.' . $key];
197 } else {
198 $configKey = $sub . '.' . $key;
199 }
200 $this->conf->set($configKey, $value);
201 }
202 }
203
204 try {
205 $this->conf->write($this->isLoggedIn);
206 return true;
207 } catch (IOException $e) {
208 error_log($e->getMessage());
209 return false;
210 }
211 }
212
213 /**
214 * Escape settings which have been manually escaped in every request in previous versions:
215 * - general.title
216 * - general.header_link
217 * - redirector.url
218 *
219 * @return bool true if the update is successful, false otherwise.
220 */
221 public function updateMethodEscapeUnescapedConfig()
222 {
223 try {
224 $this->conf->set('general.title', escape($this->conf->get('general.title')));
225 $this->conf->set('general.header_link', escape($this->conf->get('general.header_link')));
226 $this->conf->write($this->isLoggedIn);
227 } catch (Exception $e) {
228 error_log($e->getMessage());
229 return false;
230 }
231 return true;
232 }
233
234 /**
235 * Update the database to use the new ID system, which replaces linkdate primary keys.
236 * Also, creation and update dates are now DateTime objects (done by LinkDB).
237 *
238 * Since this update is very sensitve (changing the whole database), the datastore will be
239 * automatically backed up into the file datastore.<datetime>.php.
240 *
241 * LinkDB also adds the field 'shorturl' with the precedent format (linkdate smallhash),
242 * which will be saved by this method.
243 *
244 * @return bool true if the update is successful, false otherwise.
245 */
246 public function updateMethodDatastoreIds()
247 {
248 $first = 'update';
249 foreach ($this->linkDB as $key => $link) {
250 $first = $key;
251 break;
252 }
253
254 // up to date database
255 if (is_int($first)) {
256 return true;
257 }
258
259 $save = $this->conf->get('resource.data_dir') . '/datastore.' . date('YmdHis') . '.php';
260 copy($this->conf->get('resource.datastore'), $save);
261
262 $links = array();
263 foreach ($this->linkDB as $offset => $value) {
264 $links[] = $value;
265 unset($this->linkDB[$offset]);
266 }
267 $links = array_reverse($links);
268 $cpt = 0;
269 foreach ($links as $l) {
270 unset($l['linkdate']);
271 $l['id'] = $cpt;
272 $this->linkDB[$cpt++] = $l;
273 }
274
275 $this->linkDB->save($this->conf->get('resource.page_cache'));
276 $this->linkDB->reorder();
277
278 return true;
279 }
280
281 /**
282 * Rename tags starting with a '-' to work with tag exclusion search.
283 */
284 public function updateMethodRenameDashTags()
285 {
286 $linklist = $this->linkDB->filterSearch();
287 foreach ($linklist as $key => $link) {
288 $link['tags'] = preg_replace('/(^| )\-/', '$1', $link['tags']);
289 $link['tags'] = implode(' ', array_unique(BookmarkFilter::tagsStrToArray($link['tags'], true)));
290 $this->linkDB[$key] = $link;
291 }
292 $this->linkDB->save($this->conf->get('resource.page_cache'));
293 return true;
294 }
295
296 /**
297 * Initialize API settings:
298 * - api.enabled: true
299 * - api.secret: generated secret
300 */
301 public function updateMethodApiSettings()
302 {
303 if ($this->conf->exists('api.secret')) {
304 return true;
305 }
306
307 $this->conf->set('api.enabled', true);
308 $this->conf->set(
309 'api.secret',
310 generate_api_secret(
311 $this->conf->get('credentials.login'),
312 $this->conf->get('credentials.salt')
313 )
314 );
315 $this->conf->write($this->isLoggedIn);
316 return true;
317 }
318
319 /**
320 * New setting: theme name. If the default theme is used, nothing to do.
321 *
322 * If the user uses a custom theme, raintpl_tpl dir is updated to the parent directory,
323 * and the current theme is set as default in the theme setting.
324 *
325 * @return bool true if the update is successful, false otherwise.
326 */
327 public function updateMethodDefaultTheme()
328 {
329 // raintpl_tpl isn't the root template directory anymore.
330 // We run the update only if this folder still contains the template files.
331 $tplDir = $this->conf->get('resource.raintpl_tpl');
332 $tplFile = $tplDir . '/linklist.html';
333 if (!file_exists($tplFile)) {
334 return true;
335 }
336
337 $parent = dirname($tplDir);
338 $this->conf->set('resource.raintpl_tpl', $parent);
339 $this->conf->set('resource.theme', trim(str_replace($parent, '', $tplDir), '/'));
340 $this->conf->write($this->isLoggedIn);
341
342 // Dependency injection gore
343 RainTPL::$tpl_dir = $tplDir;
344
345 return true;
346 }
347
348 /**
349 * Move the file to inc/user.css to data/user.css.
350 *
351 * Note: Due to hardcoded paths, it's not unit testable. But one line of code should be fine.
352 *
353 * @return bool true if the update is successful, false otherwise.
354 */
355 public function updateMethodMoveUserCss()
356 {
357 if (!is_file('inc/user.css')) {
358 return true;
359 }
360
361 return rename('inc/user.css', 'data/user.css');
362 }
363
364 /**
365 * * `markdown_escape` is a new setting, set to true as default.
366 *
367 * If the markdown plugin was already enabled, escaping is disabled to avoid
368 * breaking existing entries.
369 */
370 public function updateMethodEscapeMarkdown()
371 {
372 if ($this->conf->exists('security.markdown_escape')) {
373 return true;
374 }
375
376 if (in_array('markdown', $this->conf->get('general.enabled_plugins'))) {
377 $this->conf->set('security.markdown_escape', false);
378 } else {
379 $this->conf->set('security.markdown_escape', true);
380 }
381 $this->conf->write($this->isLoggedIn);
382
383 return true;
384 }
385
386 /**
387 * Add 'http://' to Piwik URL the setting is set.
388 *
389 * @return bool true if the update is successful, false otherwise.
390 */
391 public function updateMethodPiwikUrl()
392 {
393 if (!$this->conf->exists('plugins.PIWIK_URL') || startsWith($this->conf->get('plugins.PIWIK_URL'), 'http')) {
394 return true;
395 }
396
397 $this->conf->set('plugins.PIWIK_URL', 'http://' . $this->conf->get('plugins.PIWIK_URL'));
398 $this->conf->write($this->isLoggedIn);
399
400 return true;
401 }
402
403 /**
404 * Use ATOM feed as default.
405 */
406 public function updateMethodAtomDefault()
407 {
408 if (!$this->conf->exists('feed.show_atom') || $this->conf->get('feed.show_atom') === true) {
409 return true;
410 }
411
412 $this->conf->set('feed.show_atom', true);
413 $this->conf->write($this->isLoggedIn);
414
415 return true;
416 }
417
418 /**
419 * Update updates.check_updates_branch setting.
420 *
421 * If the current major version digit matches the latest branch
422 * major version digit, we set the branch to `latest`,
423 * otherwise we'll check updates on the `stable` branch.
424 *
425 * No update required for the dev version.
426 *
427 * Note: due to hardcoded URL and lack of dependency injection, this is not unit testable.
428 *
429 * FIXME! This needs to be removed when we switch to first digit major version
430 * instead of the second one since the versionning process will change.
431 */
432 public function updateMethodCheckUpdateRemoteBranch()
433 {
434 if (SHAARLI_VERSION === 'dev' || $this->conf->get('updates.check_updates_branch') === 'latest') {
435 return true;
436 }
437
438 // Get latest branch major version digit
439 $latestVersion = ApplicationUtils::getLatestGitVersionCode(
440 'https://raw.githubusercontent.com/shaarli/Shaarli/latest/shaarli_version.php',
441 5
442 );
443 if (preg_match('/(\d+)\.\d+$/', $latestVersion, $matches) === false) {
444 return false;
445 }
446 $latestMajor = $matches[1];
447
448 // Get current major version digit
449 preg_match('/(\d+)\.\d+$/', SHAARLI_VERSION, $matches);
450 $currentMajor = $matches[1];
451
452 if ($currentMajor === $latestMajor) {
453 $branch = 'latest';
454 } else {
455 $branch = 'stable';
456 }
457 $this->conf->set('updates.check_updates_branch', $branch);
458 $this->conf->write($this->isLoggedIn);
459 return true;
460 }
461
462 /**
463 * Reset history store file due to date format change.
464 */
465 public function updateMethodResetHistoryFile()
466 {
467 if (is_file($this->conf->get('resource.history'))) {
468 unlink($this->conf->get('resource.history'));
469 }
470 return true;
471 }
472
473 /**
474 * Save the datastore -> the link order is now applied when bookmarks are saved.
475 */
476 public function updateMethodReorderDatastore()
477 {
478 $this->linkDB->save($this->conf->get('resource.page_cache'));
479 return true;
480 }
481
482 /**
483 * Change privateonly session key to visibility.
484 */
485 public function updateMethodVisibilitySession()
486 {
487 if (isset($_SESSION['privateonly'])) {
488 unset($_SESSION['privateonly']);
489 $_SESSION['visibility'] = 'private';
490 }
491 return true;
492 }
493
494 /**
495 * Add download size and timeout to the configuration file
496 *
497 * @return bool true if the update is successful, false otherwise.
498 */
499 public function updateMethodDownloadSizeAndTimeoutConf()
500 {
501 if ($this->conf->exists('general.download_max_size')
502 && $this->conf->exists('general.download_timeout')
503 ) {
504 return true;
505 }
506
507 if (!$this->conf->exists('general.download_max_size')) {
508 $this->conf->set('general.download_max_size', 1024 * 1024 * 4);
509 }
510
511 if (!$this->conf->exists('general.download_timeout')) {
512 $this->conf->set('general.download_timeout', 30);
513 }
514
515 $this->conf->write($this->isLoggedIn);
516 return true;
517 }
518
519 /**
520 * * Move thumbnails management to WebThumbnailer, coming with new settings.
521 */
522 public function updateMethodWebThumbnailer()
523 {
524 if ($this->conf->exists('thumbnails.mode')) {
525 return true;
526 }
527
528 $thumbnailsEnabled = extension_loaded('gd') && $this->conf->get('thumbnail.enable_thumbnails', true);
529 $this->conf->set('thumbnails.mode', $thumbnailsEnabled ? Thumbnailer::MODE_ALL : Thumbnailer::MODE_NONE);
530 $this->conf->set('thumbnails.width', 125);
531 $this->conf->set('thumbnails.height', 90);
532 $this->conf->remove('thumbnail');
533 $this->conf->write(true);
534
535 if ($thumbnailsEnabled) {
536 $this->session['warnings'][] = t(
537 'You have enabled or changed thumbnails mode. <a href="?do=thumbs_update">Please synchronize them</a>.'
538 );
539 }
540
541 return true;
542 }
543
544 /**
545 * Set sticky = false on all bookmarks
546 *
547 * @return bool true if the update is successful, false otherwise.
548 */
549 public function updateMethodSetSticky()
550 {
551 foreach ($this->linkDB as $key => $link) {
552 if (isset($link['sticky'])) {
553 return true;
554 }
555 $link['sticky'] = false;
556 $this->linkDB[$key] = $link;
557 }
558
559 $this->linkDB->save($this->conf->get('resource.page_cache'));
560
561 return true;
562 }
563
564 /**
565 * Remove redirector settings.
566 */
567 public function updateMethodRemoveRedirector()
568 {
569 $this->conf->remove('redirector');
570 $this->conf->write(true);
571 return true;
572 }
573
574 /**
575 * Migrate the legacy arrays to Bookmark objects.
576 * Also make a backup of the datastore.
577 */
578 public function updateMethodMigrateDatabase()
579 {
580 $save = $this->conf->get('resource.data_dir') . '/datastore.' . date('YmdHis') . '_1.php';
581 if (! copy($this->conf->get('resource.datastore'), $save)) {
582 die('Could not backup the datastore.');
583 }
584
585 $linksArray = new BookmarkArray();
586 foreach ($this->linkDB as $key => $link) {
587 $linksArray[$key] = (new Bookmark())->fromArray($link);
588 }
589 $linksIo = new BookmarkIO($this->conf);
590 $linksIo->write($linksArray);
591
592 return true;
593 }
594
595 /**
596 * Write the `formatter` setting in config file.
597 * Use markdown if the markdown plugin is enabled, the default one otherwise.
598 * Also remove markdown plugin setting as it is now integrated to the core.
599 */
600 public function updateMethodFormatterSetting()
601 {
602 if (!$this->conf->exists('formatter') || $this->conf->get('formatter') === 'default') {
603 $enabledPlugins = $this->conf->get('general.enabled_plugins');
604 if (($pos = array_search('markdown', $enabledPlugins)) !== false) {
605 $formatter = 'markdown';
606 unset($enabledPlugins[$pos]);
607 $this->conf->set('general.enabled_plugins', array_values($enabledPlugins));
608 } else {
609 $formatter = 'default';
610 }
611 $this->conf->set('formatter', $formatter);
612 $this->conf->write(true);
613 }
614
615 return true;
616 }
617}
diff --git a/application/netscape/NetscapeBookmarkUtils.php b/application/netscape/NetscapeBookmarkUtils.php
index 28665941..d64eef7f 100644
--- a/application/netscape/NetscapeBookmarkUtils.php
+++ b/application/netscape/NetscapeBookmarkUtils.php
@@ -7,8 +7,10 @@ use DateTimeZone;
7use Exception; 7use Exception;
8use Katzgrau\KLogger\Logger; 8use Katzgrau\KLogger\Logger;
9use Psr\Log\LogLevel; 9use Psr\Log\LogLevel;
10use Shaarli\Bookmark\LinkDB; 10use Shaarli\Bookmark\Bookmark;
11use Shaarli\Bookmark\BookmarkServiceInterface;
11use Shaarli\Config\ConfigManager; 12use Shaarli\Config\ConfigManager;
13use Shaarli\Formatter\BookmarkFormatter;
12use Shaarli\History; 14use Shaarli\History;
13use Shaarli\NetscapeBookmarkParser\NetscapeBookmarkParser; 15use Shaarli\NetscapeBookmarkParser\NetscapeBookmarkParser;
14 16
@@ -20,41 +22,39 @@ class NetscapeBookmarkUtils
20{ 22{
21 23
22 /** 24 /**
23 * Filters links and adds Netscape-formatted fields 25 * Filters bookmarks and adds Netscape-formatted fields
24 * 26 *
25 * Added fields: 27 * Added fields:
26 * - timestamp link addition date, using the Unix epoch format 28 * - timestamp link addition date, using the Unix epoch format
27 * - taglist comma-separated tag list 29 * - taglist comma-separated tag list
28 * 30 *
29 * @param LinkDB $linkDb Link datastore 31 * @param BookmarkServiceInterface $bookmarkService Link datastore
30 * @param string $selection Which links to export: (all|private|public) 32 * @param BookmarkFormatter $formatter instance
31 * @param bool $prependNoteUrl Prepend note permalinks with the server's URL 33 * @param string $selection Which bookmarks to export: (all|private|public)
32 * @param string $indexUrl Absolute URL of the Shaarli index page 34 * @param bool $prependNoteUrl Prepend note permalinks with the server's URL
35 * @param string $indexUrl Absolute URL of the Shaarli index page
33 * 36 *
34 * @throws Exception Invalid export selection 37 * @return array The bookmarks to be exported, with additional fields
38 *@throws Exception Invalid export selection
35 * 39 *
36 * @return array The links to be exported, with additional fields
37 */ 40 */
38 public static function filterAndFormat($linkDb, $selection, $prependNoteUrl, $indexUrl) 41 public static function filterAndFormat(
39 { 42 $bookmarkService,
43 $formatter,
44 $selection,
45 $prependNoteUrl,
46 $indexUrl
47 ) {
40 // see tpl/export.html for possible values 48 // see tpl/export.html for possible values
41 if (!in_array($selection, array('all', 'public', 'private'))) { 49 if (!in_array($selection, array('all', 'public', 'private'))) {
42 throw new Exception(t('Invalid export selection:') . ' "' . $selection . '"'); 50 throw new Exception(t('Invalid export selection:') . ' "' . $selection . '"');
43 } 51 }
44 52
45 $bookmarkLinks = array(); 53 $bookmarkLinks = array();
46 foreach ($linkDb as $link) { 54 foreach ($bookmarkService->search([], $selection) as $bookmark) {
47 if ($link['private'] != 0 && $selection == 'public') { 55 $link = $formatter->format($bookmark);
48 continue; 56 $link['taglist'] = implode(',', $bookmark->getTags());
49 } 57 if ($bookmark->isNote() && $prependNoteUrl) {
50 if ($link['private'] == 0 && $selection == 'private') {
51 continue;
52 }
53 $date = $link['created'];
54 $link['timestamp'] = $date->getTimestamp();
55 $link['taglist'] = str_replace(' ', ',', $link['tags']);
56
57 if (is_note($link['url']) && $prependNoteUrl) {
58 $link['url'] = $indexUrl . $link['url']; 58 $link['url'] = $indexUrl . $link['url'];
59 } 59 }
60 60
@@ -69,9 +69,9 @@ class NetscapeBookmarkUtils
69 * 69 *
70 * @param string $filename name of the file to import 70 * @param string $filename name of the file to import
71 * @param int $filesize size of the file to import 71 * @param int $filesize size of the file to import
72 * @param int $importCount how many links were imported 72 * @param int $importCount how many bookmarks were imported
73 * @param int $overwriteCount how many links were overwritten 73 * @param int $overwriteCount how many bookmarks were overwritten
74 * @param int $skipCount how many links were skipped 74 * @param int $skipCount how many bookmarks were skipped
75 * @param int $duration how many seconds did the import take 75 * @param int $duration how many seconds did the import take
76 * 76 *
77 * @return string Summary of the bookmark import status 77 * @return string Summary of the bookmark import status
@@ -91,7 +91,7 @@ class NetscapeBookmarkUtils
91 $status .= vsprintf( 91 $status .= vsprintf(
92 t( 92 t(
93 'was successfully processed in %d seconds: ' 93 'was successfully processed in %d seconds: '
94 . '%d links imported, %d links overwritten, %d links skipped.' 94 . '%d bookmarks imported, %d bookmarks overwritten, %d bookmarks skipped.'
95 ), 95 ),
96 [$duration, $importCount, $overwriteCount, $skipCount] 96 [$duration, $importCount, $overwriteCount, $skipCount]
97 ); 97 );
@@ -102,15 +102,15 @@ class NetscapeBookmarkUtils
102 /** 102 /**
103 * Imports Web bookmarks from an uploaded Netscape bookmark dump 103 * Imports Web bookmarks from an uploaded Netscape bookmark dump
104 * 104 *
105 * @param array $post Server $_POST parameters 105 * @param array $post Server $_POST parameters
106 * @param array $files Server $_FILES parameters 106 * @param array $files Server $_FILES parameters
107 * @param LinkDB $linkDb Loaded LinkDB instance 107 * @param BookmarkServiceInterface $bookmarkService Loaded LinkDB instance
108 * @param ConfigManager $conf instance 108 * @param ConfigManager $conf instance
109 * @param History $history History instance 109 * @param History $history History instance
110 * 110 *
111 * @return string Summary of the bookmark import status 111 * @return string Summary of the bookmark import status
112 */ 112 */
113 public static function import($post, $files, $linkDb, $conf, $history) 113 public static function import($post, $files, $bookmarkService, $conf, $history)
114 { 114 {
115 $start = time(); 115 $start = time();
116 $filename = $files['filetoupload']['name']; 116 $filename = $files['filetoupload']['name'];
@@ -121,10 +121,10 @@ class NetscapeBookmarkUtils
121 return self::importStatus($filename, $filesize); 121 return self::importStatus($filename, $filesize);
122 } 122 }
123 123
124 // Overwrite existing links? 124 // Overwrite existing bookmarks?
125 $overwrite = !empty($post['overwrite']); 125 $overwrite = !empty($post['overwrite']);
126 126
127 // Add tags to all imported links? 127 // Add tags to all imported bookmarks?
128 if (empty($post['default_tags'])) { 128 if (empty($post['default_tags'])) {
129 $defaultTags = array(); 129 $defaultTags = array();
130 } else { 130 } else {
@@ -134,7 +134,7 @@ class NetscapeBookmarkUtils
134 ); 134 );
135 } 135 }
136 136
137 // links are imported as public by default 137 // bookmarks are imported as public by default
138 $defaultPrivacy = 0; 138 $defaultPrivacy = 0;
139 139
140 $parser = new NetscapeBookmarkParser( 140 $parser = new NetscapeBookmarkParser(
@@ -164,22 +164,18 @@ class NetscapeBookmarkUtils
164 // use value from the imported file 164 // use value from the imported file
165 $private = $bkm['pub'] == '1' ? 0 : 1; 165 $private = $bkm['pub'] == '1' ? 0 : 1;
166 } elseif ($post['privacy'] == 'private') { 166 } elseif ($post['privacy'] == 'private') {
167 // all imported links are private 167 // all imported bookmarks are private
168 $private = 1; 168 $private = 1;
169 } elseif ($post['privacy'] == 'public') { 169 } elseif ($post['privacy'] == 'public') {
170 // all imported links are public 170 // all imported bookmarks are public
171 $private = 0; 171 $private = 0;
172 } 172 }
173 173
174 $newLink = array( 174 $link = $bookmarkService->findByUrl($bkm['uri']);
175 'title' => $bkm['title'], 175 $existingLink = $link !== null;
176 'url' => $bkm['uri'], 176 if (! $existingLink) {
177 'description' => $bkm['note'], 177 $link = new Bookmark();
178 'private' => $private, 178 }
179 'tags' => $bkm['tags']
180 );
181
182 $existingLink = $linkDb->getLinkFromUrl($bkm['uri']);
183 179
184 if ($existingLink !== false) { 180 if ($existingLink !== false) {
185 if ($overwrite === false) { 181 if ($overwrite === false) {
@@ -188,28 +184,25 @@ class NetscapeBookmarkUtils
188 continue; 184 continue;
189 } 185 }
190 186
191 // Overwrite an existing link, keep its date 187 $link->setUpdated(new DateTime());
192 $newLink['id'] = $existingLink['id'];
193 $newLink['created'] = $existingLink['created'];
194 $newLink['updated'] = new DateTime();
195 $newLink['shorturl'] = $existingLink['shorturl'];
196 $linkDb[$existingLink['id']] = $newLink;
197 $importCount++;
198 $overwriteCount++; 188 $overwriteCount++;
199 continue; 189 } else {
190 $newLinkDate = new DateTime('@' . strval($bkm['time']));
191 $newLinkDate->setTimezone(new DateTimeZone(date_default_timezone_get()));
192 $link->setCreated($newLinkDate);
200 } 193 }
201 194
202 // Add a new link - @ used for UNIX timestamps 195 $link->setTitle($bkm['title']);
203 $newLinkDate = new DateTime('@' . strval($bkm['time'])); 196 $link->setUrl($bkm['uri'], $conf->get('security.allowed_protocols'));
204 $newLinkDate->setTimezone(new DateTimeZone(date_default_timezone_get())); 197 $link->setDescription($bkm['note']);
205 $newLink['created'] = $newLinkDate; 198 $link->setPrivate($private);
206 $newLink['id'] = $linkDb->getNextId(); 199 $link->setTagsString($bkm['tags']);
207 $newLink['shorturl'] = link_small_hash($newLink['created'], $newLink['id']); 200
208 $linkDb[$newLink['id']] = $newLink; 201 $bookmarkService->addOrSet($link, false);
209 $importCount++; 202 $importCount++;
210 } 203 }
211 204
212 $linkDb->save($conf->get('resource.page_cache')); 205 $bookmarkService->save();
213 $history->importLinks(); 206 $history->importLinks();
214 207
215 $duration = time() - $start; 208 $duration = time() - $start;
diff --git a/application/render/PageBuilder.php b/application/render/PageBuilder.php
index 3f86fc26..65e85aaf 100644
--- a/application/render/PageBuilder.php
+++ b/application/render/PageBuilder.php
@@ -5,7 +5,7 @@ namespace Shaarli\Render;
5use Exception; 5use Exception;
6use RainTPL; 6use RainTPL;
7use Shaarli\ApplicationUtils; 7use Shaarli\ApplicationUtils;
8use Shaarli\Bookmark\LinkDB; 8use Shaarli\Bookmark\BookmarkServiceInterface;
9use Shaarli\Config\ConfigManager; 9use Shaarli\Config\ConfigManager;
10use Shaarli\Thumbnailer; 10use Shaarli\Thumbnailer;
11 11
@@ -34,9 +34,9 @@ class PageBuilder
34 protected $session; 34 protected $session;
35 35
36 /** 36 /**
37 * @var LinkDB $linkDB instance. 37 * @var BookmarkServiceInterface $bookmarkService instance.
38 */ 38 */
39 protected $linkDB; 39 protected $bookmarkService;
40 40
41 /** 41 /**
42 * @var null|string XSRF token 42 * @var null|string XSRF token
@@ -52,18 +52,18 @@ class PageBuilder
52 * PageBuilder constructor. 52 * PageBuilder constructor.
53 * $tpl is initialized at false for lazy loading. 53 * $tpl is initialized at false for lazy loading.
54 * 54 *
55 * @param ConfigManager $conf Configuration Manager instance (reference). 55 * @param ConfigManager $conf Configuration Manager instance (reference).
56 * @param array $session $_SESSION array 56 * @param array $session $_SESSION array
57 * @param LinkDB $linkDB instance. 57 * @param BookmarkServiceInterface $linkDB instance.
58 * @param string $token Session token 58 * @param string $token Session token
59 * @param bool $isLoggedIn 59 * @param bool $isLoggedIn
60 */ 60 */
61 public function __construct(&$conf, $session, $linkDB = null, $token = null, $isLoggedIn = false) 61 public function __construct(&$conf, $session, $linkDB = null, $token = null, $isLoggedIn = false)
62 { 62 {
63 $this->tpl = false; 63 $this->tpl = false;
64 $this->conf = $conf; 64 $this->conf = $conf;
65 $this->session = $session; 65 $this->session = $session;
66 $this->linkDB = $linkDB; 66 $this->bookmarkService = $linkDB;
67 $this->token = $token; 67 $this->token = $token;
68 $this->isLoggedIn = $isLoggedIn; 68 $this->isLoggedIn = $isLoggedIn;
69 } 69 }
@@ -125,8 +125,8 @@ class PageBuilder
125 125
126 $this->tpl->assign('language', $this->conf->get('translation.language')); 126 $this->tpl->assign('language', $this->conf->get('translation.language'));
127 127
128 if ($this->linkDB !== null) { 128 if ($this->bookmarkService !== null) {
129 $this->tpl->assign('tags', $this->linkDB->linksCountPerTag()); 129 $this->tpl->assign('tags', $this->bookmarkService->bookmarksCountPerTag());
130 } 130 }
131 131
132 $this->tpl->assign( 132 $this->tpl->assign(
@@ -141,6 +141,8 @@ class PageBuilder
141 unset($_SESSION['warnings']); 141 unset($_SESSION['warnings']);
142 } 142 }
143 143
144 $this->tpl->assign('formatter', $this->conf->get('formatter', 'default'));
145
144 // To be removed with a proper theme configuration. 146 // To be removed with a proper theme configuration.
145 $this->tpl->assign('conf', $this->conf); 147 $this->tpl->assign('conf', $this->conf);
146 } 148 }
diff --git a/application/updater/Updater.php b/application/updater/Updater.php
index beb9ea9b..95654d81 100644
--- a/application/updater/Updater.php
+++ b/application/updater/Updater.php
@@ -2,25 +2,14 @@
2 2
3namespace Shaarli\Updater; 3namespace Shaarli\Updater;
4 4
5use Exception;
6use RainTPL;
7use ReflectionClass;
8use ReflectionException;
9use ReflectionMethod;
10use Shaarli\ApplicationUtils;
11use Shaarli\Bookmark\LinkDB;
12use Shaarli\Bookmark\LinkFilter;
13use Shaarli\Config\ConfigJson;
14use Shaarli\Config\ConfigManager; 5use Shaarli\Config\ConfigManager;
15use Shaarli\Config\ConfigPhp; 6use Shaarli\Bookmark\BookmarkServiceInterface;
16use Shaarli\Exceptions\IOException;
17use Shaarli\Thumbnailer;
18use Shaarli\Updater\Exception\UpdaterException; 7use Shaarli\Updater\Exception\UpdaterException;
19 8
20/** 9/**
21 * Class updater. 10 * Class Updater.
22 * Used to update stuff when a new Shaarli's version is reached. 11 * Used to update stuff when a new Shaarli's version is reached.
23 * Update methods are ran only once, and the stored in a JSON file. 12 * Update methods are ran only once, and the stored in a TXT file.
24 */ 13 */
25class Updater 14class Updater
26{ 15{
@@ -30,9 +19,9 @@ class Updater
30 protected $doneUpdates; 19 protected $doneUpdates;
31 20
32 /** 21 /**
33 * @var LinkDB instance. 22 * @var BookmarkServiceInterface instance.
34 */ 23 */
35 protected $linkDB; 24 protected $linkServices;
36 25
37 /** 26 /**
38 * @var ConfigManager $conf Configuration Manager instance. 27 * @var ConfigManager $conf Configuration Manager instance.
@@ -45,36 +34,27 @@ class Updater
45 protected $isLoggedIn; 34 protected $isLoggedIn;
46 35
47 /** 36 /**
48 * @var array $_SESSION 37 * @var \ReflectionMethod[] List of current class methods.
49 */
50 protected $session;
51
52 /**
53 * @var ReflectionMethod[] List of current class methods.
54 */ 38 */
55 protected $methods; 39 protected $methods;
56 40
57 /** 41 /**
58 * Object constructor. 42 * Object constructor.
59 * 43 *
60 * @param array $doneUpdates Updates which are already done. 44 * @param array $doneUpdates Updates which are already done.
61 * @param LinkDB $linkDB LinkDB instance. 45 * @param BookmarkServiceInterface $linkDB LinksService instance.
62 * @param ConfigManager $conf Configuration Manager instance. 46 * @param ConfigManager $conf Configuration Manager instance.
63 * @param boolean $isLoggedIn True if the user is logged in. 47 * @param boolean $isLoggedIn True if the user is logged in.
64 * @param array $session $_SESSION (by reference)
65 *
66 * @throws ReflectionException
67 */ 48 */
68 public function __construct($doneUpdates, $linkDB, $conf, $isLoggedIn, &$session = []) 49 public function __construct($doneUpdates, $linkDB, $conf, $isLoggedIn)
69 { 50 {
70 $this->doneUpdates = $doneUpdates; 51 $this->doneUpdates = $doneUpdates;
71 $this->linkDB = $linkDB; 52 $this->linkServices = $linkDB;
72 $this->conf = $conf; 53 $this->conf = $conf;
73 $this->isLoggedIn = $isLoggedIn; 54 $this->isLoggedIn = $isLoggedIn;
74 $this->session = &$session;
75 55
76 // Retrieve all update methods. 56 // Retrieve all update methods.
77 $class = new ReflectionClass($this); 57 $class = new \ReflectionClass($this);
78 $this->methods = $class->getMethods(); 58 $this->methods = $class->getMethods();
79 } 59 }
80 60
@@ -96,12 +76,12 @@ class Updater
96 } 76 }
97 77
98 if ($this->methods === null) { 78 if ($this->methods === null) {
99 throw new UpdaterException(t('Couldn\'t retrieve updater class methods.')); 79 throw new UpdaterException('Couldn\'t retrieve LegacyUpdater class methods.');
100 } 80 }
101 81
102 foreach ($this->methods as $method) { 82 foreach ($this->methods as $method) {
103 // Not an update method or already done, pass. 83 // Not an update method or already done, pass.
104 if (!startsWith($method->getName(), 'updateMethod') 84 if (! startsWith($method->getName(), 'updateMethod')
105 || in_array($method->getName(), $this->doneUpdates) 85 || in_array($method->getName(), $this->doneUpdates)
106 ) { 86 ) {
107 continue; 87 continue;
@@ -114,7 +94,7 @@ class Updater
114 if ($res === true) { 94 if ($res === true) {
115 $updatesRan[] = $method->getName(); 95 $updatesRan[] = $method->getName();
116 } 96 }
117 } catch (Exception $e) { 97 } catch (\Exception $e) {
118 throw new UpdaterException($method, $e); 98 throw new UpdaterException($method, $e);
119 } 99 }
120 } 100 }
@@ -131,432 +111,4 @@ class Updater
131 { 111 {
132 return $this->doneUpdates; 112 return $this->doneUpdates;
133 } 113 }
134
135 /**
136 * Move deprecated options.php to config.php.
137 *
138 * Milestone 0.9 (old versioning) - shaarli/Shaarli#41:
139 * options.php is not supported anymore.
140 */
141 public function updateMethodMergeDeprecatedConfigFile()
142 {
143 if (is_file($this->conf->get('resource.data_dir') . '/options.php')) {
144 include $this->conf->get('resource.data_dir') . '/options.php';
145
146 // Load GLOBALS into config
147 $allowedKeys = array_merge(ConfigPhp::$ROOT_KEYS);
148 $allowedKeys[] = 'config';
149 foreach ($GLOBALS as $key => $value) {
150 if (in_array($key, $allowedKeys)) {
151 $this->conf->set($key, $value);
152 }
153 }
154 $this->conf->write($this->isLoggedIn);
155 unlink($this->conf->get('resource.data_dir') . '/options.php');
156 }
157
158 return true;
159 }
160
161 /**
162 * Move old configuration in PHP to the new config system in JSON format.
163 *
164 * Will rename 'config.php' into 'config.save.php' and create 'config.json.php'.
165 * It will also convert legacy setting keys to the new ones.
166 */
167 public function updateMethodConfigToJson()
168 {
169 // JSON config already exists, nothing to do.
170 if ($this->conf->getConfigIO() instanceof ConfigJson) {
171 return true;
172 }
173
174 $configPhp = new ConfigPhp();
175 $configJson = new ConfigJson();
176 $oldConfig = $configPhp->read($this->conf->getConfigFile() . '.php');
177 rename($this->conf->getConfigFileExt(), $this->conf->getConfigFile() . '.save.php');
178 $this->conf->setConfigIO($configJson);
179 $this->conf->reload();
180
181 $legacyMap = array_flip(ConfigPhp::$LEGACY_KEYS_MAPPING);
182 foreach (ConfigPhp::$ROOT_KEYS as $key) {
183 $this->conf->set($legacyMap[$key], $oldConfig[$key]);
184 }
185
186 // Set sub config keys (config and plugins)
187 $subConfig = array('config', 'plugins');
188 foreach ($subConfig as $sub) {
189 foreach ($oldConfig[$sub] as $key => $value) {
190 if (isset($legacyMap[$sub . '.' . $key])) {
191 $configKey = $legacyMap[$sub . '.' . $key];
192 } else {
193 $configKey = $sub . '.' . $key;
194 }
195 $this->conf->set($configKey, $value);
196 }
197 }
198
199 try {
200 $this->conf->write($this->isLoggedIn);
201 return true;
202 } catch (IOException $e) {
203 error_log($e->getMessage());
204 return false;
205 }
206 }
207
208 /**
209 * Escape settings which have been manually escaped in every request in previous versions:
210 * - general.title
211 * - general.header_link
212 * - redirector.url
213 *
214 * @return bool true if the update is successful, false otherwise.
215 */
216 public function updateMethodEscapeUnescapedConfig()
217 {
218 try {
219 $this->conf->set('general.title', escape($this->conf->get('general.title')));
220 $this->conf->set('general.header_link', escape($this->conf->get('general.header_link')));
221 $this->conf->write($this->isLoggedIn);
222 } catch (Exception $e) {
223 error_log($e->getMessage());
224 return false;
225 }
226 return true;
227 }
228
229 /**
230 * Update the database to use the new ID system, which replaces linkdate primary keys.
231 * Also, creation and update dates are now DateTime objects (done by LinkDB).
232 *
233 * Since this update is very sensitve (changing the whole database), the datastore will be
234 * automatically backed up into the file datastore.<datetime>.php.
235 *
236 * LinkDB also adds the field 'shorturl' with the precedent format (linkdate smallhash),
237 * which will be saved by this method.
238 *
239 * @return bool true if the update is successful, false otherwise.
240 */
241 public function updateMethodDatastoreIds()
242 {
243 // up to date database
244 if (isset($this->linkDB[0])) {
245 return true;
246 }
247
248 $save = $this->conf->get('resource.data_dir') . '/datastore.' . date('YmdHis') . '.php';
249 copy($this->conf->get('resource.datastore'), $save);
250
251 $links = array();
252 foreach ($this->linkDB as $offset => $value) {
253 $links[] = $value;
254 unset($this->linkDB[$offset]);
255 }
256 $links = array_reverse($links);
257 $cpt = 0;
258 foreach ($links as $l) {
259 unset($l['linkdate']);
260 $l['id'] = $cpt;
261 $this->linkDB[$cpt++] = $l;
262 }
263
264 $this->linkDB->save($this->conf->get('resource.page_cache'));
265 $this->linkDB->reorder();
266
267 return true;
268 }
269
270 /**
271 * Rename tags starting with a '-' to work with tag exclusion search.
272 */
273 public function updateMethodRenameDashTags()
274 {
275 $linklist = $this->linkDB->filterSearch();
276 foreach ($linklist as $key => $link) {
277 $link['tags'] = preg_replace('/(^| )\-/', '$1', $link['tags']);
278 $link['tags'] = implode(' ', array_unique(LinkFilter::tagsStrToArray($link['tags'], true)));
279 $this->linkDB[$key] = $link;
280 }
281 $this->linkDB->save($this->conf->get('resource.page_cache'));
282 return true;
283 }
284
285 /**
286 * Initialize API settings:
287 * - api.enabled: true
288 * - api.secret: generated secret
289 */
290 public function updateMethodApiSettings()
291 {
292 if ($this->conf->exists('api.secret')) {
293 return true;
294 }
295
296 $this->conf->set('api.enabled', true);
297 $this->conf->set(
298 'api.secret',
299 generate_api_secret(
300 $this->conf->get('credentials.login'),
301 $this->conf->get('credentials.salt')
302 )
303 );
304 $this->conf->write($this->isLoggedIn);
305 return true;
306 }
307
308 /**
309 * New setting: theme name. If the default theme is used, nothing to do.
310 *
311 * If the user uses a custom theme, raintpl_tpl dir is updated to the parent directory,
312 * and the current theme is set as default in the theme setting.
313 *
314 * @return bool true if the update is successful, false otherwise.
315 */
316 public function updateMethodDefaultTheme()
317 {
318 // raintpl_tpl isn't the root template directory anymore.
319 // We run the update only if this folder still contains the template files.
320 $tplDir = $this->conf->get('resource.raintpl_tpl');
321 $tplFile = $tplDir . '/linklist.html';
322 if (!file_exists($tplFile)) {
323 return true;
324 }
325
326 $parent = dirname($tplDir);
327 $this->conf->set('resource.raintpl_tpl', $parent);
328 $this->conf->set('resource.theme', trim(str_replace($parent, '', $tplDir), '/'));
329 $this->conf->write($this->isLoggedIn);
330
331 // Dependency injection gore
332 RainTPL::$tpl_dir = $tplDir;
333
334 return true;
335 }
336
337 /**
338 * Move the file to inc/user.css to data/user.css.
339 *
340 * Note: Due to hardcoded paths, it's not unit testable. But one line of code should be fine.
341 *
342 * @return bool true if the update is successful, false otherwise.
343 */
344 public function updateMethodMoveUserCss()
345 {
346 if (!is_file('inc/user.css')) {
347 return true;
348 }
349
350 return rename('inc/user.css', 'data/user.css');
351 }
352
353 /**
354 * * `markdown_escape` is a new setting, set to true as default.
355 *
356 * If the markdown plugin was already enabled, escaping is disabled to avoid
357 * breaking existing entries.
358 */
359 public function updateMethodEscapeMarkdown()
360 {
361 if ($this->conf->exists('security.markdown_escape')) {
362 return true;
363 }
364
365 if (in_array('markdown', $this->conf->get('general.enabled_plugins'))) {
366 $this->conf->set('security.markdown_escape', false);
367 } else {
368 $this->conf->set('security.markdown_escape', true);
369 }
370 $this->conf->write($this->isLoggedIn);
371
372 return true;
373 }
374
375 /**
376 * Add 'http://' to Piwik URL the setting is set.
377 *
378 * @return bool true if the update is successful, false otherwise.
379 */
380 public function updateMethodPiwikUrl()
381 {
382 if (!$this->conf->exists('plugins.PIWIK_URL') || startsWith($this->conf->get('plugins.PIWIK_URL'), 'http')) {
383 return true;
384 }
385
386 $this->conf->set('plugins.PIWIK_URL', 'http://' . $this->conf->get('plugins.PIWIK_URL'));
387 $this->conf->write($this->isLoggedIn);
388
389 return true;
390 }
391
392 /**
393 * Use ATOM feed as default.
394 */
395 public function updateMethodAtomDefault()
396 {
397 if (!$this->conf->exists('feed.show_atom') || $this->conf->get('feed.show_atom') === true) {
398 return true;
399 }
400
401 $this->conf->set('feed.show_atom', true);
402 $this->conf->write($this->isLoggedIn);
403
404 return true;
405 }
406
407 /**
408 * Update updates.check_updates_branch setting.
409 *
410 * If the current major version digit matches the latest branch
411 * major version digit, we set the branch to `latest`,
412 * otherwise we'll check updates on the `stable` branch.
413 *
414 * No update required for the dev version.
415 *
416 * Note: due to hardcoded URL and lack of dependency injection, this is not unit testable.
417 *
418 * FIXME! This needs to be removed when we switch to first digit major version
419 * instead of the second one since the versionning process will change.
420 */
421 public function updateMethodCheckUpdateRemoteBranch()
422 {
423 if (SHAARLI_VERSION === 'dev' || $this->conf->get('updates.check_updates_branch') === 'latest') {
424 return true;
425 }
426
427 // Get latest branch major version digit
428 $latestVersion = ApplicationUtils::getLatestGitVersionCode(
429 'https://raw.githubusercontent.com/shaarli/Shaarli/latest/shaarli_version.php',
430 5
431 );
432 if (preg_match('/(\d+)\.\d+$/', $latestVersion, $matches) === false) {
433 return false;
434 }
435 $latestMajor = $matches[1];
436
437 // Get current major version digit
438 preg_match('/(\d+)\.\d+$/', SHAARLI_VERSION, $matches);
439 $currentMajor = $matches[1];
440
441 if ($currentMajor === $latestMajor) {
442 $branch = 'latest';
443 } else {
444 $branch = 'stable';
445 }
446 $this->conf->set('updates.check_updates_branch', $branch);
447 $this->conf->write($this->isLoggedIn);
448 return true;
449 }
450
451 /**
452 * Reset history store file due to date format change.
453 */
454 public function updateMethodResetHistoryFile()
455 {
456 if (is_file($this->conf->get('resource.history'))) {
457 unlink($this->conf->get('resource.history'));
458 }
459 return true;
460 }
461
462 /**
463 * Save the datastore -> the link order is now applied when links are saved.
464 */
465 public function updateMethodReorderDatastore()
466 {
467 $this->linkDB->save($this->conf->get('resource.page_cache'));
468 return true;
469 }
470
471 /**
472 * Change privateonly session key to visibility.
473 */
474 public function updateMethodVisibilitySession()
475 {
476 if (isset($_SESSION['privateonly'])) {
477 unset($_SESSION['privateonly']);
478 $_SESSION['visibility'] = 'private';
479 }
480 return true;
481 }
482
483 /**
484 * Add download size and timeout to the configuration file
485 *
486 * @return bool true if the update is successful, false otherwise.
487 */
488 public function updateMethodDownloadSizeAndTimeoutConf()
489 {
490 if ($this->conf->exists('general.download_max_size')
491 && $this->conf->exists('general.download_timeout')
492 ) {
493 return true;
494 }
495
496 if (!$this->conf->exists('general.download_max_size')) {
497 $this->conf->set('general.download_max_size', 1024 * 1024 * 4);
498 }
499
500 if (!$this->conf->exists('general.download_timeout')) {
501 $this->conf->set('general.download_timeout', 30);
502 }
503
504 $this->conf->write($this->isLoggedIn);
505 return true;
506 }
507
508 /**
509 * * Move thumbnails management to WebThumbnailer, coming with new settings.
510 */
511 public function updateMethodWebThumbnailer()
512 {
513 if ($this->conf->exists('thumbnails.mode')) {
514 return true;
515 }
516
517 $thumbnailsEnabled = extension_loaded('gd') && $this->conf->get('thumbnail.enable_thumbnails', true);
518 $this->conf->set('thumbnails.mode', $thumbnailsEnabled ? Thumbnailer::MODE_ALL : Thumbnailer::MODE_NONE);
519 $this->conf->set('thumbnails.width', 125);
520 $this->conf->set('thumbnails.height', 90);
521 $this->conf->remove('thumbnail');
522 $this->conf->write(true);
523
524 if ($thumbnailsEnabled) {
525 $this->session['warnings'][] = t(
526 'You have enabled or changed thumbnails mode. <a href="?do=thumbs_update">Please synchronize them</a>.'
527 );
528 }
529
530 return true;
531 }
532
533 /**
534 * Set sticky = false on all links
535 *
536 * @return bool true if the update is successful, false otherwise.
537 */
538 public function updateMethodSetSticky()
539 {
540 foreach ($this->linkDB as $key => $link) {
541 if (isset($link['sticky'])) {
542 return true;
543 }
544 $link['sticky'] = false;
545 $this->linkDB[$key] = $link;
546 }
547
548 $this->linkDB->save($this->conf->get('resource.page_cache'));
549
550 return true;
551 }
552
553 /**
554 * Remove redirector settings.
555 */
556 public function updateMethodRemoveRedirector()
557 {
558 $this->conf->remove('redirector');
559 $this->conf->write(true);
560 return true;
561 }
562} 114}
diff --git a/application/updater/UpdaterUtils.php b/application/updater/UpdaterUtils.php
index 34d4f422..828a49fc 100644
--- a/application/updater/UpdaterUtils.php
+++ b/application/updater/UpdaterUtils.php
@@ -1,39 +1,44 @@
1<?php 1<?php
2 2
3/** 3namespace Shaarli\Updater;
4 * Read the updates file, and return already done updates. 4
5 * 5class UpdaterUtils
6 * @param string $updatesFilepath Updates file path.
7 *
8 * @return array Already done update methods.
9 */
10function read_updates_file($updatesFilepath)
11{ 6{
12 if (! empty($updatesFilepath) && is_file($updatesFilepath)) { 7 /**
13 $content = file_get_contents($updatesFilepath); 8 * Read the updates file, and return already done updates.
14 if (! empty($content)) { 9 *
15 return explode(';', $content); 10 * @param string $updatesFilepath Updates file path.
11 *
12 * @return array Already done update methods.
13 */
14 public static function read_updates_file($updatesFilepath)
15 {
16 if (! empty($updatesFilepath) && is_file($updatesFilepath)) {
17 $content = file_get_contents($updatesFilepath);
18 if (! empty($content)) {
19 return explode(';', $content);
20 }
16 } 21 }
22 return array();
17 } 23 }
18 return array();
19}
20 24
21/** 25 /**
22 * Write updates file. 26 * Write updates file.
23 * 27 *
24 * @param string $updatesFilepath Updates file path. 28 * @param string $updatesFilepath Updates file path.
25 * @param array $updates Updates array to write. 29 * @param array $updates Updates array to write.
26 * 30 *
27 * @throws Exception Couldn't write version number. 31 * @throws \Exception Couldn't write version number.
28 */ 32 */
29function write_updates_file($updatesFilepath, $updates) 33 public static function write_updates_file($updatesFilepath, $updates)
30{ 34 {
31 if (empty($updatesFilepath)) { 35 if (empty($updatesFilepath)) {
32 throw new Exception(t('Updates file path is not set, can\'t write updates.')); 36 throw new \Exception('Updates file path is not set, can\'t write updates.');
33 } 37 }
34 38
35 $res = file_put_contents($updatesFilepath, implode(';', $updates)); 39 $res = file_put_contents($updatesFilepath, implode(';', $updates));
36 if ($res === false) { 40 if ($res === false) {
37 throw new Exception(t('Unable to write updates in '. $updatesFilepath . '.')); 41 throw new \Exception('Unable to write updates in '. $updatesFilepath . '.');
42 }
38 } 43 }
39} 44}
diff --git a/plugins/markdown/markdown.css b/assets/common/css/markdown.css
index ce19cd2a..f651e67e 100644
--- a/plugins/markdown/markdown.css
+++ b/assets/common/css/markdown.css
@@ -140,7 +140,7 @@
140 -webkit-hyphens: none; 140 -webkit-hyphens: none;
141 -moz-hyphens: none; 141 -moz-hyphens: none;
142 -ms-hyphens: none; 142 -ms-hyphens: none;
143 hyphens: none; 143 hyphens: none;
144} 144}
145 145
146.markdown :not(pre) code { 146.markdown :not(pre) code {
@@ -155,7 +155,7 @@
155} 155}
156 156
157/* 157/*
158 Remove header links style 158 Remove header bookmarks style
159 */ 159 */
160#pageheader .md_help a { 160#pageheader .md_help a {
161 color: lightgray; 161 color: lightgray;
diff --git a/composer.json b/composer.json
index a028e99a..ada06a74 100644
--- a/composer.json
+++ b/composer.json
@@ -50,7 +50,9 @@
50 "Shaarli\\Config\\Exception\\": "application/config/exception", 50 "Shaarli\\Config\\Exception\\": "application/config/exception",
51 "Shaarli\\Exceptions\\": "application/exceptions", 51 "Shaarli\\Exceptions\\": "application/exceptions",
52 "Shaarli\\Feed\\": "application/feed", 52 "Shaarli\\Feed\\": "application/feed",
53 "Shaarli\\Formatter\\": "application/formatter",
53 "Shaarli\\Http\\": "application/http", 54 "Shaarli\\Http\\": "application/http",
55 "Shaarli\\Legacy\\": "application/legacy",
54 "Shaarli\\Netscape\\": "application/netscape", 56 "Shaarli\\Netscape\\": "application/netscape",
55 "Shaarli\\Plugin\\": "application/plugin", 57 "Shaarli\\Plugin\\": "application/plugin",
56 "Shaarli\\Plugin\\Exception\\": "application/plugin/exception", 58 "Shaarli\\Plugin\\Exception\\": "application/plugin/exception",
diff --git a/composer.lock b/composer.lock
index 36ce8dc6..b3373a32 100644
--- a/composer.lock
+++ b/composer.lock
@@ -8,16 +8,16 @@
8 "packages": [ 8 "packages": [
9 { 9 {
10 "name": "arthurhoaro/web-thumbnailer", 10 "name": "arthurhoaro/web-thumbnailer",
11 "version": "v2.0.0", 11 "version": "v2.0.1",
12 "source": { 12 "source": {
13 "type": "git", 13 "type": "git",
14 "url": "https://github.com/ArthurHoaro/web-thumbnailer.git", 14 "url": "https://github.com/ArthurHoaro/web-thumbnailer.git",
15 "reference": "609a495277ad3e478738d4b8dd522f9cc50c9faa" 15 "reference": "4aa27a1b54b9823341fedd7ca2dcfb11a6b3186a"
16 }, 16 },
17 "dist": { 17 "dist": {
18 "type": "zip", 18 "type": "zip",
19 "url": "https://api.github.com/repos/ArthurHoaro/web-thumbnailer/zipball/609a495277ad3e478738d4b8dd522f9cc50c9faa", 19 "url": "https://api.github.com/repos/ArthurHoaro/web-thumbnailer/zipball/4aa27a1b54b9823341fedd7ca2dcfb11a6b3186a",
20 "reference": "609a495277ad3e478738d4b8dd522f9cc50c9faa", 20 "reference": "4aa27a1b54b9823341fedd7ca2dcfb11a6b3186a",
21 "shasum": "" 21 "shasum": ""
22 }, 22 },
23 "require": { 23 "require": {
@@ -49,7 +49,7 @@
49 } 49 }
50 ], 50 ],
51 "description": "PHP library which will retrieve a thumbnail for any given URL", 51 "description": "PHP library which will retrieve a thumbnail for any given URL",
52 "time": "2019-08-10T11:33:13+00:00" 52 "time": "2020-01-17T19:42:49+00:00"
53 }, 53 },
54 { 54 {
55 "name": "erusev/parsedown", 55 "name": "erusev/parsedown",
@@ -786,16 +786,16 @@
786 }, 786 },
787 { 787 {
788 "name": "myclabs/deep-copy", 788 "name": "myclabs/deep-copy",
789 "version": "1.9.4", 789 "version": "1.9.5",
790 "source": { 790 "source": {
791 "type": "git", 791 "type": "git",
792 "url": "https://github.com/myclabs/DeepCopy.git", 792 "url": "https://github.com/myclabs/DeepCopy.git",
793 "reference": "579bb7356d91f9456ccd505f24ca8b667966a0a7" 793 "reference": "b2c28789e80a97badd14145fda39b545d83ca3ef"
794 }, 794 },
795 "dist": { 795 "dist": {
796 "type": "zip", 796 "type": "zip",
797 "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/579bb7356d91f9456ccd505f24ca8b667966a0a7", 797 "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/b2c28789e80a97badd14145fda39b545d83ca3ef",
798 "reference": "579bb7356d91f9456ccd505f24ca8b667966a0a7", 798 "reference": "b2c28789e80a97badd14145fda39b545d83ca3ef",
799 "shasum": "" 799 "shasum": ""
800 }, 800 },
801 "require": { 801 "require": {
@@ -830,7 +830,7 @@
830 "object", 830 "object",
831 "object graph" 831 "object graph"
832 ], 832 ],
833 "time": "2019-12-15T19:12:40+00:00" 833 "time": "2020-01-17T21:11:47+00:00"
834 }, 834 },
835 { 835 {
836 "name": "phar-io/manifest", 836 "name": "phar-io/manifest",
diff --git a/doc/md/guides/various-hacks.md b/doc/md/guides/various-hacks.md
index b3aa869d..0cef99df 100644
--- a/doc/md/guides/various-hacks.md
+++ b/doc/md/guides/various-hacks.md
@@ -17,14 +17,6 @@ Alternatively, you can transform to JSON format (and pretty-print if you have `j
17php -r 'print(json_encode(unserialize(gzinflate(base64_decode(preg_replace("!.*/\* (.+) \*/.*!", "$1", file_get_contents("data/datastore.php")))))));' | jq . 17php -r 'print(json_encode(unserialize(gzinflate(base64_decode(preg_replace("!.*/\* (.+) \*/.*!", "$1", file_get_contents("data/datastore.php")))))));' | jq .
18``` 18```
19 19
20### Changing the timestamp for a shaare
21
22- Look for `<input type="hidden" name="lf_linkdate" value="{$link.linkdate}">` in `tpl/editlink.tpl` (line 14)
23- Replace `type="hidden"` with `type="text"` from this line
24- A new date/time field becomes available in the edit/new link dialog.
25- You can set the timestamp manually by entering it in the format `YYYMMDD_HHMMS`.
26
27
28### See also 20### See also
29 21
30- [Add a new custom field to shaares (example patch)](https://gist.github.com/nodiscc/8b0194921f059d7b9ad89a581ecd482c) 22- [Add a new custom field to shaares (example patch)](https://gist.github.com/nodiscc/8b0194921f059d7b9ad89a581ecd482c)
diff --git a/index.php b/index.php
index 9783539a..2dd003f0 100644
--- a/index.php
+++ b/index.php
@@ -35,9 +35,6 @@ ini_set('upload_max_filesize', '16M');
35 35
36// See all error except warnings 36// See all error except warnings
37error_reporting(E_ALL^E_WARNING); 37error_reporting(E_ALL^E_WARNING);
38// See all errors (for debugging only)
39//error_reporting(-1);
40
41 38
42// 3rd-party libraries 39// 3rd-party libraries
43if (! file_exists(__DIR__ . '/vendor/autoload.php')) { 40if (! file_exists(__DIR__ . '/vendor/autoload.php')) {
@@ -65,11 +62,15 @@ require_once 'application/TimeZone.php';
65require_once 'application/Utils.php'; 62require_once 'application/Utils.php';
66 63
67use \Shaarli\ApplicationUtils; 64use \Shaarli\ApplicationUtils;
68use \Shaarli\Bookmark\Exception\LinkNotFoundException; 65use Shaarli\Bookmark\BookmarkServiceInterface;
69use \Shaarli\Bookmark\LinkDB; 66use \Shaarli\Bookmark\Exception\BookmarkNotFoundException;
67use Shaarli\Bookmark\Bookmark;
68use Shaarli\Bookmark\BookmarkFilter;
69use Shaarli\Bookmark\BookmarkFileService;
70use \Shaarli\Config\ConfigManager; 70use \Shaarli\Config\ConfigManager;
71use \Shaarli\Feed\CachedPage; 71use \Shaarli\Feed\CachedPage;
72use \Shaarli\Feed\FeedBuilder; 72use \Shaarli\Feed\FeedBuilder;
73use Shaarli\Formatter\FormatterFactory;
73use \Shaarli\History; 74use \Shaarli\History;
74use \Shaarli\Languages; 75use \Shaarli\Languages;
75use \Shaarli\Netscape\NetscapeBookmarkUtils; 76use \Shaarli\Netscape\NetscapeBookmarkUtils;
@@ -81,6 +82,7 @@ use \Shaarli\Security\LoginManager;
81use \Shaarli\Security\SessionManager; 82use \Shaarli\Security\SessionManager;
82use \Shaarli\Thumbnailer; 83use \Shaarli\Thumbnailer;
83use \Shaarli\Updater\Updater; 84use \Shaarli\Updater\Updater;
85use \Shaarli\Updater\UpdaterUtils;
84 86
85// Ensure the PHP version is supported 87// Ensure the PHP version is supported
86try { 88try {
@@ -122,6 +124,17 @@ if (isset($_COOKIE['shaarli']) && !SessionManager::checkId($_COOKIE['shaarli']))
122} 124}
123 125
124$conf = new ConfigManager(); 126$conf = new ConfigManager();
127
128// In dev mode, throw exception on any warning
129if ($conf->get('dev.debug', false)) {
130 // See all errors (for debugging only)
131 error_reporting(-1);
132
133 set_error_handler(function($errno, $errstr, $errfile, $errline, array $errcontext) {
134 throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
135 });
136}
137
125$sessionManager = new SessionManager($_SESSION, $conf); 138$sessionManager = new SessionManager($_SESSION, $conf);
126$loginManager = new LoginManager($conf, $sessionManager); 139$loginManager = new LoginManager($conf, $sessionManager);
127$loginManager->generateStaySignedInToken($_SERVER['REMOTE_ADDR']); 140$loginManager->generateStaySignedInToken($_SERVER['REMOTE_ADDR']);
@@ -140,7 +153,7 @@ if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
140new Languages(setlocale(LC_MESSAGES, 0), $conf); 153new Languages(setlocale(LC_MESSAGES, 0), $conf);
141 154
142$conf->setEmpty('general.timezone', date_default_timezone_get()); 155$conf->setEmpty('general.timezone', date_default_timezone_get());
143$conf->setEmpty('general.title', t('Shared links on '). escape(index_url($_SERVER))); 156$conf->setEmpty('general.title', t('Shared bookmarks on '). escape(index_url($_SERVER)));
144RainTPL::$tpl_dir = $conf->get('resource.raintpl_tpl').'/'.$conf->get('resource.theme').'/'; // template directory 157RainTPL::$tpl_dir = $conf->get('resource.raintpl_tpl').'/'.$conf->get('resource.theme').'/'; // template directory
145RainTPL::$cache_dir = $conf->get('resource.raintpl_tmp'); // cache directory 158RainTPL::$cache_dir = $conf->get('resource.raintpl_tmp'); // cache directory
146 159
@@ -283,14 +296,15 @@ if (!isset($_SESSION['tokens'])) {
283} 296}
284 297
285/** 298/**
286 * Daily RSS feed: 1 RSS entry per day giving all the links on that day. 299 * Daily RSS feed: 1 RSS entry per day giving all the bookmarks on that day.
287 * Gives the last 7 days (which have links). 300 * Gives the last 7 days (which have bookmarks).
288 * This RSS feed cannot be filtered. 301 * This RSS feed cannot be filtered.
289 * 302 *
290 * @param ConfigManager $conf Configuration Manager instance 303 * @param BookmarkServiceInterface $bookmarkService
291 * @param LoginManager $loginManager LoginManager instance 304 * @param ConfigManager $conf Configuration Manager instance
305 * @param LoginManager $loginManager LoginManager instance
292 */ 306 */
293function showDailyRSS($conf, $loginManager) 307function showDailyRSS($bookmarkService, $conf, $loginManager)
294{ 308{
295 // Cache system 309 // Cache system
296 $query = $_SERVER['QUERY_STRING']; 310 $query = $_SERVER['QUERY_STRING'];
@@ -305,28 +319,20 @@ function showDailyRSS($conf, $loginManager)
305 exit; 319 exit;
306 } 320 }
307 321
308 // If cached was not found (or not usable), then read the database and build the response: 322 /* Some Shaarlies may have very few bookmarks, so we need to look
309 // Read links from database (and filter private links if used it not logged in).
310 $LINKSDB = new LinkDB(
311 $conf->get('resource.datastore'),
312 $loginManager->isLoggedIn(),
313 $conf->get('privacy.hide_public_links')
314 );
315
316 /* Some Shaarlies may have very few links, so we need to look
317 back in time until we have enough days ($nb_of_days). 323 back in time until we have enough days ($nb_of_days).
318 */ 324 */
319 $nb_of_days = 7; // We take 7 days. 325 $nb_of_days = 7; // We take 7 days.
320 $today = date('Ymd'); 326 $today = date('Ymd');
321 $days = array(); 327 $days = array();
322 328
323 foreach ($LINKSDB as $link) { 329 foreach ($bookmarkService->search() as $bookmark) {
324 $day = $link['created']->format('Ymd'); // Extract day (without time) 330 $day = $bookmark->getCreated()->format('Ymd'); // Extract day (without time)
325 if (strcmp($day, $today) < 0) { 331 if (strcmp($day, $today) < 0) {
326 if (empty($days[$day])) { 332 if (empty($days[$day])) {
327 $days[$day] = array(); 333 $days[$day] = array();
328 } 334 }
329 $days[$day][] = $link; 335 $days[$day][] = $bookmark;
330 } 336 }
331 337
332 if (count($days) > $nb_of_days) { 338 if (count($days) > $nb_of_days) {
@@ -341,30 +347,38 @@ function showDailyRSS($conf, $loginManager)
341 echo '<channel>'; 347 echo '<channel>';
342 echo '<title>Daily - '. $conf->get('general.title') . '</title>'; 348 echo '<title>Daily - '. $conf->get('general.title') . '</title>';
343 echo '<link>'. $pageaddr .'</link>'; 349 echo '<link>'. $pageaddr .'</link>';
344 echo '<description>Daily shared links</description>'; 350 echo '<description>Daily shared bookmarks</description>';
345 echo '<language>en-en</language>'; 351 echo '<language>en-en</language>';
346 echo '<copyright>'. $pageaddr .'</copyright>'. PHP_EOL; 352 echo '<copyright>'. $pageaddr .'</copyright>'. PHP_EOL;
347 353
354 $factory = new FormatterFactory($conf);
355 $formatter = $factory->getFormatter();
356 $formatter->addContextData('index_url', index_url($_SERVER));
348 // For each day. 357 // For each day.
349 foreach ($days as $day => $links) { 358 /** @var Bookmark[] $bookmarks */
350 $dayDate = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $day.'_000000'); 359 foreach ($days as $day => $bookmarks) {
360 $formattedBookmarks = [];
361 $dayDate = DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, $day.'_000000');
351 $absurl = escape(index_url($_SERVER).'?do=daily&day='.$day); // Absolute URL of the corresponding "Daily" page. 362 $absurl = escape(index_url($_SERVER).'?do=daily&day='.$day); // Absolute URL of the corresponding "Daily" page.
352 363
353 // We pre-format some fields for proper output. 364 // We pre-format some fields for proper output.
354 foreach ($links as &$link) { 365 foreach ($bookmarks as $key => $bookmark) {
355 $link['formatedDescription'] = format_description($link['description']); 366 $formattedBookmarks[$key] = $formatter->format($bookmark);
356 $link['timestamp'] = $link['created']->getTimestamp(); 367 // This page is a bit specific, we need raw description to calculate the length
357 if (is_note($link['url'])) { 368 $formattedBookmarks[$key]['formatedDescription'] = $formattedBookmarks[$key]['description'];
358 $link['url'] = index_url($_SERVER) . $link['url']; // make permalink URL absolute 369 $formattedBookmarks[$key]['description'] = $bookmark->getDescription();
370
371 if ($bookmark->isNote()) {
372 $link['url'] = index_url($_SERVER) . $bookmark->getUrl(); // make permalink URL absolute
359 } 373 }
360 } 374 }
361 375
362 // Then build the HTML for this day: 376 // Then build the HTML for this day:
363 $tpl = new RainTPL; 377 $tpl = new RainTPL();
364 $tpl->assign('title', $conf->get('general.title')); 378 $tpl->assign('title', $conf->get('general.title'));
365 $tpl->assign('daydate', $dayDate->getTimestamp()); 379 $tpl->assign('daydate', $dayDate->getTimestamp());
366 $tpl->assign('absurl', $absurl); 380 $tpl->assign('absurl', $absurl);
367 $tpl->assign('links', $links); 381 $tpl->assign('links', $formattedBookmarks);
368 $tpl->assign('rssdate', escape($dayDate->format(DateTime::RSS))); 382 $tpl->assign('rssdate', escape($dayDate->format(DateTime::RSS)));
369 $tpl->assign('hide_timestamps', $conf->get('privacy.hide_timestamps', false)); 383 $tpl->assign('hide_timestamps', $conf->get('privacy.hide_timestamps', false));
370 $tpl->assign('index_url', $pageaddr); 384 $tpl->assign('index_url', $pageaddr);
@@ -382,13 +396,13 @@ function showDailyRSS($conf, $loginManager)
382/** 396/**
383 * Show the 'Daily' page. 397 * Show the 'Daily' page.
384 * 398 *
385 * @param PageBuilder $pageBuilder Template engine wrapper. 399 * @param PageBuilder $pageBuilder Template engine wrapper.
386 * @param LinkDB $LINKSDB LinkDB instance. 400 * @param BookmarkServiceInterface $bookmarkService instance.
387 * @param ConfigManager $conf Configuration Manager instance. 401 * @param ConfigManager $conf Configuration Manager instance.
388 * @param PluginManager $pluginManager Plugin Manager instance. 402 * @param PluginManager $pluginManager Plugin Manager instance.
389 * @param LoginManager $loginManager Login Manager instance 403 * @param LoginManager $loginManager Login Manager instance
390 */ 404 */
391function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager, $loginManager) 405function showDaily($pageBuilder, $bookmarkService, $conf, $pluginManager, $loginManager)
392{ 406{
393 if (isset($_GET['day'])) { 407 if (isset($_GET['day'])) {
394 $day = $_GET['day']; 408 $day = $_GET['day'];
@@ -402,10 +416,10 @@ function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager, $loginManager)
402 $pageBuilder->assign('dayDesc', t('Today')); 416 $pageBuilder->assign('dayDesc', t('Today'));
403 } 417 }
404 418
405 $days = $LINKSDB->days(); 419 $days = $bookmarkService->days();
406 $i = array_search($day, $days); 420 $i = array_search($day, $days);
407 if ($i === false && count($days)) { 421 if ($i === false && count($days)) {
408 // no links for day, but at least one day with links 422 // no bookmarks for day, but at least one day with bookmarks
409 $i = count($days) - 1; 423 $i = count($days) - 1;
410 $day = $days[$i]; 424 $day = $days[$i];
411 } 425 }
@@ -414,29 +428,30 @@ function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager, $loginManager)
414 428
415 if ($i !== false) { 429 if ($i !== false) {
416 if ($i >= 1) { 430 if ($i >= 1) {
417 $previousday=$days[$i - 1]; 431 $previousday = $days[$i - 1];
418 } 432 }
419 if ($i < count($days) - 1) { 433 if ($i < count($days) - 1) {
420 $nextday = $days[$i + 1]; 434 $nextday = $days[$i + 1];
421 } 435 }
422 } 436 }
423 try { 437 try {
424 $linksToDisplay = $LINKSDB->filterDay($day); 438 $linksToDisplay = $bookmarkService->filterDay($day);
425 } catch (Exception $exc) { 439 } catch (Exception $exc) {
426 error_log($exc); 440 error_log($exc);
427 $linksToDisplay = array(); 441 $linksToDisplay = [];
428 } 442 }
429 443
444 $factory = new FormatterFactory($conf);
445 $formatter = $factory->getFormatter();
430 // We pre-format some fields for proper output. 446 // We pre-format some fields for proper output.
431 foreach ($linksToDisplay as $key => $link) { 447 foreach ($linksToDisplay as $key => $bookmark) {
432 $taglist = explode(' ', $link['tags']); 448 $linksToDisplay[$key] = $formatter->format($bookmark);
433 uasort($taglist, 'strcasecmp'); 449 // This page is a bit specific, we need raw description to calculate the length
434 $linksToDisplay[$key]['taglist']=$taglist; 450 $linksToDisplay[$key]['formatedDescription'] = $linksToDisplay[$key]['description'];
435 $linksToDisplay[$key]['formatedDescription'] = format_description($link['description']); 451 $linksToDisplay[$key]['description'] = $bookmark->getDescription();
436 $linksToDisplay[$key]['timestamp'] = $link['created']->getTimestamp();
437 } 452 }
438 453
439 $dayDate = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $day.'_000000'); 454 $dayDate = DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, $day.'_000000');
440 $data = array( 455 $data = array(
441 'pagetitle' => $conf->get('general.title') .' - '. format_date($dayDate, false), 456 'pagetitle' => $conf->get('general.title') .' - '. format_date($dayDate, false),
442 'linksToDisplay' => $linksToDisplay, 457 'linksToDisplay' => $linksToDisplay,
@@ -457,19 +472,19 @@ function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager, $loginManager)
457 */ 472 */
458 $columns = array(array(), array(), array()); // Entries to display, for each column. 473 $columns = array(array(), array(), array()); // Entries to display, for each column.
459 $fill = array(0, 0, 0); // Rough estimate of columns fill. 474 $fill = array(0, 0, 0); // Rough estimate of columns fill.
460 foreach ($data['linksToDisplay'] as $key => $link) { 475 foreach ($data['linksToDisplay'] as $key => $bookmark) {
461 // Roughly estimate length of entry (by counting characters) 476 // Roughly estimate length of entry (by counting characters)
462 // Title: 30 chars = 1 line. 1 line is 30 pixels height. 477 // Title: 30 chars = 1 line. 1 line is 30 pixels height.
463 // Description: 836 characters gives roughly 342 pixel height. 478 // Description: 836 characters gives roughly 342 pixel height.
464 // This is not perfect, but it's usually OK. 479 // This is not perfect, but it's usually OK.
465 $length = strlen($link['title']) + (342 * strlen($link['description'])) / 836; 480 $length = strlen($bookmark['title']) + (342 * strlen($bookmark['description'])) / 836;
466 if (! empty($link['thumbnail'])) { 481 if (! empty($bookmark['thumbnail'])) {
467 $length += 100; // 1 thumbnails roughly takes 100 pixels height. 482 $length += 100; // 1 thumbnails roughly takes 100 pixels height.
468 } 483 }
469 // Then put in column which is the less filled: 484 // Then put in column which is the less filled:
470 $smallest = min($fill); // find smallest value in array. 485 $smallest = min($fill); // find smallest value in array.
471 $index = array_search($smallest, $fill); // find index of this smallest value. 486 $index = array_search($smallest, $fill); // find index of this smallest value.
472 array_push($columns[$index], $link); // Put entry in this column. 487 array_push($columns[$index], $bookmark); // Put entry in this column.
473 $fill[$index] += $length; 488 $fill[$index] += $length;
474 } 489 }
475 490
@@ -487,40 +502,39 @@ function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager, $loginManager)
487/** 502/**
488 * Renders the linklist 503 * Renders the linklist
489 * 504 *
490 * @param pageBuilder $PAGE pageBuilder instance. 505 * @param pageBuilder $PAGE pageBuilder instance.
491 * @param LinkDB $LINKSDB LinkDB instance. 506 * @param BookmarkServiceInterface $linkDb instance.
492 * @param ConfigManager $conf Configuration Manager instance. 507 * @param ConfigManager $conf Configuration Manager instance.
493 * @param PluginManager $pluginManager Plugin Manager instance. 508 * @param PluginManager $pluginManager Plugin Manager instance.
494 */ 509 */
495function showLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager) 510function showLinkList($PAGE, $linkDb, $conf, $pluginManager, $loginManager)
496{ 511{
497 buildLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager); 512 buildLinkList($PAGE, $linkDb, $conf, $pluginManager, $loginManager);
498 $PAGE->renderPage('linklist'); 513 $PAGE->renderPage('linklist');
499} 514}
500 515
501/** 516/**
502 * Render HTML page (according to URL parameters and user rights) 517 * Render HTML page (according to URL parameters and user rights)
503 * 518 *
504 * @param ConfigManager $conf Configuration Manager instance. 519 * @param ConfigManager $conf Configuration Manager instance.
505 * @param PluginManager $pluginManager Plugin Manager instance, 520 * @param PluginManager $pluginManager Plugin Manager instance,
506 * @param LinkDB $LINKSDB 521 * @param BookmarkServiceInterface $bookmarkService
507 * @param History $history instance 522 * @param History $history instance
508 * @param SessionManager $sessionManager SessionManager instance 523 * @param SessionManager $sessionManager SessionManager instance
509 * @param LoginManager $loginManager LoginManager instance 524 * @param LoginManager $loginManager LoginManager instance
510 */ 525 */
511function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager, $loginManager) 526function renderPage($conf, $pluginManager, $bookmarkService, $history, $sessionManager, $loginManager)
512{ 527{
513 $updater = new Updater( 528 $updater = new Updater(
514 read_updates_file($conf->get('resource.updates')), 529 UpdaterUtils::read_updates_file($conf->get('resource.updates')),
515 $LINKSDB, 530 $bookmarkService,
516 $conf, 531 $conf,
517 $loginManager->isLoggedIn(), 532 $loginManager->isLoggedIn()
518 $_SESSION
519 ); 533 );
520 try { 534 try {
521 $newUpdates = $updater->update(); 535 $newUpdates = $updater->update();
522 if (! empty($newUpdates)) { 536 if (! empty($newUpdates)) {
523 write_updates_file( 537 UpdaterUtils::write_updates_file(
524 $conf->get('resource.updates'), 538 $conf->get('resource.updates'),
525 $updater->getDoneUpdates() 539 $updater->getDoneUpdates()
526 ); 540 );
@@ -529,9 +543,9 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
529 die($e->getMessage()); 543 die($e->getMessage());
530 } 544 }
531 545
532 $PAGE = new PageBuilder($conf, $_SESSION, $LINKSDB, $sessionManager->generateToken(), $loginManager->isLoggedIn()); 546 $PAGE = new PageBuilder($conf, $_SESSION, $bookmarkService, $sessionManager->generateToken(), $loginManager->isLoggedIn());
533 $PAGE->assign('linkcount', count($LINKSDB)); 547 $PAGE->assign('linkcount', $bookmarkService->count(BookmarkFilter::$ALL));
534 $PAGE->assign('privateLinkcount', count_private($LINKSDB)); 548 $PAGE->assign('privateLinkcount', $bookmarkService->count(BookmarkFilter::$PRIVATE));
535 $PAGE->assign('plugin_errors', $pluginManager->getErrors()); 549 $PAGE->assign('plugin_errors', $pluginManager->getErrors());
536 550
537 // Determine which page will be rendered. 551 // Determine which page will be rendered.
@@ -611,27 +625,28 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
611 } 625 }
612 626
613 // Optionally filter the results: 627 // Optionally filter the results:
614 $links = $LINKSDB->filterSearch($_GET); 628 $links = $bookmarkService->search($_GET);
615 $linksToDisplay = array(); 629 $linksToDisplay = [];
616 630
617 // Get only links which have a thumbnail. 631 // Get only bookmarks which have a thumbnail.
618 // Note: we do not retrieve thumbnails here, the request is too heavy. 632 // Note: we do not retrieve thumbnails here, the request is too heavy.
633 $factory = new FormatterFactory($conf);
634 $formatter = $factory->getFormatter();
619 foreach ($links as $key => $link) { 635 foreach ($links as $key => $link) {
620 if (isset($link['thumbnail']) && $link['thumbnail'] !== false) { 636 if ($link->getThumbnail() !== false) {
621 $linksToDisplay[] = $link; // Add to array. 637 $linksToDisplay[] = $formatter->format($link);
622 } 638 }
623 } 639 }
624 640
625 $data = array( 641 $data = [
626 'linksToDisplay' => $linksToDisplay, 642 'linksToDisplay' => $linksToDisplay,
627 ); 643 ];
628 $pluginManager->executeHooks('render_picwall', $data, array('loggedin' => $loginManager->isLoggedIn())); 644 $pluginManager->executeHooks('render_picwall', $data, ['loggedin' => $loginManager->isLoggedIn()]);
629 645
630 foreach ($data as $key => $value) { 646 foreach ($data as $key => $value) {
631 $PAGE->assign($key, $value); 647 $PAGE->assign($key, $value);
632 } 648 }
633 649
634
635 $PAGE->renderPage('picwall'); 650 $PAGE->renderPage('picwall');
636 exit; 651 exit;
637 } 652 }
@@ -640,7 +655,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
640 if ($targetPage == Router::$PAGE_TAGCLOUD) { 655 if ($targetPage == Router::$PAGE_TAGCLOUD) {
641 $visibility = ! empty($_SESSION['visibility']) ? $_SESSION['visibility'] : ''; 656 $visibility = ! empty($_SESSION['visibility']) ? $_SESSION['visibility'] : '';
642 $filteringTags = isset($_GET['searchtags']) ? explode(' ', $_GET['searchtags']) : []; 657 $filteringTags = isset($_GET['searchtags']) ? explode(' ', $_GET['searchtags']) : [];
643 $tags = $LINKSDB->linksCountPerTag($filteringTags, $visibility); 658 $tags = $bookmarkService->bookmarksCountPerTag($filteringTags, $visibility);
644 659
645 // We sort tags alphabetically, then choose a font size according to count. 660 // We sort tags alphabetically, then choose a font size according to count.
646 // First, find max value. 661 // First, find max value.
@@ -687,7 +702,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
687 if ($targetPage == Router::$PAGE_TAGLIST) { 702 if ($targetPage == Router::$PAGE_TAGLIST) {
688 $visibility = ! empty($_SESSION['visibility']) ? $_SESSION['visibility'] : ''; 703 $visibility = ! empty($_SESSION['visibility']) ? $_SESSION['visibility'] : '';
689 $filteringTags = isset($_GET['searchtags']) ? explode(' ', $_GET['searchtags']) : []; 704 $filteringTags = isset($_GET['searchtags']) ? explode(' ', $_GET['searchtags']) : [];
690 $tags = $LINKSDB->linksCountPerTag($filteringTags, $visibility); 705 $tags = $bookmarkService->bookmarksCountPerTag($filteringTags, $visibility);
691 foreach ($filteringTags as $tag) { 706 foreach ($filteringTags as $tag) {
692 if (array_key_exists($tag, $tags)) { 707 if (array_key_exists($tag, $tags)) {
693 unset($tags[$tag]); 708 unset($tags[$tag]);
@@ -717,7 +732,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
717 732
718 // Daily page. 733 // Daily page.
719 if ($targetPage == Router::$PAGE_DAILY) { 734 if ($targetPage == Router::$PAGE_DAILY) {
720 showDaily($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager); 735 showDaily($PAGE, $bookmarkService, $conf, $pluginManager, $loginManager);
721 } 736 }
722 737
723 // ATOM and RSS feed. 738 // ATOM and RSS feed.
@@ -738,8 +753,16 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
738 exit; 753 exit;
739 } 754 }
740 755
756 $factory = new FormatterFactory($conf);
741 // Generate data. 757 // Generate data.
742 $feedGenerator = new FeedBuilder($LINKSDB, $feedType, $_SERVER, $_GET, $loginManager->isLoggedIn()); 758 $feedGenerator = new FeedBuilder(
759 $bookmarkService,
760 $factory->getFormatter(),
761 $feedType,
762 $_SERVER,
763 $_GET,
764 $loginManager->isLoggedIn()
765 );
743 $feedGenerator->setLocale(strtolower(setlocale(LC_COLLATE, 0))); 766 $feedGenerator->setLocale(strtolower(setlocale(LC_COLLATE, 0)));
744 $feedGenerator->setHideDates($conf->get('privacy.hide_timestamps') && !$loginManager->isLoggedIn()); 767 $feedGenerator->setHideDates($conf->get('privacy.hide_timestamps') && !$loginManager->isLoggedIn());
745 $feedGenerator->setUsePermalinks(isset($_GET['permalinks']) || !$conf->get('feed.rss_permalinks')); 768 $feedGenerator->setUsePermalinks(isset($_GET['permalinks']) || !$conf->get('feed.rss_permalinks'));
@@ -845,7 +868,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
845 exit; 868 exit;
846 } 869 }
847 870
848 // -------- User wants to change the number of links per page (linksperpage=...) 871 // -------- User wants to change the number of bookmarks per page (linksperpage=...)
849 if (isset($_GET['linksperpage'])) { 872 if (isset($_GET['linksperpage'])) {
850 if (is_numeric($_GET['linksperpage'])) { 873 if (is_numeric($_GET['linksperpage'])) {
851 $_SESSION['LINKS_PER_PAGE']=abs(intval($_GET['linksperpage'])); 874 $_SESSION['LINKS_PER_PAGE']=abs(intval($_GET['linksperpage']));
@@ -860,19 +883,19 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
860 exit; 883 exit;
861 } 884 }
862 885
863 // -------- User wants to see only private links (toggle) 886 // -------- User wants to see only private bookmarks (toggle)
864 if (isset($_GET['visibility'])) { 887 if (isset($_GET['visibility'])) {
865 if ($_GET['visibility'] === 'private') { 888 if ($_GET['visibility'] === 'private') {
866 // Visibility not set or not already private, set private, otherwise reset it 889 // Visibility not set or not already private, set private, otherwise reset it
867 if (empty($_SESSION['visibility']) || $_SESSION['visibility'] !== 'private') { 890 if (empty($_SESSION['visibility']) || $_SESSION['visibility'] !== 'private') {
868 // See only private links 891 // See only private bookmarks
869 $_SESSION['visibility'] = 'private'; 892 $_SESSION['visibility'] = 'private';
870 } else { 893 } else {
871 unset($_SESSION['visibility']); 894 unset($_SESSION['visibility']);
872 } 895 }
873 } elseif ($_GET['visibility'] === 'public') { 896 } elseif ($_GET['visibility'] === 'public') {
874 if (empty($_SESSION['visibility']) || $_SESSION['visibility'] !== 'public') { 897 if (empty($_SESSION['visibility']) || $_SESSION['visibility'] !== 'public') {
875 // See only public links 898 // See only public bookmarks
876 $_SESSION['visibility'] = 'public'; 899 $_SESSION['visibility'] = 'public';
877 } else { 900 } else {
878 unset($_SESSION['visibility']); 901 unset($_SESSION['visibility']);
@@ -888,7 +911,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
888 exit; 911 exit;
889 } 912 }
890 913
891 // -------- User wants to see only untagged links (toggle) 914 // -------- User wants to see only untagged bookmarks (toggle)
892 if (isset($_GET['untaggedonly'])) { 915 if (isset($_GET['untaggedonly'])) {
893 $_SESSION['untaggedonly'] = empty($_SESSION['untaggedonly']); 916 $_SESSION['untaggedonly'] = empty($_SESSION['untaggedonly']);
894 917
@@ -916,7 +939,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
916 exit; 939 exit;
917 } 940 }
918 941
919 showLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager); 942 showLinkList($PAGE, $bookmarkService, $conf, $pluginManager, $loginManager);
920 if (isset($_GET['edit_link'])) { 943 if (isset($_GET['edit_link'])) {
921 header('Location: ?do=login&edit_link='. escape($_GET['edit_link'])); 944 header('Location: ?do=login&edit_link='. escape($_GET['edit_link']));
922 exit; 945 exit;
@@ -1022,7 +1045,11 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
1022 $conf->set('privacy.hide_public_links', !empty($_POST['hidePublicLinks'])); 1045 $conf->set('privacy.hide_public_links', !empty($_POST['hidePublicLinks']));
1023 $conf->set('api.enabled', !empty($_POST['enableApi'])); 1046 $conf->set('api.enabled', !empty($_POST['enableApi']));
1024 $conf->set('api.secret', escape($_POST['apiSecret'])); 1047 $conf->set('api.secret', escape($_POST['apiSecret']));
1025 $conf->set('translation.language', escape($_POST['language'])); 1048 $conf->set('formatter', escape($_POST['formatter']));
1049
1050 if (! empty($_POST['language'])) {
1051 $conf->set('translation.language', escape($_POST['language']));
1052 }
1026 1053
1027 $thumbnailsMode = extension_loaded('gd') ? $_POST['enableThumbnails'] : Thumbnailer::MODE_NONE; 1054 $thumbnailsMode = extension_loaded('gd') ? $_POST['enableThumbnails'] : Thumbnailer::MODE_NONE;
1028 if ($thumbnailsMode !== Thumbnailer::MODE_NONE 1055 if ($thumbnailsMode !== Thumbnailer::MODE_NONE
@@ -1056,6 +1083,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
1056 $PAGE->assign('title', $conf->get('general.title')); 1083 $PAGE->assign('title', $conf->get('general.title'));
1057 $PAGE->assign('theme', $conf->get('resource.theme')); 1084 $PAGE->assign('theme', $conf->get('resource.theme'));
1058 $PAGE->assign('theme_available', ThemeUtils::getThemes($conf->get('resource.raintpl_tpl'))); 1085 $PAGE->assign('theme_available', ThemeUtils::getThemes($conf->get('resource.raintpl_tpl')));
1086 $PAGE->assign('formatter_available', ['default', 'markdown']);
1059 list($continents, $cities) = generateTimeZoneData( 1087 list($continents, $cities) = generateTimeZoneData(
1060 timezone_identifiers_list(), 1088 timezone_identifiers_list(),
1061 $conf->get('general.timezone') 1089 $conf->get('general.timezone')
@@ -1093,17 +1121,25 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
1093 } 1121 }
1094 1122
1095 $toTag = isset($_POST['totag']) ? escape($_POST['totag']) : null; 1123 $toTag = isset($_POST['totag']) ? escape($_POST['totag']) : null;
1096 $alteredLinks = $LINKSDB->renameTag(escape($_POST['fromtag']), $toTag); 1124 $fromTag = escape($_POST['fromtag']);
1097 $LINKSDB->save($conf->get('resource.page_cache')); 1125 $count = 0;
1098 foreach ($alteredLinks as $link) { 1126 $bookmarks = $bookmarkService->search(['searchtags' => $fromTag], BookmarkFilter::$ALL, true);
1099 $history->updateLink($link); 1127 foreach ($bookmarks as $bookmark) {
1128 if ($toTag) {
1129 $bookmark->renameTag($fromTag, $toTag);
1130 } else {
1131 $bookmark->deleteTag($fromTag);
1132 }
1133 $bookmarkService->set($bookmark, false);
1134 $history->updateLink($bookmark);
1135 $count++;
1100 } 1136 }
1137 $bookmarkService->save();
1101 $delete = empty($_POST['totag']); 1138 $delete = empty($_POST['totag']);
1102 $redirect = $delete ? 'do=changetag' : 'searchtags='. urlencode(escape($_POST['totag'])); 1139 $redirect = $delete ? 'do=changetag' : 'searchtags='. urlencode(escape($_POST['totag']));
1103 $count = count($alteredLinks);
1104 $alert = $delete 1140 $alert = $delete
1105 ? sprintf(t('The tag was removed from %d link.', 'The tag was removed from %d links.', $count), $count) 1141 ? sprintf(t('The tag was removed from %d link.', 'The tag was removed from %d bookmarks.', $count), $count)
1106 : sprintf(t('The tag was renamed in %d link.', 'The tag was renamed in %d links.', $count), $count); 1142 : sprintf(t('The tag was renamed in %d link.', 'The tag was renamed in %d bookmarks.', $count), $count);
1107 echo '<script>alert("'. $alert .'");document.location=\'?'. $redirect .'\';</script>'; 1143 echo '<script>alert("'. $alert .'");document.location=\'?'. $redirect .'\';</script>';
1108 exit; 1144 exit;
1109 } 1145 }
@@ -1123,69 +1159,37 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
1123 } 1159 }
1124 1160
1125 // lf_id should only be present if the link exists. 1161 // lf_id should only be present if the link exists.
1126 $id = isset($_POST['lf_id']) ? intval(escape($_POST['lf_id'])) : $LINKSDB->getNextId(); 1162 $id = isset($_POST['lf_id']) ? intval(escape($_POST['lf_id'])) : null;
1127 $link['id'] = $id; 1163 if ($id && $bookmarkService->exists($id)) {
1128 // Linkdate is kept here to:
1129 // - use the same permalink for notes as they're displayed when creating them
1130 // - let users hack creation date of their posts
1131 // See: https://shaarli.readthedocs.io/en/master/guides/various-hacks/#changing-the-timestamp-for-a-shaare
1132 $linkdate = escape($_POST['lf_linkdate']);
1133 $link['created'] = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $linkdate);
1134 if (isset($LINKSDB[$id])) {
1135 // Edit 1164 // Edit
1136 $link['updated'] = new DateTime(); 1165 $bookmark = $bookmarkService->get($id);
1137 $link['shorturl'] = $LINKSDB[$id]['shorturl'];
1138 $link['sticky'] = isset($LINKSDB[$id]['sticky']) ? $LINKSDB[$id]['sticky'] : false;
1139 $new = false;
1140 } else { 1166 } else {
1141 // New link 1167 // New link
1142 $link['updated'] = null; 1168 $bookmark = new Bookmark();
1143 $link['shorturl'] = link_small_hash($link['created'], $id);
1144 $link['sticky'] = false;
1145 $new = true;
1146 }
1147
1148 // Remove multiple spaces.
1149 $tags = trim(preg_replace('/\s\s+/', ' ', $_POST['lf_tags']));
1150 // Remove first '-' char in tags.
1151 $tags = preg_replace('/(^| )\-/', '$1', $tags);
1152 // Remove duplicates.
1153 $tags = implode(' ', array_unique(explode(' ', $tags)));
1154
1155 if (empty(trim($_POST['lf_url']))) {
1156 $_POST['lf_url'] = '?' . smallHash($linkdate . $id);
1157 } 1169 }
1158 $url = whitelist_protocols(trim($_POST['lf_url']), $conf->get('security.allowed_protocols'));
1159 1170
1160 $link = array_merge($link, [ 1171 $bookmark->setTitle($_POST['lf_title']);
1161 'title' => trim($_POST['lf_title']), 1172 $bookmark->setDescription($_POST['lf_description']);
1162 'url' => $url, 1173 $bookmark->setUrl($_POST['lf_url'], $conf->get('security.allowed_protocols'));
1163 'description' => $_POST['lf_description'], 1174 $bookmark->setPrivate(isset($_POST['lf_private']));
1164 'private' => (isset($_POST['lf_private']) ? 1 : 0), 1175 $bookmark->setTagsString($_POST['lf_tags']);
1165 'tags' => str_replace(',', ' ', $tags),
1166 ]);
1167
1168 // If title is empty, use the URL as title.
1169 if ($link['title'] == '') {
1170 $link['title'] = $link['url'];
1171 }
1172 1176
1173 if ($conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE 1177 if ($conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE
1174 && ! is_note($link['url']) 1178 && ! $bookmark->isNote()
1175 ) { 1179 ) {
1176 $thumbnailer = new Thumbnailer($conf); 1180 $thumbnailer = new Thumbnailer($conf);
1177 $link['thumbnail'] = $thumbnailer->get($url); 1181 $bookmark->setThumbnail($thumbnailer->get($bookmark->getUrl()));
1178 } 1182 }
1183 $bookmarkService->addOrSet($bookmark, false);
1179 1184
1180 $pluginManager->executeHooks('save_link', $link); 1185 // To preserve backward compatibility with 3rd parties, plugins still use arrays
1186 $factory = new FormatterFactory($conf);
1187 $formatter = $factory->getFormatter('raw');
1188 $data = $formatter->format($bookmark);
1189 $pluginManager->executeHooks('save_link', $data);
1181 1190
1182 $LINKSDB[$id] = $link; 1191 $bookmark->fromArray($data);
1183 $LINKSDB->save($conf->get('resource.page_cache')); 1192 $bookmarkService->set($bookmark);
1184 if ($new) {
1185 $history->addLink($link);
1186 } else {
1187 $history->updateLink($link);
1188 }
1189 1193
1190 // If we are called from the bookmarklet, we must close the popup: 1194 // If we are called from the bookmarklet, we must close the popup:
1191 if (isset($_GET['source']) && ($_GET['source']=='bookmarklet' || $_GET['source']=='firefoxsocialapi')) { 1195 if (isset($_GET['source']) && ($_GET['source']=='bookmarklet' || $_GET['source']=='firefoxsocialapi')) {
@@ -1196,32 +1200,12 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
1196 $returnurl = !empty($_POST['returnurl']) ? $_POST['returnurl'] : '?'; 1200 $returnurl = !empty($_POST['returnurl']) ? $_POST['returnurl'] : '?';
1197 $location = generateLocation($returnurl, $_SERVER['HTTP_HOST'], array('addlink', 'post', 'edit_link')); 1201 $location = generateLocation($returnurl, $_SERVER['HTTP_HOST'], array('addlink', 'post', 'edit_link'));
1198 // Scroll to the link which has been edited. 1202 // Scroll to the link which has been edited.
1199 $location .= '#' . $link['shorturl']; 1203 $location .= '#' . $bookmark->getShortUrl();
1200 // After saving the link, redirect to the page the user was on. 1204 // After saving the link, redirect to the page the user was on.
1201 header('Location: '. $location); 1205 header('Location: '. $location);
1202 exit; 1206 exit;
1203 } 1207 }
1204 1208
1205 // -------- User clicked the "Cancel" button when editing a link.
1206 if (isset($_POST['cancel_edit'])) {
1207 $id = isset($_POST['lf_id']) ? (int) escape($_POST['lf_id']) : false;
1208 if (! isset($LINKSDB[$id])) {
1209 header('Location: ?');
1210 }
1211 // If we are called from the bookmarklet, we must close the popup:
1212 if (isset($_GET['source']) && ($_GET['source']=='bookmarklet' || $_GET['source']=='firefoxsocialapi')) {
1213 echo '<script>self.close();</script>';
1214 exit;
1215 }
1216 $link = $LINKSDB[$id];
1217 $returnurl = ( isset($_POST['returnurl']) ? $_POST['returnurl'] : '?' );
1218 // Scroll to the link which has been edited.
1219 $returnurl .= '#'. $link['shorturl'];
1220 $returnurl = generateLocation($returnurl, $_SERVER['HTTP_HOST'], array('addlink', 'post', 'edit_link'));
1221 header('Location: '.$returnurl); // After canceling, redirect to the page the user was on.
1222 exit;
1223 }
1224
1225 // -------- User clicked the "Delete" button when editing a link: Delete link from database. 1209 // -------- User clicked the "Delete" button when editing a link: Delete link from database.
1226 if ($targetPage == Router::$PAGE_DELETELINK) { 1210 if ($targetPage == Router::$PAGE_DELETELINK) {
1227 if (! $sessionManager->checkToken($_GET['token'])) { 1211 if (! $sessionManager->checkToken($_GET['token'])) {
@@ -1231,23 +1215,31 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
1231 $ids = trim($_GET['lf_linkdate']); 1215 $ids = trim($_GET['lf_linkdate']);
1232 if (strpos($ids, ' ') !== false) { 1216 if (strpos($ids, ' ') !== false) {
1233 // multiple, space-separated ids provided 1217 // multiple, space-separated ids provided
1234 $ids = array_values(array_filter(preg_split('/\s+/', escape($ids)))); 1218 $ids = array_values(array_filter(
1219 preg_split('/\s+/', escape($ids)),
1220 function ($item) {
1221 return $item !== '';
1222 }
1223 ));
1235 } else { 1224 } else {
1236 // only a single id provided 1225 // only a single id provided
1226 $shortUrl = $bookmarkService->get($ids)->getShortUrl();
1237 $ids = [$ids]; 1227 $ids = [$ids];
1238 } 1228 }
1239 // assert at least one id is given 1229 // assert at least one id is given
1240 if (!count($ids)) { 1230 if (!count($ids)) {
1241 die('no id provided'); 1231 die('no id provided');
1242 } 1232 }
1233 $factory = new FormatterFactory($conf);
1234 $formatter = $factory->getFormatter('raw');
1243 foreach ($ids as $id) { 1235 foreach ($ids as $id) {
1244 $id = (int) escape($id); 1236 $id = (int) escape($id);
1245 $link = $LINKSDB[$id]; 1237 $bookmark = $bookmarkService->get($id);
1246 $pluginManager->executeHooks('delete_link', $link); 1238 $data = $formatter->format($bookmark);
1247 $history->deleteLink($link); 1239 $pluginManager->executeHooks('delete_link', $data);
1248 unset($LINKSDB[$id]); 1240 $bookmarkService->remove($bookmark, false);
1249 } 1241 }
1250 $LINKSDB->save($conf->get('resource.page_cache')); // save to disk 1242 $bookmarkService->save();
1251 1243
1252 // If we are called from the bookmarklet, we must close the popup: 1244 // If we are called from the bookmarklet, we must close the popup:
1253 if (isset($_GET['source']) && ($_GET['source']=='bookmarklet' || $_GET['source']=='firefoxsocialapi')) { 1245 if (isset($_GET['source']) && ($_GET['source']=='bookmarklet' || $_GET['source']=='firefoxsocialapi')) {
@@ -1261,7 +1253,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
1261 $location = generateLocation( 1253 $location = generateLocation(
1262 $_SERVER['HTTP_REFERER'], 1254 $_SERVER['HTTP_REFERER'],
1263 $_SERVER['HTTP_HOST'], 1255 $_SERVER['HTTP_HOST'],
1264 ['delete_link', 'edit_link', $link['shorturl']] 1256 ['delete_link', 'edit_link', ! empty($shortUrl) ? $shortUrl : null]
1265 ); 1257 );
1266 } 1258 }
1267 1259
@@ -1294,14 +1286,21 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
1294 } else { 1286 } else {
1295 $private = $_GET['newVisibility'] === 'private'; 1287 $private = $_GET['newVisibility'] === 'private';
1296 } 1288 }
1289 $factory = new FormatterFactory($conf);
1290 $formatter = $factory->getFormatter('raw');
1297 foreach ($ids as $id) { 1291 foreach ($ids as $id) {
1298 $id = (int) escape($id); 1292 $id = (int) escape($id);
1299 $link = $LINKSDB[$id]; 1293 $bookmark = $bookmarkService->get($id);
1300 $link['private'] = $private; 1294 $bookmark->setPrivate($private);
1301 $pluginManager->executeHooks('save_link', $link); 1295
1302 $LINKSDB[$id] = $link; 1296 // To preserve backward compatibility with 3rd parties, plugins still use arrays
1297 $data = $formatter->format($bookmark);
1298 $pluginManager->executeHooks('save_link', $data);
1299 $bookmark->fromArray($data);
1300
1301 $bookmarkService->set($bookmark);
1303 } 1302 }
1304 $LINKSDB->save($conf->get('resource.page_cache')); // save to disk 1303 $bookmarkService->save();
1305 1304
1306 $location = '?'; 1305 $location = '?';
1307 if (isset($_SERVER['HTTP_REFERER'])) { 1306 if (isset($_SERVER['HTTP_REFERER'])) {
@@ -1317,17 +1316,22 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
1317 // -------- User clicked the "EDIT" button on a link: Display link edit form. 1316 // -------- User clicked the "EDIT" button on a link: Display link edit form.
1318 if (isset($_GET['edit_link'])) { 1317 if (isset($_GET['edit_link'])) {
1319 $id = (int) escape($_GET['edit_link']); 1318 $id = (int) escape($_GET['edit_link']);
1320 $link = $LINKSDB[$id]; // Read database 1319 try {
1321 if (!$link) { 1320 $link = $bookmarkService->get($id); // Read database
1321 } catch (BookmarkNotFoundException $e) {
1322 // Link not found in database.
1322 header('Location: ?'); 1323 header('Location: ?');
1323 exit; 1324 exit;
1324 } // Link not found in database. 1325 }
1325 $link['linkdate'] = $link['created']->format(LinkDB::LINK_DATE_FORMAT); 1326
1327 $factory = new FormatterFactory($conf);
1328 $formatter = $factory->getFormatter('raw');
1329 $formattedLink = $formatter->format($link);
1326 $data = array( 1330 $data = array(
1327 'link' => $link, 1331 'link' => $formattedLink,
1328 'link_is_new' => false, 1332 'link_is_new' => false,
1329 'http_referer' => (isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']) : ''), 1333 'http_referer' => (isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']) : ''),
1330 'tags' => $LINKSDB->linksCountPerTag(), 1334 'tags' => $bookmarkService->bookmarksCountPerTag(),
1331 ); 1335 );
1332 $pluginManager->executeHooks('render_editlink', $data); 1336 $pluginManager->executeHooks('render_editlink', $data);
1333 1337
@@ -1346,10 +1350,9 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
1346 1350
1347 $link_is_new = false; 1351 $link_is_new = false;
1348 // Check if URL is not already in database (in this case, we will edit the existing link) 1352 // Check if URL is not already in database (in this case, we will edit the existing link)
1349 $link = $LINKSDB->getLinkFromUrl($url); 1353 $bookmark = $bookmarkService->findByUrl($url);
1350 if (! $link) { 1354 if (! $bookmark) {
1351 $link_is_new = true; 1355 $link_is_new = true;
1352 $linkdate = strval(date(LinkDB::LINK_DATE_FORMAT));
1353 // Get title if it was provided in URL (by the bookmarklet). 1356 // Get title if it was provided in URL (by the bookmarklet).
1354 $title = empty($_GET['title']) ? '' : escape($_GET['title']); 1357 $title = empty($_GET['title']) ? '' : escape($_GET['title']);
1355 // Get description if it was provided in URL (by the bookmarklet). [Bronco added that] 1358 // Get description if it was provided in URL (by the bookmarklet). [Bronco added that]
@@ -1375,32 +1378,32 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
1375 } 1378 }
1376 1379
1377 if ($url == '') { 1380 if ($url == '') {
1378 $url = '?' . smallHash($linkdate . $LINKSDB->getNextId());
1379 $title = $conf->get('general.default_note_title', t('Note: ')); 1381 $title = $conf->get('general.default_note_title', t('Note: '));
1380 } 1382 }
1381 $url = escape($url); 1383 $url = escape($url);
1382 $title = escape($title); 1384 $title = escape($title);
1383 1385
1384 $link = array( 1386 $link = [
1385 'linkdate' => $linkdate,
1386 'title' => $title, 1387 'title' => $title,
1387 'url' => $url, 1388 'url' => $url,
1388 'description' => $description, 1389 'description' => $description,
1389 'tags' => $tags, 1390 'tags' => $tags,
1390 'private' => $private, 1391 'private' => $private,
1391 ); 1392 ];
1392 } else { 1393 } else {
1393 $link['linkdate'] = $link['created']->format(LinkDB::LINK_DATE_FORMAT); 1394 $factory = new FormatterFactory($conf);
1395 $formatter = $factory->getFormatter('raw');
1396 $link = $formatter->format($bookmark);
1394 } 1397 }
1395 1398
1396 $data = array( 1399 $data = [
1397 'link' => $link, 1400 'link' => $link,
1398 'link_is_new' => $link_is_new, 1401 'link_is_new' => $link_is_new,
1399 'http_referer' => (isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']) : ''), 1402 'http_referer' => (isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']) : ''),
1400 'source' => (isset($_GET['source']) ? $_GET['source'] : ''), 1403 'source' => (isset($_GET['source']) ? $_GET['source'] : ''),
1401 'tags' => $LINKSDB->linksCountPerTag(), 1404 'tags' => $bookmarkService->bookmarksCountPerTag(),
1402 'default_private_links' => $conf->get('privacy.default_private_links', false), 1405 'default_private_links' => $conf->get('privacy.default_private_links', false),
1403 ); 1406 ];
1404 $pluginManager->executeHooks('render_editlink', $data); 1407 $pluginManager->executeHooks('render_editlink', $data);
1405 1408
1406 foreach ($data as $key => $value) { 1409 foreach ($data as $key => $value) {
@@ -1413,7 +1416,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
1413 } 1416 }
1414 1417
1415 if ($targetPage == Router::$PAGE_PINLINK) { 1418 if ($targetPage == Router::$PAGE_PINLINK) {
1416 if (! isset($_GET['id']) || empty($LINKSDB[$_GET['id']])) { 1419 if (! isset($_GET['id']) || !$bookmarkService->exists($_GET['id'])) {
1417 // FIXME! Use a proper error system. 1420 // FIXME! Use a proper error system.
1418 $msg = t('Invalid link ID provided'); 1421 $msg = t('Invalid link ID provided');
1419 echo '<script>alert("'. $msg .'");document.location=\''. index_url($_SERVER) .'\';</script>'; 1422 echo '<script>alert("'. $msg .'");document.location=\''. index_url($_SERVER) .'\';</script>';
@@ -1423,16 +1426,15 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
1423 die('Wrong token.'); 1426 die('Wrong token.');
1424 } 1427 }
1425 1428
1426 $link = $LINKSDB[$_GET['id']]; 1429 $link = $bookmarkService->get($_GET['id']);
1427 $link['sticky'] = ! $link['sticky']; 1430 $link->setSticky(! $link->isSticky());
1428 $LINKSDB[(int) $_GET['id']] = $link; 1431 $bookmarkService->set($link);
1429 $LINKSDB->save($conf->get('resource.page_cache'));
1430 header('Location: '.index_url($_SERVER)); 1432 header('Location: '.index_url($_SERVER));
1431 exit; 1433 exit;
1432 } 1434 }
1433 1435
1434 if ($targetPage == Router::$PAGE_EXPORT) { 1436 if ($targetPage == Router::$PAGE_EXPORT) {
1435 // Export links as a Netscape Bookmarks file 1437 // Export bookmarks as a Netscape Bookmarks file
1436 1438
1437 if (empty($_GET['selection'])) { 1439 if (empty($_GET['selection'])) {
1438 $PAGE->assign('pagetitle', t('Export') .' - '. $conf->get('general.title', 'Shaarli')); 1440 $PAGE->assign('pagetitle', t('Export') .' - '. $conf->get('general.title', 'Shaarli'));
@@ -1449,10 +1451,13 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
1449 } 1451 }
1450 1452
1451 try { 1453 try {
1454 $factory = new FormatterFactory($conf);
1455 $formatter = $factory->getFormatter('raw');
1452 $PAGE->assign( 1456 $PAGE->assign(
1453 'links', 1457 'links',
1454 NetscapeBookmarkUtils::filterAndFormat( 1458 NetscapeBookmarkUtils::filterAndFormat(
1455 $LINKSDB, 1459 $bookmarkService,
1460 $formatter,
1456 $selection, 1461 $selection,
1457 $prependNoteUrl, 1462 $prependNoteUrl,
1458 index_url($_SERVER) 1463 index_url($_SERVER)
@@ -1467,7 +1472,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
1467 header('Content-Type: text/html; charset=utf-8'); 1472 header('Content-Type: text/html; charset=utf-8');
1468 header( 1473 header(
1469 'Content-disposition: attachment; filename=bookmarks_' 1474 'Content-disposition: attachment; filename=bookmarks_'
1470 .$selection.'_'.$now->format(LinkDB::LINK_DATE_FORMAT).'.html' 1475 .$selection.'_'.$now->format(Bookmark::LINK_DATE_FORMAT).'.html'
1471 ); 1476 );
1472 $PAGE->assign('date', $now->format(DateTime::RFC822)); 1477 $PAGE->assign('date', $now->format(DateTime::RFC822));
1473 $PAGE->assign('eol', PHP_EOL); 1478 $PAGE->assign('eol', PHP_EOL);
@@ -1521,7 +1526,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
1521 $status = NetscapeBookmarkUtils::import( 1526 $status = NetscapeBookmarkUtils::import(
1522 $_POST, 1527 $_POST,
1523 $_FILES, 1528 $_FILES,
1524 $LINKSDB, 1529 $bookmarkService,
1525 $conf, 1530 $conf,
1526 $history 1531 $history
1527 ); 1532 );
@@ -1592,19 +1597,19 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
1592 // Get a fresh token 1597 // Get a fresh token
1593 if ($targetPage == Router::$GET_TOKEN) { 1598 if ($targetPage == Router::$GET_TOKEN) {
1594 header('Content-Type:text/plain'); 1599 header('Content-Type:text/plain');
1595 echo $sessionManager->generateToken($conf); 1600 echo $sessionManager->generateToken();
1596 exit; 1601 exit;
1597 } 1602 }
1598 1603
1599 // -------- Thumbnails Update 1604 // -------- Thumbnails Update
1600 if ($targetPage == Router::$PAGE_THUMBS_UPDATE) { 1605 if ($targetPage == Router::$PAGE_THUMBS_UPDATE) {
1601 $ids = []; 1606 $ids = [];
1602 foreach ($LINKSDB as $link) { 1607 foreach ($bookmarkService->search() as $bookmark) {
1603 // A note or not HTTP(S) 1608 // A note or not HTTP(S)
1604 if (is_note($link['url']) || ! startsWith(strtolower($link['url']), 'http')) { 1609 if ($bookmark->isNote() || ! startsWith(strtolower($bookmark->getUrl()), 'http')) {
1605 continue; 1610 continue;
1606 } 1611 }
1607 $ids[] = $link['id']; 1612 $ids[] = $bookmark->getId();
1608 } 1613 }
1609 $PAGE->assign('ids', $ids); 1614 $PAGE->assign('ids', $ids);
1610 $PAGE->assign('pagetitle', t('Thumbnails update') .' - '. $conf->get('general.title', 'Shaarli')); 1615 $PAGE->assign('pagetitle', t('Thumbnails update') .' - '. $conf->get('general.title', 'Shaarli'));
@@ -1619,37 +1624,40 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
1619 exit; 1624 exit;
1620 } 1625 }
1621 $id = (int) $_POST['id']; 1626 $id = (int) $_POST['id'];
1622 if (empty($LINKSDB[$id])) { 1627 if (! $bookmarkService->exists($id)) {
1623 http_response_code(404); 1628 http_response_code(404);
1624 exit; 1629 exit;
1625 } 1630 }
1626 $thumbnailer = new Thumbnailer($conf); 1631 $thumbnailer = new Thumbnailer($conf);
1627 $link = $LINKSDB[$id]; 1632 $bookmark = $bookmarkService->get($id);
1628 $link['thumbnail'] = $thumbnailer->get($link['url']); 1633 $bookmark->setThumbnail($thumbnailer->get($bookmark->getUrl()));
1629 $LINKSDB[$id] = $link; 1634 $bookmarkService->set($bookmark);
1630 $LINKSDB->save($conf->get('resource.page_cache'));
1631 1635
1632 echo json_encode($link); 1636 $factory = new FormatterFactory($conf);
1637 echo json_encode($factory->getFormatter('raw')->format($bookmark));
1633 exit; 1638 exit;
1634 } 1639 }
1635 1640
1636 // -------- Otherwise, simply display search form and links: 1641 // -------- Otherwise, simply display search form and bookmarks:
1637 showLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager); 1642 showLinkList($PAGE, $bookmarkService, $conf, $pluginManager, $loginManager);
1638 exit; 1643 exit;
1639} 1644}
1640 1645
1641/** 1646/**
1642 * Template for the list of links (<div id="linklist">) 1647 * Template for the list of bookmarks (<div id="linklist">)
1643 * This function fills all the necessary fields in the $PAGE for the template 'linklist.html' 1648 * This function fills all the necessary fields in the $PAGE for the template 'linklist.html'
1644 * 1649 *
1645 * @param pageBuilder $PAGE pageBuilder instance. 1650 * @param pageBuilder $PAGE pageBuilder instance.
1646 * @param LinkDB $LINKSDB LinkDB instance. 1651 * @param BookmarkServiceInterface $linkDb LinkDB instance.
1647 * @param ConfigManager $conf Configuration Manager instance. 1652 * @param ConfigManager $conf Configuration Manager instance.
1648 * @param PluginManager $pluginManager Plugin Manager instance. 1653 * @param PluginManager $pluginManager Plugin Manager instance.
1649 * @param LoginManager $loginManager LoginManager instance 1654 * @param LoginManager $loginManager LoginManager instance
1650 */ 1655 */
1651function buildLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager) 1656function buildLinkList($PAGE, $linkDb, $conf, $pluginManager, $loginManager)
1652{ 1657{
1658 $factory = new FormatterFactory($conf);
1659 $formatter = $factory->getFormatter();
1660
1653 // Used in templates 1661 // Used in templates
1654 if (isset($_GET['searchtags'])) { 1662 if (isset($_GET['searchtags'])) {
1655 if (! empty($_GET['searchtags'])) { 1663 if (! empty($_GET['searchtags'])) {
@@ -1666,19 +1674,19 @@ function buildLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager)
1666 if (! empty($_SERVER['QUERY_STRING']) 1674 if (! empty($_SERVER['QUERY_STRING'])
1667 && preg_match('/^[a-zA-Z0-9-_@]{6}($|&|#)/', $_SERVER['QUERY_STRING'])) { 1675 && preg_match('/^[a-zA-Z0-9-_@]{6}($|&|#)/', $_SERVER['QUERY_STRING'])) {
1668 try { 1676 try {
1669 $linksToDisplay = $LINKSDB->filterHash($_SERVER['QUERY_STRING']); 1677 $linksToDisplay = $linkDb->findByHash($_SERVER['QUERY_STRING']);
1670 } catch (LinkNotFoundException $e) { 1678 } catch (BookmarkNotFoundException $e) {
1671 $PAGE->render404($e->getMessage()); 1679 $PAGE->render404($e->getMessage());
1672 exit; 1680 exit;
1673 } 1681 }
1674 } else { 1682 } else {
1675 // Filter links according search parameters. 1683 // Filter bookmarks according search parameters.
1676 $visibility = ! empty($_SESSION['visibility']) ? $_SESSION['visibility'] : ''; 1684 $visibility = ! empty($_SESSION['visibility']) ? $_SESSION['visibility'] : '';
1677 $request = [ 1685 $request = [
1678 'searchtags' => $searchtags, 1686 'searchtags' => $searchtags,
1679 'searchterm' => $searchterm, 1687 'searchterm' => $searchterm,
1680 ]; 1688 ];
1681 $linksToDisplay = $LINKSDB->filterSearch($request, false, $visibility, !empty($_SESSION['untaggedonly'])); 1689 $linksToDisplay = $linkDb->search($request, $visibility, false, !empty($_SESSION['untaggedonly']));
1682 } 1690 }
1683 1691
1684 // ---- Handle paging. 1692 // ---- Handle paging.
@@ -1704,36 +1712,26 @@ function buildLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager)
1704 1712
1705 $linkDisp = array(); 1713 $linkDisp = array();
1706 while ($i<$end && $i<count($keys)) { 1714 while ($i<$end && $i<count($keys)) {
1707 $link = $linksToDisplay[$keys[$i]]; 1715 $link = $formatter->format($linksToDisplay[$keys[$i]]);
1708 $link['description'] = format_description($link['description']);
1709 $classLi = ($i % 2) != 0 ? '' : 'publicLinkHightLight';
1710 $link['class'] = $link['private'] == 0 ? $classLi : 'private';
1711 $link['timestamp'] = $link['created']->getTimestamp();
1712 if (! empty($link['updated'])) {
1713 $link['updated_timestamp'] = $link['updated']->getTimestamp();
1714 } else {
1715 $link['updated_timestamp'] = '';
1716 }
1717 $taglist = preg_split('/\s+/', $link['tags'], -1, PREG_SPLIT_NO_EMPTY);
1718 uasort($taglist, 'strcasecmp');
1719 $link['taglist'] = $taglist;
1720 1716
1721 // Logged in, thumbnails enabled, not a note, 1717 // Logged in, thumbnails enabled, not a note,
1722 // and (never retrieved yet or no valid cache file) 1718 // and (never retrieved yet or no valid cache file)
1723 if ($loginManager->isLoggedIn() && $thumbnailsEnabled && $link['url'][0] != '?' 1719 if ($loginManager->isLoggedIn()
1724 && (! isset($link['thumbnail']) || ($link['thumbnail'] !== false && ! is_file($link['thumbnail']))) 1720 && $thumbnailsEnabled
1721 && !$linksToDisplay[$keys[$i]]->isNote()
1722 && $linksToDisplay[$keys[$i]]->getThumbnail() !== false
1723 && ! is_file($linksToDisplay[$keys[$i]]->getThumbnail())
1725 ) { 1724 ) {
1726 $elem = $LINKSDB[$keys[$i]]; 1725 $linksToDisplay[$keys[$i]]->setThumbnail($thumbnailer->get($link['url']));
1727 $elem['thumbnail'] = $thumbnailer->get($link['url']); 1726 $linkDb->set($linksToDisplay[$keys[$i]], false);
1728 $LINKSDB[$keys[$i]] = $elem;
1729 $updateDB = true; 1727 $updateDB = true;
1730 $link['thumbnail'] = $elem['thumbnail']; 1728 $link['thumbnail'] = $linksToDisplay[$keys[$i]]->getThumbnail();
1731 } 1729 }
1732 1730
1733 // Check for both signs of a note: starting with ? and 7 chars long. 1731 // Check for both signs of a note: starting with ? and 7 chars long.
1734 if ($link['url'][0] === '?' && strlen($link['url']) === 7) { 1732// if ($link['url'][0] === '?' && strlen($link['url']) === 7) {
1735 $link['url'] = index_url($_SERVER) . $link['url']; 1733// $link['url'] = index_url($_SERVER) . $link['url'];
1736 } 1734// }
1737 1735
1738 $linkDisp[$keys[$i]] = $link; 1736 $linkDisp[$keys[$i]] = $link;
1739 $i++; 1737 $i++;
@@ -1741,7 +1739,7 @@ function buildLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager)
1741 1739
1742 // If we retrieved new thumbnails, we update the database. 1740 // If we retrieved new thumbnails, we update the database.
1743 if (!empty($updateDB)) { 1741 if (!empty($updateDB)) {
1744 $LINKSDB->save($conf->get('resource.page_cache')); 1742 $linkDb->save();
1745 } 1743 }
1746 1744
1747 // Compute paging navigation 1745 // Compute paging navigation
@@ -1771,7 +1769,7 @@ function buildLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager)
1771 1769
1772 // If there is only a single link, we change on-the-fly the title of the page. 1770 // If there is only a single link, we change on-the-fly the title of the page.
1773 if (count($linksToDisplay) == 1) { 1771 if (count($linksToDisplay) == 1) {
1774 $data['pagetitle'] = $linksToDisplay[$keys[0]]['title'] .' - '. $conf->get('general.title'); 1772 $data['pagetitle'] = $linksToDisplay[$keys[0]]->getTitle() .' - '. $conf->get('general.title');
1775 } elseif (! empty($searchterm) || ! empty($searchtags)) { 1773 } elseif (! empty($searchterm) || ! empty($searchtags)) {
1776 $data['pagetitle'] = t('Search: '); 1774 $data['pagetitle'] = t('Search: ');
1777 $data['pagetitle'] .= ! empty($searchterm) ? $searchterm .' ' : ''; 1775 $data['pagetitle'] .= ! empty($searchterm) ? $searchterm .' ' : '';
@@ -1856,7 +1854,7 @@ function install($conf, $sessionManager, $loginManager)
1856 if (!empty($_POST['title'])) { 1854 if (!empty($_POST['title'])) {
1857 $conf->set('general.title', escape($_POST['title'])); 1855 $conf->set('general.title', escape($_POST['title']));
1858 } else { 1856 } else {
1859 $conf->set('general.title', 'Shared links on '.escape(index_url($_SERVER))); 1857 $conf->set('general.title', 'Shared bookmarks on '.escape(index_url($_SERVER)));
1860 } 1858 }
1861 $conf->set('translation.language', escape($_POST['language'])); 1859 $conf->set('translation.language', escape($_POST['language']));
1862 $conf->set('updates.check_updates', !empty($_POST['updateCheck'])); 1860 $conf->set('updates.check_updates', !empty($_POST['updateCheck']));
@@ -1881,9 +1879,16 @@ function install($conf, $sessionManager, $loginManager)
1881 echo '<script>alert("'. $e->getMessage() .'");document.location=\'?\';</script>'; 1879 echo '<script>alert("'. $e->getMessage() .'");document.location=\'?\';</script>';
1882 exit; 1880 exit;
1883 } 1881 }
1882
1883 $history = new History($conf->get('resource.history'));
1884 $bookmarkService = new BookmarkFileService($conf, $history, true);
1885 if ($bookmarkService->count() === 0) {
1886 $bookmarkService->initialize();
1887 }
1888
1884 echo '<script>alert(' 1889 echo '<script>alert('
1885 .'"Shaarli is now configured. ' 1890 .'"Shaarli is now configured. '
1886 .'Please enter your login/password and start shaaring your links!"' 1891 .'Please enter your login/password and start shaaring your bookmarks!"'
1887 .');document.location=\'?do=login\';</script>'; 1892 .');document.location=\'?do=login\';</script>';
1888 exit; 1893 exit;
1889 } 1894 }
@@ -1897,11 +1902,6 @@ function install($conf, $sessionManager, $loginManager)
1897 exit; 1902 exit;
1898} 1903}
1899 1904
1900if (isset($_SERVER['QUERY_STRING']) && startsWith($_SERVER['QUERY_STRING'], 'do=dailyrss')) {
1901 showDailyRSS($conf, $loginManager);
1902 exit;
1903}
1904
1905if (!isset($_SESSION['LINKS_PER_PAGE'])) { 1905if (!isset($_SESSION['LINKS_PER_PAGE'])) {
1906 $_SESSION['LINKS_PER_PAGE'] = $conf->get('general.links_per_page', 20); 1906 $_SESSION['LINKS_PER_PAGE'] = $conf->get('general.links_per_page', 20);
1907} 1907}
@@ -1912,11 +1912,12 @@ try {
1912 die($e->getMessage()); 1912 die($e->getMessage());
1913} 1913}
1914 1914
1915$linkDb = new LinkDB( 1915$linkDb = new BookmarkFileService($conf, $history, $loginManager->isLoggedIn());
1916 $conf->get('resource.datastore'), 1916
1917 $loginManager->isLoggedIn(), 1917if (isset($_SERVER['QUERY_STRING']) && startsWith($_SERVER['QUERY_STRING'], 'do=dailyrss')) {
1918 $conf->get('privacy.hide_public_links') 1918 showDailyRSS($linkDb, $conf, $loginManager);
1919); 1919 exit;
1920}
1920 1921
1921$container = new \Slim\Container(); 1922$container = new \Slim\Container();
1922$container['conf'] = $conf; 1923$container['conf'] = $conf;
@@ -1927,11 +1928,11 @@ $app = new \Slim\App($container);
1927// REST API routes 1928// REST API routes
1928$app->group('/api/v1', function () { 1929$app->group('/api/v1', function () {
1929 $this->get('/info', '\Shaarli\Api\Controllers\Info:getInfo')->setName('getInfo'); 1930 $this->get('/info', '\Shaarli\Api\Controllers\Info:getInfo')->setName('getInfo');
1930 $this->get('/links', '\Shaarli\Api\Controllers\Links:getLinks')->setName('getLinks'); 1931 $this->get('/bookmarks', '\Shaarli\Api\Controllers\Links:getLinks')->setName('getLinks');
1931 $this->get('/links/{id:[\d]+}', '\Shaarli\Api\Controllers\Links:getLink')->setName('getLink'); 1932 $this->get('/bookmarks/{id:[\d]+}', '\Shaarli\Api\Controllers\Links:getLink')->setName('getLink');
1932 $this->post('/links', '\Shaarli\Api\Controllers\Links:postLink')->setName('postLink'); 1933 $this->post('/bookmarks', '\Shaarli\Api\Controllers\Links:postLink')->setName('postLink');
1933 $this->put('/links/{id:[\d]+}', '\Shaarli\Api\Controllers\Links:putLink')->setName('putLink'); 1934 $this->put('/bookmarks/{id:[\d]+}', '\Shaarli\Api\Controllers\Links:putLink')->setName('putLink');
1934 $this->delete('/links/{id:[\d]+}', '\Shaarli\Api\Controllers\Links:deleteLink')->setName('deleteLink'); 1935 $this->delete('/bookmarks/{id:[\d]+}', '\Shaarli\Api\Controllers\Links:deleteLink')->setName('deleteLink');
1935 1936
1936 $this->get('/tags', '\Shaarli\Api\Controllers\Tags:getTags')->setName('getTags'); 1937 $this->get('/tags', '\Shaarli\Api\Controllers\Tags:getTags')->setName('getTags');
1937 $this->get('/tags/{tagName:[\w]+}', '\Shaarli\Api\Controllers\Tags:getTag')->setName('getTag'); 1938 $this->get('/tags/{tagName:[\w]+}', '\Shaarli\Api\Controllers\Tags:getTag')->setName('getTag');
diff --git a/plugins/markdown/README.md b/plugins/markdown/README.md
deleted file mode 100644
index bc9427e2..00000000
--- a/plugins/markdown/README.md
+++ /dev/null
@@ -1,102 +0,0 @@
1## Markdown Shaarli plugin
2
3Convert all your shaares description to HTML formatted Markdown.
4
5[Read more about Markdown syntax](http://daringfireball.net/projects/markdown/syntax).
6
7Markdown processing is done with [Parsedown library](https://github.com/erusev/parsedown).
8
9### Installation
10
11As a default plugin, it should already be in `tpl/plugins/` directory.
12If not, download and unpack it there.
13
14The directory structure should look like:
15
16```
17--- plugins
18 |--- markdown
19 |--- help.html
20 |--- markdown.css
21 |--- markdown.meta
22 |--- markdown.php
23 |--- README.md
24```
25
26To enable the plugin, just check it in the plugin administration page.
27
28You can also add `markdown` to your list of enabled plugins in `data/config.json.php`
29(`general.enabled_plugins` list).
30
31This should look like:
32
33```
34"general": {
35 "enabled_plugins": [
36 "markdown",
37 [...]
38 ],
39}
40```
41
42Parsedown parsing library is imported using Composer. If you installed Shaarli using `git`,
43or the `master` branch, run
44
45 composer update --no-dev --prefer-dist
46
47### No Markdown tag
48
49If the tag `nomarkdown` is set for a shaare, it won't be converted to Markdown syntax.
50
51> Note: this is a special tag, so it won't be displayed in link list.
52
53### HTML escape
54
55By default, HTML tags are escaped. You can enable HTML tags rendering
56by setting `security.markdwon_escape` to `false` in `data/config.json.php`:
57
58```json
59{
60 "security": {
61 "markdown_escape": false
62 }
63}
64```
65
66With this setting, Markdown support HTML tags. For example:
67
68 > <strong>strong</strong><strike>strike</strike>
69
70Will render as:
71
72> <strong>strong</strong><strike>strike</strike>
73
74
75**Warning:**
76
77 * This setting might present **security risks** (XSS) on shared instances, even though tags
78 such as script, iframe, etc should be disabled.
79 * If you want to shaare HTML code, it is necessary to use inline code or code blocks.
80 * If your shaared descriptions contained HTML tags before enabling the markdown plugin,
81enabling it might break your page.
82
83### Known issue
84
85#### Redirector
86
87If you're using a redirector, you *need* to add a space after a link,
88otherwise the rest of the line will be `urlencode`.
89
90```
91[link](http://domain.tld)-->test
92```
93
94Will consider `http://domain.tld)-->test` as URL.
95
96Instead, add an additional space.
97
98```
99[link](http://domain.tld) -->test
100```
101
102> Won't fix because a `)` is a valid part of an URL.
diff --git a/plugins/markdown/help.html b/plugins/markdown/help.html
deleted file mode 100644
index ded3d347..00000000
--- a/plugins/markdown/help.html
+++ /dev/null
@@ -1,5 +0,0 @@
1<div class="md_help">
2 %s
3 <a href="http://daringfireball.net/projects/markdown/syntax" title="%s">
4 %s</a>.
5</div>
diff --git a/plugins/markdown/markdown.meta b/plugins/markdown/markdown.meta
deleted file mode 100644
index 322856ea..00000000
--- a/plugins/markdown/markdown.meta
+++ /dev/null
@@ -1,4 +0,0 @@
1description="Render shaare description with Markdown syntax.<br><strong>Warning</strong>:
2If your shaared descriptions contained HTML tags before enabling the markdown plugin,
3enabling it might break your page.
4See the <a href=\"https://github.com/shaarli/Shaarli/tree/master/plugins/markdown#html-rendering\">README</a>."
diff --git a/plugins/markdown/markdown.php b/plugins/markdown/markdown.php
deleted file mode 100644
index f6f66cc5..00000000
--- a/plugins/markdown/markdown.php
+++ /dev/null
@@ -1,365 +0,0 @@
1<?php
2
3/**
4 * Plugin Markdown.
5 *
6 * Shaare's descriptions are parsed with Markdown.
7 */
8
9use Shaarli\Config\ConfigManager;
10use Shaarli\Plugin\PluginManager;
11use Shaarli\Router;
12
13/*
14 * If this tag is used on a shaare, the description won't be processed by Parsedown.
15 */
16define('NO_MD_TAG', 'nomarkdown');
17
18/**
19 * Parse linklist descriptions.
20 *
21 * @param array $data linklist data.
22 * @param ConfigManager $conf instance.
23 *
24 * @return mixed linklist data parsed in markdown (and converted to HTML).
25 */
26function hook_markdown_render_linklist($data, $conf)
27{
28 foreach ($data['links'] as &$value) {
29 if (!empty($value['tags']) && noMarkdownTag($value['tags'])) {
30 $value = stripNoMarkdownTag($value);
31 continue;
32 }
33 $value['description_src'] = $value['description'];
34 $value['description'] = process_markdown(
35 $value['description'],
36 $conf->get('security.markdown_escape', true),
37 $conf->get('security.allowed_protocols')
38 );
39 }
40 return $data;
41}
42
43/**
44 * Parse feed linklist descriptions.
45 *
46 * @param array $data linklist data.
47 * @param ConfigManager $conf instance.
48 *
49 * @return mixed linklist data parsed in markdown (and converted to HTML).
50 */
51function hook_markdown_render_feed($data, $conf)
52{
53 foreach ($data['links'] as &$value) {
54 if (!empty($value['tags']) && noMarkdownTag($value['tags'])) {
55 $value = stripNoMarkdownTag($value);
56 continue;
57 }
58 $value['description'] = reverse_feed_permalink($value['description']);
59 $value['description'] = process_markdown(
60 $value['description'],
61 $conf->get('security.markdown_escape', true),
62 $conf->get('security.allowed_protocols')
63 );
64 }
65
66 return $data;
67}
68
69/**
70 * Parse daily descriptions.
71 *
72 * @param array $data daily data.
73 * @param ConfigManager $conf instance.
74 *
75 * @return mixed daily data parsed in markdown (and converted to HTML).
76 */
77function hook_markdown_render_daily($data, $conf)
78{
79 //var_dump($data);die;
80 // Manipulate columns data
81 foreach ($data['linksToDisplay'] as &$value) {
82 if (!empty($value['tags']) && noMarkdownTag($value['tags'])) {
83 $value = stripNoMarkdownTag($value);
84 continue;
85 }
86 $value['formatedDescription'] = process_markdown(
87 $value['formatedDescription'],
88 $conf->get('security.markdown_escape', true),
89 $conf->get('security.allowed_protocols')
90 );
91 }
92
93 return $data;
94}
95
96/**
97 * Check if noMarkdown is set in tags.
98 *
99 * @param string $tags tag list
100 *
101 * @return bool true if markdown should be disabled on this link.
102 */
103function noMarkdownTag($tags)
104{
105 return preg_match('/(^|\s)'. NO_MD_TAG .'(\s|$)/', $tags);
106}
107
108/**
109 * Remove the no-markdown meta tag so it won't be displayed.
110 *
111 * @param array $link Link data.
112 *
113 * @return array Updated link without no markdown tag.
114 */
115function stripNoMarkdownTag($link)
116{
117 if (! empty($link['taglist'])) {
118 $offset = array_search(NO_MD_TAG, $link['taglist']);
119 if ($offset !== false) {
120 unset($link['taglist'][$offset]);
121 }
122 }
123
124 if (!empty($link['tags'])) {
125 str_replace(NO_MD_TAG, '', $link['tags']);
126 }
127
128 return $link;
129}
130
131/**
132 * When link list is displayed, include markdown CSS.
133 *
134 * @param array $data includes data.
135 *
136 * @return mixed - includes data with markdown CSS file added.
137 */
138function hook_markdown_render_includes($data)
139{
140 if ($data['_PAGE_'] == Router::$PAGE_LINKLIST
141 || $data['_PAGE_'] == Router::$PAGE_DAILY
142 || $data['_PAGE_'] == Router::$PAGE_EDITLINK
143 ) {
144 $data['css_files'][] = PluginManager::$PLUGINS_PATH . '/markdown/markdown.css';
145 }
146
147 return $data;
148}
149
150/**
151 * Hook render_editlink.
152 * Adds an help link to markdown syntax.
153 *
154 * @param array $data data passed to plugin
155 *
156 * @return array altered $data.
157 */
158function hook_markdown_render_editlink($data)
159{
160 // Load help HTML into a string
161 $txt = file_get_contents(PluginManager::$PLUGINS_PATH .'/markdown/help.html');
162 $translations = [
163 t('Description will be rendered with'),
164 t('Markdown syntax documentation'),
165 t('Markdown syntax'),
166 ];
167 $data['edit_link_plugin'][] = vsprintf($txt, $translations);
168 // Add no markdown 'meta-tag' in tag list if it was never used, for autocompletion.
169 if (! in_array(NO_MD_TAG, $data['tags'])) {
170 $data['tags'][NO_MD_TAG] = 0;
171 }
172
173 return $data;
174}
175
176
177/**
178 * Remove HTML links auto generated by Shaarli core system.
179 * Keeps HREF attributes.
180 *
181 * @param string $description input description text.
182 *
183 * @return string $description without HTML links.
184 */
185function reverse_text2clickable($description)
186{
187 $descriptionLines = explode(PHP_EOL, $description);
188 $descriptionOut = '';
189 $codeBlockOn = false;
190 $lineCount = 0;
191
192 foreach ($descriptionLines as $descriptionLine) {
193 // Detect line of code: starting with 4 spaces,
194 // except lists which can start with +/*/- or `2.` after spaces.
195 $codeLineOn = preg_match('/^ +(?=[^\+\*\-])(?=(?!\d\.).)/', $descriptionLine) > 0;
196 // Detect and toggle block of code
197 if (!$codeBlockOn) {
198 $codeBlockOn = preg_match('/^```/', $descriptionLine) > 0;
199 } elseif (preg_match('/^```/', $descriptionLine) > 0) {
200 $codeBlockOn = false;
201 }
202
203 $hashtagTitle = ' title="Hashtag [^"]+"';
204 // Reverse `inline code` hashtags.
205 $descriptionLine = preg_replace(
206 '!(`[^`\n]*)<a href="[^ ]*"'. $hashtagTitle .'>([^<]+)</a>([^`\n]*`)!m',
207 '$1$2$3',
208 $descriptionLine
209 );
210
211 // Reverse all links in code blocks, only non hashtag elsewhere.
212 $hashtagFilter = (!$codeBlockOn && !$codeLineOn) ? '(?!'. $hashtagTitle .')': '(?:'. $hashtagTitle .')?';
213 $descriptionLine = preg_replace(
214 '#<a href="[^ ]*"'. $hashtagFilter .'>([^<]+)</a>#m',
215 '$1',
216 $descriptionLine
217 );
218
219 // Make hashtag links markdown ready, otherwise the links will be ignored with escape set to true
220 if (!$codeBlockOn && !$codeLineOn) {
221 $descriptionLine = preg_replace(
222 '#<a href="([^ ]*)"'. $hashtagTitle .'>([^<]+)</a>#m',
223 '[$2]($1)',
224 $descriptionLine
225 );
226 }
227
228 $descriptionOut .= $descriptionLine;
229 if ($lineCount++ < count($descriptionLines) - 1) {
230 $descriptionOut .= PHP_EOL;
231 }
232 }
233 return $descriptionOut;
234}
235
236/**
237 * Remove <br> tag to let markdown handle it.
238 *
239 * @param string $description input description text.
240 *
241 * @return string $description without <br> tags.
242 */
243function reverse_nl2br($description)
244{
245 return preg_replace('!<br */?>!im', '', $description);
246}
247
248/**
249 * Remove HTML spaces '&nbsp;' auto generated by Shaarli core system.
250 *
251 * @param string $description input description text.
252 *
253 * @return string $description without HTML links.
254 */
255function reverse_space2nbsp($description)
256{
257 return preg_replace('/(^| )&nbsp;/m', '$1 ', $description);
258}
259
260function reverse_feed_permalink($description)
261{
262 return preg_replace('@&#8212; <a href="([^"]+)" title="[^"]+">([^<]+)</a>$@im', '&#8212; [$2]($1)', $description);
263}
264
265/**
266 * Replace not whitelisted protocols with http:// in given description.
267 *
268 * @param string $description input description text.
269 * @param array $allowedProtocols list of allowed protocols.
270 *
271 * @return string $description without malicious link.
272 */
273function filter_protocols($description, $allowedProtocols)
274{
275 return preg_replace_callback(
276 '#]\((.*?)\)#is',
277 function ($match) use ($allowedProtocols) {
278 return ']('. whitelist_protocols($match[1], $allowedProtocols) .')';
279 },
280 $description
281 );
282}
283
284/**
285 * Remove dangerous HTML tags (tags, iframe, etc.).
286 * Doesn't affect <code> content (already escaped by Parsedown).
287 *
288 * @param string $description input description text.
289 *
290 * @return string given string escaped.
291 */
292function sanitize_html($description)
293{
294 $escapeTags = array(
295 'script',
296 'style',
297 'link',
298 'iframe',
299 'frameset',
300 'frame',
301 );
302 foreach ($escapeTags as $tag) {
303 $description = preg_replace_callback(
304 '#<\s*'. $tag .'[^>]*>(.*</\s*'. $tag .'[^>]*>)?#is',
305 function ($match) {
306 return escape($match[0]);
307 },
308 $description
309 );
310 }
311 $description = preg_replace(
312 '#(<[^>]+\s)on[a-z]*="?[^ "]*"?#is',
313 '$1',
314 $description
315 );
316 return $description;
317}
318
319/**
320 * Render shaare contents through Markdown parser.
321 * 1. Remove HTML generated by Shaarli core.
322 * 2. Reverse the escape function.
323 * 3. Generate markdown descriptions.
324 * 4. Sanitize sensible HTML tags for security.
325 * 5. Wrap description in 'markdown' CSS class.
326 *
327 * @param string $description input description text.
328 * @param bool $escape escape HTML entities
329 *
330 * @return string HTML processed $description.
331 */
332function process_markdown($description, $escape = true, $allowedProtocols = [])
333{
334 $parsedown = new Parsedown();
335
336 $processedDescription = $description;
337 $processedDescription = reverse_nl2br($processedDescription);
338 $processedDescription = reverse_space2nbsp($processedDescription);
339 $processedDescription = reverse_text2clickable($processedDescription);
340 $processedDescription = filter_protocols($processedDescription, $allowedProtocols);
341 $processedDescription = unescape($processedDescription);
342 $processedDescription = $parsedown
343 ->setMarkupEscaped($escape)
344 ->setBreaksEnabled(true)
345 ->text($processedDescription);
346 $processedDescription = sanitize_html($processedDescription);
347
348 if (!empty($processedDescription)) {
349 $processedDescription = '<div class="markdown">'. $processedDescription . '</div>';
350 }
351
352 return $processedDescription;
353}
354
355/**
356 * This function is never called, but contains translation calls for GNU gettext extraction.
357 */
358function markdown_dummy_translation()
359{
360 // meta
361 t('Render shaare description with Markdown syntax.<br><strong>Warning</strong>:
362If your shaared descriptions contained HTML tags before enabling the markdown plugin,
363enabling it might break your page.
364See the <a href="https://github.com/shaarli/Shaarli/tree/master/plugins/markdown#html-rendering">README</a>.');
365}
diff --git a/tests/HistoryTest.php b/tests/HistoryTest.php
index 8303e53a..7189c3a9 100644
--- a/tests/HistoryTest.php
+++ b/tests/HistoryTest.php
@@ -4,6 +4,7 @@ namespace Shaarli;
4 4
5use DateTime; 5use DateTime;
6use Exception; 6use Exception;
7use Shaarli\Bookmark\Bookmark;
7 8
8class HistoryTest extends \PHPUnit\Framework\TestCase 9class HistoryTest extends \PHPUnit\Framework\TestCase
9{ 10{
@@ -15,9 +16,11 @@ class HistoryTest extends \PHPUnit\Framework\TestCase
15 /** 16 /**
16 * Delete history file. 17 * Delete history file.
17 */ 18 */
18 public function tearDown() 19 public function setUp()
19 { 20 {
20 @unlink(self::$historyFilePath); 21 if (file_exists(self::$historyFilePath)) {
22 unlink(self::$historyFilePath);
23 }
21 } 24 }
22 25
23 /** 26 /**
@@ -73,137 +76,140 @@ class HistoryTest extends \PHPUnit\Framework\TestCase
73 public function testAddLink() 76 public function testAddLink()
74 { 77 {
75 $history = new History(self::$historyFilePath); 78 $history = new History(self::$historyFilePath);
76 $history->addLink(['id' => 0]); 79 $bookmark = (new Bookmark())->setId(0);
80 $history->addLink($bookmark);
77 $actual = $history->getHistory()[0]; 81 $actual = $history->getHistory()[0];
78 $this->assertEquals(History::CREATED, $actual['event']); 82 $this->assertEquals(History::CREATED, $actual['event']);
79 $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']); 83 $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']);
80 $this->assertEquals(0, $actual['id']); 84 $this->assertEquals(0, $actual['id']);
81 85
82 $history = new History(self::$historyFilePath); 86 $history = new History(self::$historyFilePath);
83 $history->addLink(['id' => 1]); 87 $bookmark = (new Bookmark())->setId(1);
88 $history->addLink($bookmark);
84 $actual = $history->getHistory()[0]; 89 $actual = $history->getHistory()[0];
85 $this->assertEquals(History::CREATED, $actual['event']); 90 $this->assertEquals(History::CREATED, $actual['event']);
86 $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']); 91 $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']);
87 $this->assertEquals(1, $actual['id']); 92 $this->assertEquals(1, $actual['id']);
88 93
89 $history = new History(self::$historyFilePath); 94 $history = new History(self::$historyFilePath);
90 $history->addLink(['id' => 'str']); 95 $bookmark = (new Bookmark())->setId('str');
96 $history->addLink($bookmark);
91 $actual = $history->getHistory()[0]; 97 $actual = $history->getHistory()[0];
92 $this->assertEquals(History::CREATED, $actual['event']); 98 $this->assertEquals(History::CREATED, $actual['event']);
93 $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']); 99 $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']);
94 $this->assertEquals('str', $actual['id']); 100 $this->assertEquals('str', $actual['id']);
95 } 101 }
96 102
97 /** 103// /**
98 * Test updated link event 104// * Test updated link event
99 */ 105// */
100 public function testUpdateLink() 106// public function testUpdateLink()
101 { 107// {
102 $history = new History(self::$historyFilePath); 108// $history = new History(self::$historyFilePath);
103 $history->updateLink(['id' => 1]); 109// $history->updateLink(['id' => 1]);
104 $actual = $history->getHistory()[0]; 110// $actual = $history->getHistory()[0];
105 $this->assertEquals(History::UPDATED, $actual['event']); 111// $this->assertEquals(History::UPDATED, $actual['event']);
106 $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']); 112// $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']);
107 $this->assertEquals(1, $actual['id']); 113// $this->assertEquals(1, $actual['id']);
108 } 114// }
109 115//
110 /** 116// /**
111 * Test delete link event 117// * Test delete link event
112 */ 118// */
113 public function testDeleteLink() 119// public function testDeleteLink()
114 { 120// {
115 $history = new History(self::$historyFilePath); 121// $history = new History(self::$historyFilePath);
116 $history->deleteLink(['id' => 1]); 122// $history->deleteLink(['id' => 1]);
117 $actual = $history->getHistory()[0]; 123// $actual = $history->getHistory()[0];
118 $this->assertEquals(History::DELETED, $actual['event']); 124// $this->assertEquals(History::DELETED, $actual['event']);
119 $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']); 125// $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']);
120 $this->assertEquals(1, $actual['id']); 126// $this->assertEquals(1, $actual['id']);
121 } 127// }
122 128//
123 /** 129// /**
124 * Test updated settings event 130// * Test updated settings event
125 */ 131// */
126 public function testUpdateSettings() 132// public function testUpdateSettings()
127 { 133// {
128 $history = new History(self::$historyFilePath); 134// $history = new History(self::$historyFilePath);
129 $history->updateSettings(); 135// $history->updateSettings();
130 $actual = $history->getHistory()[0]; 136// $actual = $history->getHistory()[0];
131 $this->assertEquals(History::SETTINGS, $actual['event']); 137// $this->assertEquals(History::SETTINGS, $actual['event']);
132 $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']); 138// $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']);
133 $this->assertEmpty($actual['id']); 139// $this->assertEmpty($actual['id']);
134 } 140// }
135 141//
136 /** 142// /**
137 * Make sure that new items are stored at the beginning 143// * Make sure that new items are stored at the beginning
138 */ 144// */
139 public function testHistoryOrder() 145// public function testHistoryOrder()
140 { 146// {
141 $history = new History(self::$historyFilePath); 147// $history = new History(self::$historyFilePath);
142 $history->updateLink(['id' => 1]); 148// $history->updateLink(['id' => 1]);
143 $actual = $history->getHistory()[0]; 149// $actual = $history->getHistory()[0];
144 $this->assertEquals(History::UPDATED, $actual['event']); 150// $this->assertEquals(History::UPDATED, $actual['event']);
145 $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']); 151// $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']);
146 $this->assertEquals(1, $actual['id']); 152// $this->assertEquals(1, $actual['id']);
147 153//
148 $history->addLink(['id' => 1]); 154// $history->addLink(['id' => 1]);
149 $actual = $history->getHistory()[0]; 155// $actual = $history->getHistory()[0];
150 $this->assertEquals(History::CREATED, $actual['event']); 156// $this->assertEquals(History::CREATED, $actual['event']);
151 $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']); 157// $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']);
152 $this->assertEquals(1, $actual['id']); 158// $this->assertEquals(1, $actual['id']);
153 } 159// }
154 160//
155 /** 161// /**
156 * Re-read history from file after writing an event 162// * Re-read history from file after writing an event
157 */ 163// */
158 public function testHistoryRead() 164// public function testHistoryRead()
159 { 165// {
160 $history = new History(self::$historyFilePath); 166// $history = new History(self::$historyFilePath);
161 $history->updateLink(['id' => 1]); 167// $history->updateLink(['id' => 1]);
162 $history = new History(self::$historyFilePath); 168// $history = new History(self::$historyFilePath);
163 $actual = $history->getHistory()[0]; 169// $actual = $history->getHistory()[0];
164 $this->assertEquals(History::UPDATED, $actual['event']); 170// $this->assertEquals(History::UPDATED, $actual['event']);
165 $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']); 171// $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']);
166 $this->assertEquals(1, $actual['id']); 172// $this->assertEquals(1, $actual['id']);
167 } 173// }
168 174//
169 /** 175// /**
170 * Re-read history from file after writing an event and make sure that the order is correct 176// * Re-read history from file after writing an event and make sure that the order is correct
171 */ 177// */
172 public function testHistoryOrderRead() 178// public function testHistoryOrderRead()
173 { 179// {
174 $history = new History(self::$historyFilePath); 180// $history = new History(self::$historyFilePath);
175 $history->updateLink(['id' => 1]); 181// $history->updateLink(['id' => 1]);
176 $history->addLink(['id' => 1]); 182// $history->addLink(['id' => 1]);
177 183//
178 $history = new History(self::$historyFilePath); 184// $history = new History(self::$historyFilePath);
179 $actual = $history->getHistory()[0]; 185// $actual = $history->getHistory()[0];
180 $this->assertEquals(History::CREATED, $actual['event']); 186// $this->assertEquals(History::CREATED, $actual['event']);
181 $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']); 187// $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']);
182 $this->assertEquals(1, $actual['id']); 188// $this->assertEquals(1, $actual['id']);
183 189//
184 $actual = $history->getHistory()[1]; 190// $actual = $history->getHistory()[1];
185 $this->assertEquals(History::UPDATED, $actual['event']); 191// $this->assertEquals(History::UPDATED, $actual['event']);
186 $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']); 192// $this->assertTrue(new DateTime('-2 seconds') < $actual['datetime']);
187 $this->assertEquals(1, $actual['id']); 193// $this->assertEquals(1, $actual['id']);
188 } 194// }
189 195//
190 /** 196// /**
191 * Test retention time: delete old entries. 197// * Test retention time: delete old entries.
192 */ 198// */
193 public function testHistoryRententionTime() 199// public function testHistoryRententionTime()
194 { 200// {
195 $history = new History(self::$historyFilePath, 5); 201// $history = new History(self::$historyFilePath, 5);
196 $history->updateLink(['id' => 1]); 202// $history->updateLink(['id' => 1]);
197 $this->assertEquals(1, count($history->getHistory())); 203// $this->assertEquals(1, count($history->getHistory()));
198 $arr = $history->getHistory(); 204// $arr = $history->getHistory();
199 $arr[0]['datetime'] = new DateTime('-1 hour'); 205// $arr[0]['datetime'] = new DateTime('-1 hour');
200 FileUtils::writeFlatDB(self::$historyFilePath, $arr); 206// FileUtils::writeFlatDB(self::$historyFilePath, $arr);
201 207//
202 $history = new History(self::$historyFilePath, 60); 208// $history = new History(self::$historyFilePath, 60);
203 $this->assertEquals(1, count($history->getHistory())); 209// $this->assertEquals(1, count($history->getHistory()));
204 $this->assertEquals(1, $history->getHistory()[0]['id']); 210// $this->assertEquals(1, $history->getHistory()[0]['id']);
205 $history->updateLink(['id' => 2]); 211// $history->updateLink(['id' => 2]);
206 $this->assertEquals(1, count($history->getHistory())); 212// $this->assertEquals(1, count($history->getHistory()));
207 $this->assertEquals(2, $history->getHistory()[0]['id']); 213// $this->assertEquals(2, $history->getHistory()[0]['id']);
208 } 214// }
209} 215}
diff --git a/tests/api/ApiMiddlewareTest.php b/tests/api/ApiMiddlewareTest.php
index 0b9b03f2..df2fb33a 100644
--- a/tests/api/ApiMiddlewareTest.php
+++ b/tests/api/ApiMiddlewareTest.php
@@ -2,6 +2,7 @@
2namespace Shaarli\Api; 2namespace Shaarli\Api;
3 3
4use Shaarli\Config\ConfigManager; 4use Shaarli\Config\ConfigManager;
5use Shaarli\History;
5use Slim\Container; 6use Slim\Container;
6use Slim\Http\Environment; 7use Slim\Http\Environment;
7use Slim\Http\Request; 8use Slim\Http\Request;
@@ -40,18 +41,21 @@ class ApiMiddlewareTest extends \PHPUnit\Framework\TestCase
40 protected $container; 41 protected $container;
41 42
42 /** 43 /**
43 * Before every test, instantiate a new Api with its config, plugins and links. 44 * Before every test, instantiate a new Api with its config, plugins and bookmarks.
44 */ 45 */
45 public function setUp() 46 public function setUp()
46 { 47 {
47 $this->conf = new ConfigManager('tests/utils/config/configJson.json.php'); 48 $this->conf = new ConfigManager('tests/utils/config/configJson');
48 $this->conf->set('api.secret', 'NapoleonWasALizard'); 49 $this->conf->set('api.secret', 'NapoleonWasALizard');
49 50
50 $this->refDB = new \ReferenceLinkDB(); 51 $this->refDB = new \ReferenceLinkDB();
51 $this->refDB->write(self::$testDatastore); 52 $this->refDB->write(self::$testDatastore);
52 53
54 $history = new History('sandbox/history.php');
55
53 $this->container = new Container(); 56 $this->container = new Container();
54 $this->container['conf'] = $this->conf; 57 $this->container['conf'] = $this->conf;
58 $this->container['history'] = $history;
55 } 59 }
56 60
57 /** 61 /**
diff --git a/tests/api/ApiUtilsTest.php b/tests/api/ApiUtilsTest.php
index 7499dd71..7efec9bb 100644
--- a/tests/api/ApiUtilsTest.php
+++ b/tests/api/ApiUtilsTest.php
@@ -2,6 +2,7 @@
2 2
3namespace Shaarli\Api; 3namespace Shaarli\Api;
4 4
5use Shaarli\Bookmark\Bookmark;
5use Shaarli\Http\Base64Url; 6use Shaarli\Http\Base64Url;
6 7
7/** 8/**
@@ -212,7 +213,7 @@ class ApiUtilsTest extends \PHPUnit\Framework\TestCase
212 public function testFormatLinkComplete() 213 public function testFormatLinkComplete()
213 { 214 {
214 $indexUrl = 'https://domain.tld/sub/'; 215 $indexUrl = 'https://domain.tld/sub/';
215 $link = [ 216 $data = [
216 'id' => 12, 217 'id' => 12,
217 'url' => 'http://lol.lol', 218 'url' => 'http://lol.lol',
218 'shorturl' => 'abc', 219 'shorturl' => 'abc',
@@ -223,6 +224,8 @@ class ApiUtilsTest extends \PHPUnit\Framework\TestCase
223 'created' => \DateTime::createFromFormat('Ymd_His', '20170107_160102'), 224 'created' => \DateTime::createFromFormat('Ymd_His', '20170107_160102'),
224 'updated' => \DateTime::createFromFormat('Ymd_His', '20170107_160612'), 225 'updated' => \DateTime::createFromFormat('Ymd_His', '20170107_160612'),
225 ]; 226 ];
227 $bookmark = new Bookmark();
228 $bookmark->fromArray($data);
226 229
227 $expected = [ 230 $expected = [
228 'id' => 12, 231 'id' => 12,
@@ -236,7 +239,7 @@ class ApiUtilsTest extends \PHPUnit\Framework\TestCase
236 'updated' => '2017-01-07T16:06:12+00:00', 239 'updated' => '2017-01-07T16:06:12+00:00',
237 ]; 240 ];
238 241
239 $this->assertEquals($expected, ApiUtils::formatLink($link, $indexUrl)); 242 $this->assertEquals($expected, ApiUtils::formatLink($bookmark, $indexUrl));
240 } 243 }
241 244
242 /** 245 /**
@@ -245,7 +248,7 @@ class ApiUtilsTest extends \PHPUnit\Framework\TestCase
245 public function testFormatLinkMinimalNote() 248 public function testFormatLinkMinimalNote()
246 { 249 {
247 $indexUrl = 'https://domain.tld/sub/'; 250 $indexUrl = 'https://domain.tld/sub/';
248 $link = [ 251 $data = [
249 'id' => 12, 252 'id' => 12,
250 'url' => '?abc', 253 'url' => '?abc',
251 'shorturl' => 'abc', 254 'shorturl' => 'abc',
@@ -255,6 +258,8 @@ class ApiUtilsTest extends \PHPUnit\Framework\TestCase
255 'private' => '', 258 'private' => '',
256 'created' => \DateTime::createFromFormat('Ymd_His', '20170107_160102'), 259 'created' => \DateTime::createFromFormat('Ymd_His', '20170107_160102'),
257 ]; 260 ];
261 $bookmark = new Bookmark();
262 $bookmark->fromArray($data);
258 263
259 $expected = [ 264 $expected = [
260 'id' => 12, 265 'id' => 12,
@@ -268,7 +273,7 @@ class ApiUtilsTest extends \PHPUnit\Framework\TestCase
268 'updated' => '', 273 'updated' => '',
269 ]; 274 ];
270 275
271 $this->assertEquals($expected, ApiUtils::formatLink($link, $indexUrl)); 276 $this->assertEquals($expected, ApiUtils::formatLink($bookmark, $indexUrl));
272 } 277 }
273 278
274 /** 279 /**
@@ -277,7 +282,7 @@ class ApiUtilsTest extends \PHPUnit\Framework\TestCase
277 public function testUpdateLink() 282 public function testUpdateLink()
278 { 283 {
279 $created = \DateTime::createFromFormat('Ymd_His', '20170107_160102'); 284 $created = \DateTime::createFromFormat('Ymd_His', '20170107_160102');
280 $old = [ 285 $data = [
281 'id' => 12, 286 'id' => 12,
282 'url' => '?abc', 287 'url' => '?abc',
283 'shorturl' => 'abc', 288 'shorturl' => 'abc',
@@ -287,8 +292,10 @@ class ApiUtilsTest extends \PHPUnit\Framework\TestCase
287 'private' => '', 292 'private' => '',
288 'created' => $created, 293 'created' => $created,
289 ]; 294 ];
295 $old = new Bookmark();
296 $old->fromArray($data);
290 297
291 $new = [ 298 $data = [
292 'id' => 13, 299 'id' => 13,
293 'shorturl' => 'nope', 300 'shorturl' => 'nope',
294 'url' => 'http://somewhere.else', 301 'url' => 'http://somewhere.else',
@@ -299,17 +306,18 @@ class ApiUtilsTest extends \PHPUnit\Framework\TestCase
299 'created' => 'creation', 306 'created' => 'creation',
300 'updated' => 'updation', 307 'updated' => 'updation',
301 ]; 308 ];
309 $new = new Bookmark();
310 $new->fromArray($data);
302 311
303 $result = ApiUtils::updateLink($old, $new); 312 $result = ApiUtils::updateLink($old, $new);
304 $this->assertEquals(12, $result['id']); 313 $this->assertEquals(12, $result->getId());
305 $this->assertEquals('http://somewhere.else', $result['url']); 314 $this->assertEquals('http://somewhere.else', $result->getUrl());
306 $this->assertEquals('abc', $result['shorturl']); 315 $this->assertEquals('abc', $result->getShortUrl());
307 $this->assertEquals('Le Cid', $result['title']); 316 $this->assertEquals('Le Cid', $result->getTitle());
308 $this->assertEquals('PercĆ© jusques au fond du cœur [...]', $result['description']); 317 $this->assertEquals('PercĆ© jusques au fond du cœur [...]', $result->getDescription());
309 $this->assertEquals('corneille rodrigue', $result['tags']); 318 $this->assertEquals('corneille rodrigue', $result->getTagsString());
310 $this->assertEquals(true, $result['private']); 319 $this->assertEquals(true, $result->isPrivate());
311 $this->assertEquals($created, $result['created']); 320 $this->assertEquals($created, $result->getCreated());
312 $this->assertTrue(new \DateTime('5 seconds ago') < $result['updated']);
313 } 321 }
314 322
315 /** 323 /**
@@ -318,7 +326,7 @@ class ApiUtilsTest extends \PHPUnit\Framework\TestCase
318 public function testUpdateLinkMinimal() 326 public function testUpdateLinkMinimal()
319 { 327 {
320 $created = \DateTime::createFromFormat('Ymd_His', '20170107_160102'); 328 $created = \DateTime::createFromFormat('Ymd_His', '20170107_160102');
321 $old = [ 329 $data = [
322 'id' => 12, 330 'id' => 12,
323 'url' => '?abc', 331 'url' => '?abc',
324 'shorturl' => 'abc', 332 'shorturl' => 'abc',
@@ -328,24 +336,19 @@ class ApiUtilsTest extends \PHPUnit\Framework\TestCase
328 'private' => true, 336 'private' => true,
329 'created' => $created, 337 'created' => $created,
330 ]; 338 ];
339 $old = new Bookmark();
340 $old->fromArray($data);
331 341
332 $new = [ 342 $new = new Bookmark();
333 'url' => '',
334 'title' => '',
335 'description' => '',
336 'tags' => '',
337 'private' => false,
338 ];
339 343
340 $result = ApiUtils::updateLink($old, $new); 344 $result = ApiUtils::updateLink($old, $new);
341 $this->assertEquals(12, $result['id']); 345 $this->assertEquals(12, $result->getId());
342 $this->assertEquals('?abc', $result['url']); 346 $this->assertEquals('', $result->getUrl());
343 $this->assertEquals('abc', $result['shorturl']); 347 $this->assertEquals('abc', $result->getShortUrl());
344 $this->assertEquals('?abc', $result['title']); 348 $this->assertEquals('', $result->getTitle());
345 $this->assertEquals('', $result['description']); 349 $this->assertEquals('', $result->getDescription());
346 $this->assertEquals('', $result['tags']); 350 $this->assertEquals('', $result->getTagsString());
347 $this->assertEquals(false, $result['private']); 351 $this->assertEquals(false, $result->isPrivate());
348 $this->assertEquals($created, $result['created']); 352 $this->assertEquals($created, $result->getCreated());
349 $this->assertTrue(new \DateTime('5 seconds ago') < $result['updated']);
350 } 353 }
351} 354}
diff --git a/tests/api/controllers/history/HistoryTest.php b/tests/api/controllers/history/HistoryTest.php
index e287f239..f4d3b646 100644
--- a/tests/api/controllers/history/HistoryTest.php
+++ b/tests/api/controllers/history/HistoryTest.php
@@ -39,11 +39,11 @@ class HistoryTest extends \PHPUnit\Framework\TestCase
39 protected $controller; 39 protected $controller;
40 40
41 /** 41 /**
42 * Before every test, instantiate a new Api with its config, plugins and links. 42 * Before every test, instantiate a new Api with its config, plugins and bookmarks.
43 */ 43 */
44 public function setUp() 44 public function setUp()
45 { 45 {
46 $this->conf = new ConfigManager('tests/utils/config/configJson.json.php'); 46 $this->conf = new ConfigManager('tests/utils/config/configJson');
47 $this->refHistory = new \ReferenceHistory(); 47 $this->refHistory = new \ReferenceHistory();
48 $this->refHistory->write(self::$testHistory); 48 $this->refHistory->write(self::$testHistory);
49 $this->container = new Container(); 49 $this->container = new Container();
diff --git a/tests/api/controllers/info/InfoTest.php b/tests/api/controllers/info/InfoTest.php
index e70d371b..b5c938e1 100644
--- a/tests/api/controllers/info/InfoTest.php
+++ b/tests/api/controllers/info/InfoTest.php
@@ -1,7 +1,10 @@
1<?php 1<?php
2namespace Shaarli\Api\Controllers; 2namespace Shaarli\Api\Controllers;
3 3
4use PHPUnit\Framework\TestCase;
5use Shaarli\Bookmark\BookmarkFileService;
4use Shaarli\Config\ConfigManager; 6use Shaarli\Config\ConfigManager;
7use Shaarli\History;
5use Slim\Container; 8use Slim\Container;
6use Slim\Http\Environment; 9use Slim\Http\Environment;
7use Slim\Http\Request; 10use Slim\Http\Request;
@@ -14,7 +17,7 @@ use Slim\Http\Response;
14 * 17 *
15 * @package Api\Controllers 18 * @package Api\Controllers
16 */ 19 */
17class InfoTest extends \PHPUnit\Framework\TestCase 20class InfoTest extends TestCase
18{ 21{
19 /** 22 /**
20 * @var string datastore to test write operations 23 * @var string datastore to test write operations
@@ -42,17 +45,20 @@ class InfoTest extends \PHPUnit\Framework\TestCase
42 protected $controller; 45 protected $controller;
43 46
44 /** 47 /**
45 * Before every test, instantiate a new Api with its config, plugins and links. 48 * Before every test, instantiate a new Api with its config, plugins and bookmarks.
46 */ 49 */
47 public function setUp() 50 public function setUp()
48 { 51 {
49 $this->conf = new ConfigManager('tests/utils/config/configJson.json.php'); 52 $this->conf = new ConfigManager('tests/utils/config/configJson');
53 $this->conf->set('resource.datastore', self::$testDatastore);
50 $this->refDB = new \ReferenceLinkDB(); 54 $this->refDB = new \ReferenceLinkDB();
51 $this->refDB->write(self::$testDatastore); 55 $this->refDB->write(self::$testDatastore);
52 56
57 $history = new History('sandbox/history.php');
58
53 $this->container = new Container(); 59 $this->container = new Container();
54 $this->container['conf'] = $this->conf; 60 $this->container['conf'] = $this->conf;
55 $this->container['db'] = new \Shaarli\Bookmark\LinkDB(self::$testDatastore, true, false); 61 $this->container['db'] = new BookmarkFileService($this->conf, $history, true);
56 $this->container['history'] = null; 62 $this->container['history'] = null;
57 63
58 $this->controller = new Info($this->container); 64 $this->controller = new Info($this->container);
@@ -84,11 +90,11 @@ class InfoTest extends \PHPUnit\Framework\TestCase
84 $this->assertEquals(2, $data['private_counter']); 90 $this->assertEquals(2, $data['private_counter']);
85 $this->assertEquals('Shaarli', $data['settings']['title']); 91 $this->assertEquals('Shaarli', $data['settings']['title']);
86 $this->assertEquals('?', $data['settings']['header_link']); 92 $this->assertEquals('?', $data['settings']['header_link']);
87 $this->assertEquals('UTC', $data['settings']['timezone']); 93 $this->assertEquals('Europe/Paris', $data['settings']['timezone']);
88 $this->assertEquals(ConfigManager::$DEFAULT_PLUGINS, $data['settings']['enabled_plugins']); 94 $this->assertEquals(ConfigManager::$DEFAULT_PLUGINS, $data['settings']['enabled_plugins']);
89 $this->assertEquals(false, $data['settings']['default_private_links']); 95 $this->assertEquals(true, $data['settings']['default_private_links']);
90 96
91 $title = 'My links'; 97 $title = 'My bookmarks';
92 $headerLink = 'http://shaarli.tld'; 98 $headerLink = 'http://shaarli.tld';
93 $timezone = 'Europe/Paris'; 99 $timezone = 'Europe/Paris';
94 $enabledPlugins = array('foo', 'bar'); 100 $enabledPlugins = array('foo', 'bar');
diff --git a/tests/api/controllers/links/DeleteLinkTest.php b/tests/api/controllers/links/DeleteLinkTest.php
index 90193e28..6c2b3698 100644
--- a/tests/api/controllers/links/DeleteLinkTest.php
+++ b/tests/api/controllers/links/DeleteLinkTest.php
@@ -3,7 +3,7 @@
3 3
4namespace Shaarli\Api\Controllers; 4namespace Shaarli\Api\Controllers;
5 5
6use Shaarli\Bookmark\LinkDB; 6use Shaarli\Bookmark\BookmarkFileService;
7use Shaarli\Config\ConfigManager; 7use Shaarli\Config\ConfigManager;
8use Shaarli\History; 8use Shaarli\History;
9use Slim\Container; 9use Slim\Container;
@@ -34,9 +34,9 @@ class DeleteLinkTest extends \PHPUnit\Framework\TestCase
34 protected $refDB = null; 34 protected $refDB = null;
35 35
36 /** 36 /**
37 * @var LinkDB instance. 37 * @var BookmarkFileService instance.
38 */ 38 */
39 protected $linkDB; 39 protected $bookmarkService;
40 40
41 /** 41 /**
42 * @var HistoryController instance. 42 * @var HistoryController instance.
@@ -54,20 +54,22 @@ class DeleteLinkTest extends \PHPUnit\Framework\TestCase
54 protected $controller; 54 protected $controller;
55 55
56 /** 56 /**
57 * Before each test, instantiate a new Api with its config, plugins and links. 57 * Before each test, instantiate a new Api with its config, plugins and bookmarks.
58 */ 58 */
59 public function setUp() 59 public function setUp()
60 { 60 {
61 $this->conf = new ConfigManager('tests/utils/config/configJson'); 61 $this->conf = new ConfigManager('tests/utils/config/configJson');
62 $this->conf->set('resource.datastore', self::$testDatastore);
62 $this->refDB = new \ReferenceLinkDB(); 63 $this->refDB = new \ReferenceLinkDB();
63 $this->refDB->write(self::$testDatastore); 64 $this->refDB->write(self::$testDatastore);
64 $this->linkDB = new LinkDB(self::$testDatastore, true, false);
65 $refHistory = new \ReferenceHistory(); 65 $refHistory = new \ReferenceHistory();
66 $refHistory->write(self::$testHistory); 66 $refHistory->write(self::$testHistory);
67 $this->history = new History(self::$testHistory); 67 $this->history = new History(self::$testHistory);
68 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true);
69
68 $this->container = new Container(); 70 $this->container = new Container();
69 $this->container['conf'] = $this->conf; 71 $this->container['conf'] = $this->conf;
70 $this->container['db'] = $this->linkDB; 72 $this->container['db'] = $this->bookmarkService;
71 $this->container['history'] = $this->history; 73 $this->container['history'] = $this->history;
72 74
73 $this->controller = new Links($this->container); 75 $this->controller = new Links($this->container);
@@ -88,7 +90,7 @@ class DeleteLinkTest extends \PHPUnit\Framework\TestCase
88 public function testDeleteLinkValid() 90 public function testDeleteLinkValid()
89 { 91 {
90 $id = '41'; 92 $id = '41';
91 $this->assertTrue(isset($this->linkDB[$id])); 93 $this->assertTrue($this->bookmarkService->exists($id));
92 $env = Environment::mock([ 94 $env = Environment::mock([
93 'REQUEST_METHOD' => 'DELETE', 95 'REQUEST_METHOD' => 'DELETE',
94 ]); 96 ]);
@@ -98,8 +100,8 @@ class DeleteLinkTest extends \PHPUnit\Framework\TestCase
98 $this->assertEquals(204, $response->getStatusCode()); 100 $this->assertEquals(204, $response->getStatusCode());
99 $this->assertEmpty((string) $response->getBody()); 101 $this->assertEmpty((string) $response->getBody());
100 102
101 $this->linkDB = new LinkDB(self::$testDatastore, true, false); 103 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true);
102 $this->assertFalse(isset($this->linkDB[$id])); 104 $this->assertFalse($this->bookmarkService->exists($id));
103 105
104 $historyEntry = $this->history->getHistory()[0]; 106 $historyEntry = $this->history->getHistory()[0];
105 $this->assertEquals(History::DELETED, $historyEntry['event']); 107 $this->assertEquals(History::DELETED, $historyEntry['event']);
@@ -117,7 +119,7 @@ class DeleteLinkTest extends \PHPUnit\Framework\TestCase
117 public function testDeleteLink404() 119 public function testDeleteLink404()
118 { 120 {
119 $id = -1; 121 $id = -1;
120 $this->assertFalse(isset($this->linkDB[$id])); 122 $this->assertFalse($this->bookmarkService->exists($id));
121 $env = Environment::mock([ 123 $env = Environment::mock([
122 'REQUEST_METHOD' => 'DELETE', 124 'REQUEST_METHOD' => 'DELETE',
123 ]); 125 ]);
diff --git a/tests/api/controllers/links/GetLinkIdTest.php b/tests/api/controllers/links/GetLinkIdTest.php
index cb9b7f6a..c26411ac 100644
--- a/tests/api/controllers/links/GetLinkIdTest.php
+++ b/tests/api/controllers/links/GetLinkIdTest.php
@@ -2,7 +2,10 @@
2 2
3namespace Shaarli\Api\Controllers; 3namespace Shaarli\Api\Controllers;
4 4
5use Shaarli\Bookmark\Bookmark;
6use Shaarli\Bookmark\BookmarkFileService;
5use Shaarli\Config\ConfigManager; 7use Shaarli\Config\ConfigManager;
8use Shaarli\History;
6use Slim\Container; 9use Slim\Container;
7use Slim\Http\Environment; 10use Slim\Http\Environment;
8use Slim\Http\Request; 11use Slim\Http\Request;
@@ -50,17 +53,19 @@ class GetLinkIdTest extends \PHPUnit\Framework\TestCase
50 const NB_FIELDS_LINK = 9; 53 const NB_FIELDS_LINK = 9;
51 54
52 /** 55 /**
53 * Before each test, instantiate a new Api with its config, plugins and links. 56 * Before each test, instantiate a new Api with its config, plugins and bookmarks.
54 */ 57 */
55 public function setUp() 58 public function setUp()
56 { 59 {
57 $this->conf = new ConfigManager('tests/utils/config/configJson'); 60 $this->conf = new ConfigManager('tests/utils/config/configJson');
61 $this->conf->set('resource.datastore', self::$testDatastore);
58 $this->refDB = new \ReferenceLinkDB(); 62 $this->refDB = new \ReferenceLinkDB();
59 $this->refDB->write(self::$testDatastore); 63 $this->refDB->write(self::$testDatastore);
64 $history = new History('sandbox/history.php');
60 65
61 $this->container = new Container(); 66 $this->container = new Container();
62 $this->container['conf'] = $this->conf; 67 $this->container['conf'] = $this->conf;
63 $this->container['db'] = new \Shaarli\Bookmark\LinkDB(self::$testDatastore, true, false); 68 $this->container['db'] = new BookmarkFileService($this->conf, $history, true);
64 $this->container['history'] = null; 69 $this->container['history'] = null;
65 70
66 $this->controller = new Links($this->container); 71 $this->controller = new Links($this->container);
@@ -107,7 +112,7 @@ class GetLinkIdTest extends \PHPUnit\Framework\TestCase
107 $this->assertEquals('sTuff', $data['tags'][0]); 112 $this->assertEquals('sTuff', $data['tags'][0]);
108 $this->assertEquals(false, $data['private']); 113 $this->assertEquals(false, $data['private']);
109 $this->assertEquals( 114 $this->assertEquals(
110 \DateTime::createFromFormat(\Shaarli\Bookmark\LinkDB::LINK_DATE_FORMAT, '20150310_114651')->format(\DateTime::ATOM), 115 \DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20150310_114651')->format(\DateTime::ATOM),
111 $data['created'] 116 $data['created']
112 ); 117 );
113 $this->assertEmpty($data['updated']); 118 $this->assertEmpty($data['updated']);
diff --git a/tests/api/controllers/links/GetLinksTest.php b/tests/api/controllers/links/GetLinksTest.php
index 711a3152..4e2d55ac 100644
--- a/tests/api/controllers/links/GetLinksTest.php
+++ b/tests/api/controllers/links/GetLinksTest.php
@@ -1,8 +1,11 @@
1<?php 1<?php
2namespace Shaarli\Api\Controllers; 2namespace Shaarli\Api\Controllers;
3 3
4use Shaarli\Bookmark\Bookmark;
5use Shaarli\Bookmark\BookmarkFileService;
4use Shaarli\Bookmark\LinkDB; 6use Shaarli\Bookmark\LinkDB;
5use Shaarli\Config\ConfigManager; 7use Shaarli\Config\ConfigManager;
8use Shaarli\History;
6use Slim\Container; 9use Slim\Container;
7use Slim\Http\Environment; 10use Slim\Http\Environment;
8use Slim\Http\Request; 11use Slim\Http\Request;
@@ -50,17 +53,19 @@ class GetLinksTest extends \PHPUnit\Framework\TestCase
50 const NB_FIELDS_LINK = 9; 53 const NB_FIELDS_LINK = 9;
51 54
52 /** 55 /**
53 * Before every test, instantiate a new Api with its config, plugins and links. 56 * Before every test, instantiate a new Api with its config, plugins and bookmarks.
54 */ 57 */
55 public function setUp() 58 public function setUp()
56 { 59 {
57 $this->conf = new ConfigManager('tests/utils/config/configJson'); 60 $this->conf = new ConfigManager('tests/utils/config/configJson');
61 $this->conf->set('resource.datastore', self::$testDatastore);
58 $this->refDB = new \ReferenceLinkDB(); 62 $this->refDB = new \ReferenceLinkDB();
59 $this->refDB->write(self::$testDatastore); 63 $this->refDB->write(self::$testDatastore);
64 $history = new History('sandbox/history.php');
60 65
61 $this->container = new Container(); 66 $this->container = new Container();
62 $this->container['conf'] = $this->conf; 67 $this->container['conf'] = $this->conf;
63 $this->container['db'] = new LinkDB(self::$testDatastore, true, false); 68 $this->container['db'] = new BookmarkFileService($this->conf, $history, true);
64 $this->container['history'] = null; 69 $this->container['history'] = null;
65 70
66 $this->controller = new Links($this->container); 71 $this->controller = new Links($this->container);
@@ -75,7 +80,7 @@ class GetLinksTest extends \PHPUnit\Framework\TestCase
75 } 80 }
76 81
77 /** 82 /**
78 * Test basic getLinks service: returns all links. 83 * Test basic getLinks service: returns all bookmarks.
79 */ 84 */
80 public function testGetLinks() 85 public function testGetLinks()
81 { 86 {
@@ -114,7 +119,7 @@ class GetLinksTest extends \PHPUnit\Framework\TestCase
114 $this->assertEquals('sTuff', $first['tags'][0]); 119 $this->assertEquals('sTuff', $first['tags'][0]);
115 $this->assertEquals(false, $first['private']); 120 $this->assertEquals(false, $first['private']);
116 $this->assertEquals( 121 $this->assertEquals(
117 \DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20150310_114651')->format(\DateTime::ATOM), 122 \DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20150310_114651')->format(\DateTime::ATOM),
118 $first['created'] 123 $first['created']
119 ); 124 );
120 $this->assertEmpty($first['updated']); 125 $this->assertEmpty($first['updated']);
@@ -125,7 +130,7 @@ class GetLinksTest extends \PHPUnit\Framework\TestCase
125 130
126 // Update date 131 // Update date
127 $this->assertEquals( 132 $this->assertEquals(
128 \DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160803_093033')->format(\DateTime::ATOM), 133 \DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20160803_093033')->format(\DateTime::ATOM),
129 $link['updated'] 134 $link['updated']
130 ); 135 );
131 } 136 }
diff --git a/tests/api/controllers/links/PostLinkTest.php b/tests/api/controllers/links/PostLinkTest.php
index d683a984..b2dd09eb 100644
--- a/tests/api/controllers/links/PostLinkTest.php
+++ b/tests/api/controllers/links/PostLinkTest.php
@@ -3,6 +3,8 @@
3namespace Shaarli\Api\Controllers; 3namespace Shaarli\Api\Controllers;
4 4
5use PHPUnit\Framework\TestCase; 5use PHPUnit\Framework\TestCase;
6use Shaarli\Bookmark\Bookmark;
7use Shaarli\Bookmark\BookmarkFileService;
6use Shaarli\Config\ConfigManager; 8use Shaarli\Config\ConfigManager;
7use Shaarli\History; 9use Shaarli\History;
8use Slim\Container; 10use Slim\Container;
@@ -41,6 +43,11 @@ class PostLinkTest extends TestCase
41 protected $refDB = null; 43 protected $refDB = null;
42 44
43 /** 45 /**
46 * @var BookmarkFileService instance.
47 */
48 protected $bookmarkService;
49
50 /**
44 * @var HistoryController instance. 51 * @var HistoryController instance.
45 */ 52 */
46 protected $history; 53 protected $history;
@@ -61,29 +68,30 @@ class PostLinkTest extends TestCase
61 const NB_FIELDS_LINK = 9; 68 const NB_FIELDS_LINK = 9;
62 69
63 /** 70 /**
64 * Before every test, instantiate a new Api with its config, plugins and links. 71 * Before every test, instantiate a new Api with its config, plugins and bookmarks.
65 */ 72 */
66 public function setUp() 73 public function setUp()
67 { 74 {
68 $this->conf = new ConfigManager('tests/utils/config/configJson.json.php'); 75 $this->conf = new ConfigManager('tests/utils/config/configJson');
76 $this->conf->set('resource.datastore', self::$testDatastore);
69 $this->refDB = new \ReferenceLinkDB(); 77 $this->refDB = new \ReferenceLinkDB();
70 $this->refDB->write(self::$testDatastore); 78 $this->refDB->write(self::$testDatastore);
71
72 $refHistory = new \ReferenceHistory(); 79 $refHistory = new \ReferenceHistory();
73 $refHistory->write(self::$testHistory); 80 $refHistory->write(self::$testHistory);
74 $this->history = new History(self::$testHistory); 81 $this->history = new History(self::$testHistory);
82 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true);
75 83
76 $this->container = new Container(); 84 $this->container = new Container();
77 $this->container['conf'] = $this->conf; 85 $this->container['conf'] = $this->conf;
78 $this->container['db'] = new \Shaarli\Bookmark\LinkDB(self::$testDatastore, true, false); 86 $this->container['db'] = $this->bookmarkService;
79 $this->container['history'] = new History(self::$testHistory); 87 $this->container['history'] = $this->history;
80 88
81 $this->controller = new Links($this->container); 89 $this->controller = new Links($this->container);
82 90
83 $mock = $this->createMock(Router::class); 91 $mock = $this->createMock(Router::class);
84 $mock->expects($this->any()) 92 $mock->expects($this->any())
85 ->method('relativePathFor') 93 ->method('relativePathFor')
86 ->willReturn('api/v1/links/1'); 94 ->willReturn('api/v1/bookmarks/1');
87 95
88 // affect @property-read... seems to work 96 // affect @property-read... seems to work
89 $this->controller->getCi()->router = $mock; 97 $this->controller->getCi()->router = $mock;
@@ -118,7 +126,7 @@ class PostLinkTest extends TestCase
118 126
119 $response = $this->controller->postLink($request, new Response()); 127 $response = $this->controller->postLink($request, new Response());
120 $this->assertEquals(201, $response->getStatusCode()); 128 $this->assertEquals(201, $response->getStatusCode());
121 $this->assertEquals('api/v1/links/1', $response->getHeader('Location')[0]); 129 $this->assertEquals('api/v1/bookmarks/1', $response->getHeader('Location')[0]);
122 $data = json_decode((string) $response->getBody(), true); 130 $data = json_decode((string) $response->getBody(), true);
123 $this->assertEquals(self::NB_FIELDS_LINK, count($data)); 131 $this->assertEquals(self::NB_FIELDS_LINK, count($data));
124 $this->assertEquals(43, $data['id']); 132 $this->assertEquals(43, $data['id']);
@@ -127,7 +135,7 @@ class PostLinkTest extends TestCase
127 $this->assertEquals('?' . $data['shorturl'], $data['title']); 135 $this->assertEquals('?' . $data['shorturl'], $data['title']);
128 $this->assertEquals('', $data['description']); 136 $this->assertEquals('', $data['description']);
129 $this->assertEquals([], $data['tags']); 137 $this->assertEquals([], $data['tags']);
130 $this->assertEquals(false, $data['private']); 138 $this->assertEquals(true, $data['private']);
131 $this->assertTrue( 139 $this->assertTrue(
132 new \DateTime('5 seconds ago') < \DateTime::createFromFormat(\DateTime::ATOM, $data['created']) 140 new \DateTime('5 seconds ago') < \DateTime::createFromFormat(\DateTime::ATOM, $data['created'])
133 ); 141 );
@@ -163,7 +171,7 @@ class PostLinkTest extends TestCase
163 $response = $this->controller->postLink($request, new Response()); 171 $response = $this->controller->postLink($request, new Response());
164 172
165 $this->assertEquals(201, $response->getStatusCode()); 173 $this->assertEquals(201, $response->getStatusCode());
166 $this->assertEquals('api/v1/links/1', $response->getHeader('Location')[0]); 174 $this->assertEquals('api/v1/bookmarks/1', $response->getHeader('Location')[0]);
167 $data = json_decode((string) $response->getBody(), true); 175 $data = json_decode((string) $response->getBody(), true);
168 $this->assertEquals(self::NB_FIELDS_LINK, count($data)); 176 $this->assertEquals(self::NB_FIELDS_LINK, count($data));
169 $this->assertEquals(43, $data['id']); 177 $this->assertEquals(43, $data['id']);
@@ -211,11 +219,11 @@ class PostLinkTest extends TestCase
211 $this->assertEquals(['gnu', 'media', 'web', '.hidden', 'hashtag'], $data['tags']); 219 $this->assertEquals(['gnu', 'media', 'web', '.hidden', 'hashtag'], $data['tags']);
212 $this->assertEquals(false, $data['private']); 220 $this->assertEquals(false, $data['private']);
213 $this->assertEquals( 221 $this->assertEquals(
214 \DateTime::createFromFormat(\Shaarli\Bookmark\LinkDB::LINK_DATE_FORMAT, '20130614_184135'), 222 \DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20130614_184135'),
215 \DateTime::createFromFormat(\DateTime::ATOM, $data['created']) 223 \DateTime::createFromFormat(\DateTime::ATOM, $data['created'])
216 ); 224 );
217 $this->assertEquals( 225 $this->assertEquals(
218 \DateTime::createFromFormat(\Shaarli\Bookmark\LinkDB::LINK_DATE_FORMAT, '20130615_184230'), 226 \DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20130615_184230'),
219 \DateTime::createFromFormat(\DateTime::ATOM, $data['updated']) 227 \DateTime::createFromFormat(\DateTime::ATOM, $data['updated'])
220 ); 228 );
221 } 229 }
diff --git a/tests/api/controllers/links/PutLinkTest.php b/tests/api/controllers/links/PutLinkTest.php
index cd815b66..cb63742e 100644
--- a/tests/api/controllers/links/PutLinkTest.php
+++ b/tests/api/controllers/links/PutLinkTest.php
@@ -3,6 +3,8 @@
3 3
4namespace Shaarli\Api\Controllers; 4namespace Shaarli\Api\Controllers;
5 5
6use Shaarli\Bookmark\Bookmark;
7use Shaarli\Bookmark\BookmarkFileService;
6use Shaarli\Config\ConfigManager; 8use Shaarli\Config\ConfigManager;
7use Shaarli\History; 9use Shaarli\History;
8use Slim\Container; 10use Slim\Container;
@@ -33,6 +35,11 @@ class PutLinkTest extends \PHPUnit\Framework\TestCase
33 protected $refDB = null; 35 protected $refDB = null;
34 36
35 /** 37 /**
38 * @var BookmarkFileService instance.
39 */
40 protected $bookmarkService;
41
42 /**
36 * @var HistoryController instance. 43 * @var HistoryController instance.
37 */ 44 */
38 protected $history; 45 protected $history;
@@ -53,22 +60,23 @@ class PutLinkTest extends \PHPUnit\Framework\TestCase
53 const NB_FIELDS_LINK = 9; 60 const NB_FIELDS_LINK = 9;
54 61
55 /** 62 /**
56 * Before every test, instantiate a new Api with its config, plugins and links. 63 * Before every test, instantiate a new Api with its config, plugins and bookmarks.
57 */ 64 */
58 public function setUp() 65 public function setUp()
59 { 66 {
60 $this->conf = new ConfigManager('tests/utils/config/configJson.json.php'); 67 $this->conf = new ConfigManager('tests/utils/config/configJson');
68 $this->conf->set('resource.datastore', self::$testDatastore);
61 $this->refDB = new \ReferenceLinkDB(); 69 $this->refDB = new \ReferenceLinkDB();
62 $this->refDB->write(self::$testDatastore); 70 $this->refDB->write(self::$testDatastore);
63
64 $refHistory = new \ReferenceHistory(); 71 $refHistory = new \ReferenceHistory();
65 $refHistory->write(self::$testHistory); 72 $refHistory->write(self::$testHistory);
66 $this->history = new History(self::$testHistory); 73 $this->history = new History(self::$testHistory);
74 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true);
67 75
68 $this->container = new Container(); 76 $this->container = new Container();
69 $this->container['conf'] = $this->conf; 77 $this->container['conf'] = $this->conf;
70 $this->container['db'] = new \Shaarli\Bookmark\LinkDB(self::$testDatastore, true, false); 78 $this->container['db'] = $this->bookmarkService;
71 $this->container['history'] = new History(self::$testHistory); 79 $this->container['history'] = $this->history;
72 80
73 $this->controller = new Links($this->container); 81 $this->controller = new Links($this->container);
74 82
@@ -110,7 +118,7 @@ class PutLinkTest extends \PHPUnit\Framework\TestCase
110 $this->assertEquals('?WDWyig', $data['title']); 118 $this->assertEquals('?WDWyig', $data['title']);
111 $this->assertEquals('', $data['description']); 119 $this->assertEquals('', $data['description']);
112 $this->assertEquals([], $data['tags']); 120 $this->assertEquals([], $data['tags']);
113 $this->assertEquals(false, $data['private']); 121 $this->assertEquals(true, $data['private']);
114 $this->assertEquals( 122 $this->assertEquals(
115 \DateTime::createFromFormat('Ymd_His', '20150310_114651'), 123 \DateTime::createFromFormat('Ymd_His', '20150310_114651'),
116 \DateTime::createFromFormat(\DateTime::ATOM, $data['created']) 124 \DateTime::createFromFormat(\DateTime::ATOM, $data['created'])
@@ -199,11 +207,11 @@ class PutLinkTest extends \PHPUnit\Framework\TestCase
199 $this->assertEquals(['gnu', 'media', 'web', '.hidden', 'hashtag'], $data['tags']); 207 $this->assertEquals(['gnu', 'media', 'web', '.hidden', 'hashtag'], $data['tags']);
200 $this->assertEquals(false, $data['private']); 208 $this->assertEquals(false, $data['private']);
201 $this->assertEquals( 209 $this->assertEquals(
202 \DateTime::createFromFormat(\Shaarli\Bookmark\LinkDB::LINK_DATE_FORMAT, '20130614_184135'), 210 \DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20130614_184135'),
203 \DateTime::createFromFormat(\DateTime::ATOM, $data['created']) 211 \DateTime::createFromFormat(\DateTime::ATOM, $data['created'])
204 ); 212 );
205 $this->assertEquals( 213 $this->assertEquals(
206 \DateTime::createFromFormat(\Shaarli\Bookmark\LinkDB::LINK_DATE_FORMAT, '20130615_184230'), 214 \DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20130615_184230'),
207 \DateTime::createFromFormat(\DateTime::ATOM, $data['updated']) 215 \DateTime::createFromFormat(\DateTime::ATOM, $data['updated'])
208 ); 216 );
209 } 217 }
diff --git a/tests/api/controllers/tags/DeleteTagTest.php b/tests/api/controllers/tags/DeleteTagTest.php
index 84e1d56e..c6748872 100644
--- a/tests/api/controllers/tags/DeleteTagTest.php
+++ b/tests/api/controllers/tags/DeleteTagTest.php
@@ -3,6 +3,7 @@
3 3
4namespace Shaarli\Api\Controllers; 4namespace Shaarli\Api\Controllers;
5 5
6use Shaarli\Bookmark\BookmarkFileService;
6use Shaarli\Bookmark\LinkDB; 7use Shaarli\Bookmark\LinkDB;
7use Shaarli\Config\ConfigManager; 8use Shaarli\Config\ConfigManager;
8use Shaarli\History; 9use Shaarli\History;
@@ -34,9 +35,9 @@ class DeleteTagTest extends \PHPUnit\Framework\TestCase
34 protected $refDB = null; 35 protected $refDB = null;
35 36
36 /** 37 /**
37 * @var LinkDB instance. 38 * @var BookmarkFileService instance.
38 */ 39 */
39 protected $linkDB; 40 protected $bookmarkService;
40 41
41 /** 42 /**
42 * @var HistoryController instance. 43 * @var HistoryController instance.
@@ -54,20 +55,22 @@ class DeleteTagTest extends \PHPUnit\Framework\TestCase
54 protected $controller; 55 protected $controller;
55 56
56 /** 57 /**
57 * Before each test, instantiate a new Api with its config, plugins and links. 58 * Before each test, instantiate a new Api with its config, plugins and bookmarks.
58 */ 59 */
59 public function setUp() 60 public function setUp()
60 { 61 {
61 $this->conf = new ConfigManager('tests/utils/config/configJson'); 62 $this->conf = new ConfigManager('tests/utils/config/configJson');
63 $this->conf->set('resource.datastore', self::$testDatastore);
62 $this->refDB = new \ReferenceLinkDB(); 64 $this->refDB = new \ReferenceLinkDB();
63 $this->refDB->write(self::$testDatastore); 65 $this->refDB->write(self::$testDatastore);
64 $this->linkDB = new LinkDB(self::$testDatastore, true, false);
65 $refHistory = new \ReferenceHistory(); 66 $refHistory = new \ReferenceHistory();
66 $refHistory->write(self::$testHistory); 67 $refHistory->write(self::$testHistory);
67 $this->history = new History(self::$testHistory); 68 $this->history = new History(self::$testHistory);
69 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true);
70
68 $this->container = new Container(); 71 $this->container = new Container();
69 $this->container['conf'] = $this->conf; 72 $this->container['conf'] = $this->conf;
70 $this->container['db'] = $this->linkDB; 73 $this->container['db'] = $this->bookmarkService;
71 $this->container['history'] = $this->history; 74 $this->container['history'] = $this->history;
72 75
73 $this->controller = new Tags($this->container); 76 $this->controller = new Tags($this->container);
@@ -88,7 +91,7 @@ class DeleteTagTest extends \PHPUnit\Framework\TestCase
88 public function testDeleteTagValid() 91 public function testDeleteTagValid()
89 { 92 {
90 $tagName = 'gnu'; 93 $tagName = 'gnu';
91 $tags = $this->linkDB->linksCountPerTag(); 94 $tags = $this->bookmarkService->bookmarksCountPerTag();
92 $this->assertTrue($tags[$tagName] > 0); 95 $this->assertTrue($tags[$tagName] > 0);
93 $env = Environment::mock([ 96 $env = Environment::mock([
94 'REQUEST_METHOD' => 'DELETE', 97 'REQUEST_METHOD' => 'DELETE',
@@ -99,11 +102,11 @@ class DeleteTagTest extends \PHPUnit\Framework\TestCase
99 $this->assertEquals(204, $response->getStatusCode()); 102 $this->assertEquals(204, $response->getStatusCode());
100 $this->assertEmpty((string) $response->getBody()); 103 $this->assertEmpty((string) $response->getBody());
101 104
102 $this->linkDB = new LinkDB(self::$testDatastore, true, false); 105 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true);
103 $tags = $this->linkDB->linksCountPerTag(); 106 $tags = $this->bookmarkService->bookmarksCountPerTag();
104 $this->assertFalse(isset($tags[$tagName])); 107 $this->assertFalse(isset($tags[$tagName]));
105 108
106 // 2 links affected 109 // 2 bookmarks affected
107 $historyEntry = $this->history->getHistory()[0]; 110 $historyEntry = $this->history->getHistory()[0];
108 $this->assertEquals(History::UPDATED, $historyEntry['event']); 111 $this->assertEquals(History::UPDATED, $historyEntry['event']);
109 $this->assertTrue( 112 $this->assertTrue(
@@ -122,7 +125,7 @@ class DeleteTagTest extends \PHPUnit\Framework\TestCase
122 public function testDeleteTagCaseSensitivity() 125 public function testDeleteTagCaseSensitivity()
123 { 126 {
124 $tagName = 'sTuff'; 127 $tagName = 'sTuff';
125 $tags = $this->linkDB->linksCountPerTag(); 128 $tags = $this->bookmarkService->bookmarksCountPerTag();
126 $this->assertTrue($tags[$tagName] > 0); 129 $this->assertTrue($tags[$tagName] > 0);
127 $env = Environment::mock([ 130 $env = Environment::mock([
128 'REQUEST_METHOD' => 'DELETE', 131 'REQUEST_METHOD' => 'DELETE',
@@ -133,8 +136,8 @@ class DeleteTagTest extends \PHPUnit\Framework\TestCase
133 $this->assertEquals(204, $response->getStatusCode()); 136 $this->assertEquals(204, $response->getStatusCode());
134 $this->assertEmpty((string) $response->getBody()); 137 $this->assertEmpty((string) $response->getBody());
135 138
136 $this->linkDB = new LinkDB(self::$testDatastore, true, false); 139 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true);
137 $tags = $this->linkDB->linksCountPerTag(); 140 $tags = $this->bookmarkService->bookmarksCountPerTag();
138 $this->assertFalse(isset($tags[$tagName])); 141 $this->assertFalse(isset($tags[$tagName]));
139 $this->assertTrue($tags[strtolower($tagName)] > 0); 142 $this->assertTrue($tags[strtolower($tagName)] > 0);
140 143
@@ -154,7 +157,7 @@ class DeleteTagTest extends \PHPUnit\Framework\TestCase
154 public function testDeleteLink404() 157 public function testDeleteLink404()
155 { 158 {
156 $tagName = 'nopenope'; 159 $tagName = 'nopenope';
157 $tags = $this->linkDB->linksCountPerTag(); 160 $tags = $this->bookmarkService->bookmarksCountPerTag();
158 $this->assertFalse(isset($tags[$tagName])); 161 $this->assertFalse(isset($tags[$tagName]));
159 $env = Environment::mock([ 162 $env = Environment::mock([
160 'REQUEST_METHOD' => 'DELETE', 163 'REQUEST_METHOD' => 'DELETE',
diff --git a/tests/api/controllers/tags/GetTagNameTest.php b/tests/api/controllers/tags/GetTagNameTest.php
index a2525c17..b9a81f9b 100644
--- a/tests/api/controllers/tags/GetTagNameTest.php
+++ b/tests/api/controllers/tags/GetTagNameTest.php
@@ -2,8 +2,10 @@
2 2
3namespace Shaarli\Api\Controllers; 3namespace Shaarli\Api\Controllers;
4 4
5use Shaarli\Bookmark\BookmarkFileService;
5use Shaarli\Bookmark\LinkDB; 6use Shaarli\Bookmark\LinkDB;
6use Shaarli\Config\ConfigManager; 7use Shaarli\Config\ConfigManager;
8use Shaarli\History;
7use Slim\Container; 9use Slim\Container;
8use Slim\Http\Environment; 10use Slim\Http\Environment;
9use Slim\Http\Request; 11use Slim\Http\Request;
@@ -49,17 +51,19 @@ class GetTagNameTest extends \PHPUnit\Framework\TestCase
49 const NB_FIELDS_TAG = 2; 51 const NB_FIELDS_TAG = 2;
50 52
51 /** 53 /**
52 * Before each test, instantiate a new Api with its config, plugins and links. 54 * Before each test, instantiate a new Api with its config, plugins and bookmarks.
53 */ 55 */
54 public function setUp() 56 public function setUp()
55 { 57 {
56 $this->conf = new ConfigManager('tests/utils/config/configJson'); 58 $this->conf = new ConfigManager('tests/utils/config/configJson');
59 $this->conf->set('resource.datastore', self::$testDatastore);
57 $this->refDB = new \ReferenceLinkDB(); 60 $this->refDB = new \ReferenceLinkDB();
58 $this->refDB->write(self::$testDatastore); 61 $this->refDB->write(self::$testDatastore);
62 $history = new History('sandbox/history.php');
59 63
60 $this->container = new Container(); 64 $this->container = new Container();
61 $this->container['conf'] = $this->conf; 65 $this->container['conf'] = $this->conf;
62 $this->container['db'] = new LinkDB(self::$testDatastore, true, false); 66 $this->container['db'] = new BookmarkFileService($this->conf, $history, true);
63 $this->container['history'] = null; 67 $this->container['history'] = null;
64 68
65 $this->controller = new Tags($this->container); 69 $this->controller = new Tags($this->container);
diff --git a/tests/api/controllers/tags/GetTagsTest.php b/tests/api/controllers/tags/GetTagsTest.php
index 98628c98..53a3326d 100644
--- a/tests/api/controllers/tags/GetTagsTest.php
+++ b/tests/api/controllers/tags/GetTagsTest.php
@@ -1,8 +1,10 @@
1<?php 1<?php
2namespace Shaarli\Api\Controllers; 2namespace Shaarli\Api\Controllers;
3 3
4use Shaarli\Bookmark\BookmarkFileService;
4use Shaarli\Bookmark\LinkDB; 5use Shaarli\Bookmark\LinkDB;
5use Shaarli\Config\ConfigManager; 6use Shaarli\Config\ConfigManager;
7use Shaarli\History;
6use Slim\Container; 8use Slim\Container;
7use Slim\Http\Environment; 9use Slim\Http\Environment;
8use Slim\Http\Request; 10use Slim\Http\Request;
@@ -38,9 +40,9 @@ class GetTagsTest extends \PHPUnit\Framework\TestCase
38 protected $container; 40 protected $container;
39 41
40 /** 42 /**
41 * @var LinkDB instance. 43 * @var BookmarkFileService instance.
42 */ 44 */
43 protected $linkDB; 45 protected $bookmarkService;
44 46
45 /** 47 /**
46 * @var Tags controller instance. 48 * @var Tags controller instance.
@@ -53,18 +55,21 @@ class GetTagsTest extends \PHPUnit\Framework\TestCase
53 const NB_FIELDS_TAG = 2; 55 const NB_FIELDS_TAG = 2;
54 56
55 /** 57 /**
56 * Before every test, instantiate a new Api with its config, plugins and links. 58 * Before every test, instantiate a new Api with its config, plugins and bookmarks.
57 */ 59 */
58 public function setUp() 60 public function setUp()
59 { 61 {
60 $this->conf = new ConfigManager('tests/utils/config/configJson'); 62 $this->conf = new ConfigManager('tests/utils/config/configJson');
63 $this->conf->set('resource.datastore', self::$testDatastore);
61 $this->refDB = new \ReferenceLinkDB(); 64 $this->refDB = new \ReferenceLinkDB();
62 $this->refDB->write(self::$testDatastore); 65 $this->refDB->write(self::$testDatastore);
66 $history = new History('sandbox/history.php');
67
68 $this->bookmarkService = new BookmarkFileService($this->conf, $history, true);
63 69
64 $this->container = new Container(); 70 $this->container = new Container();
65 $this->container['conf'] = $this->conf; 71 $this->container['conf'] = $this->conf;
66 $this->linkDB = new LinkDB(self::$testDatastore, true, false); 72 $this->container['db'] = $this->bookmarkService;
67 $this->container['db'] = $this->linkDB;
68 $this->container['history'] = null; 73 $this->container['history'] = null;
69 74
70 $this->controller = new Tags($this->container); 75 $this->controller = new Tags($this->container);
@@ -83,7 +88,7 @@ class GetTagsTest extends \PHPUnit\Framework\TestCase
83 */ 88 */
84 public function testGetTagsAll() 89 public function testGetTagsAll()
85 { 90 {
86 $tags = $this->linkDB->linksCountPerTag(); 91 $tags = $this->bookmarkService->bookmarksCountPerTag();
87 $env = Environment::mock([ 92 $env = Environment::mock([
88 'REQUEST_METHOD' => 'GET', 93 'REQUEST_METHOD' => 'GET',
89 ]); 94 ]);
@@ -136,7 +141,7 @@ class GetTagsTest extends \PHPUnit\Framework\TestCase
136 */ 141 */
137 public function testGetTagsLimitAll() 142 public function testGetTagsLimitAll()
138 { 143 {
139 $tags = $this->linkDB->linksCountPerTag(); 144 $tags = $this->bookmarkService->bookmarksCountPerTag();
140 $env = Environment::mock([ 145 $env = Environment::mock([
141 'REQUEST_METHOD' => 'GET', 146 'REQUEST_METHOD' => 'GET',
142 'QUERY_STRING' => 'limit=all' 147 'QUERY_STRING' => 'limit=all'
@@ -170,7 +175,7 @@ class GetTagsTest extends \PHPUnit\Framework\TestCase
170 */ 175 */
171 public function testGetTagsVisibilityPrivate() 176 public function testGetTagsVisibilityPrivate()
172 { 177 {
173 $tags = $this->linkDB->linksCountPerTag([], 'private'); 178 $tags = $this->bookmarkService->bookmarksCountPerTag([], 'private');
174 $env = Environment::mock([ 179 $env = Environment::mock([
175 'REQUEST_METHOD' => 'GET', 180 'REQUEST_METHOD' => 'GET',
176 'QUERY_STRING' => 'visibility=private' 181 'QUERY_STRING' => 'visibility=private'
@@ -190,7 +195,7 @@ class GetTagsTest extends \PHPUnit\Framework\TestCase
190 */ 195 */
191 public function testGetTagsVisibilityPublic() 196 public function testGetTagsVisibilityPublic()
192 { 197 {
193 $tags = $this->linkDB->linksCountPerTag([], 'public'); 198 $tags = $this->bookmarkService->bookmarksCountPerTag([], 'public');
194 $env = Environment::mock( 199 $env = Environment::mock(
195 [ 200 [
196 'REQUEST_METHOD' => 'GET', 201 'REQUEST_METHOD' => 'GET',
diff --git a/tests/api/controllers/tags/PutTagTest.php b/tests/api/controllers/tags/PutTagTest.php
index 86106fc7..2a3cc15a 100644
--- a/tests/api/controllers/tags/PutTagTest.php
+++ b/tests/api/controllers/tags/PutTagTest.php
@@ -3,6 +3,7 @@
3namespace Shaarli\Api\Controllers; 3namespace Shaarli\Api\Controllers;
4 4
5use Shaarli\Api\Exceptions\ApiBadParametersException; 5use Shaarli\Api\Exceptions\ApiBadParametersException;
6use Shaarli\Bookmark\BookmarkFileService;
6use Shaarli\Bookmark\LinkDB; 7use Shaarli\Bookmark\LinkDB;
7use Shaarli\Config\ConfigManager; 8use Shaarli\Config\ConfigManager;
8use Shaarli\History; 9use Shaarli\History;
@@ -44,9 +45,9 @@ class PutTagTest extends \PHPUnit\Framework\TestCase
44 protected $container; 45 protected $container;
45 46
46 /** 47 /**
47 * @var LinkDB instance. 48 * @var BookmarkFileService instance.
48 */ 49 */
49 protected $linkDB; 50 protected $bookmarkService;
50 51
51 /** 52 /**
52 * @var Tags controller instance. 53 * @var Tags controller instance.
@@ -59,22 +60,22 @@ class PutTagTest extends \PHPUnit\Framework\TestCase
59 const NB_FIELDS_TAG = 2; 60 const NB_FIELDS_TAG = 2;
60 61
61 /** 62 /**
62 * Before every test, instantiate a new Api with its config, plugins and links. 63 * Before every test, instantiate a new Api with its config, plugins and bookmarks.
63 */ 64 */
64 public function setUp() 65 public function setUp()
65 { 66 {
66 $this->conf = new ConfigManager('tests/utils/config/configJson.json.php'); 67 $this->conf = new ConfigManager('tests/utils/config/configJson');
68 $this->conf->set('resource.datastore', self::$testDatastore);
67 $this->refDB = new \ReferenceLinkDB(); 69 $this->refDB = new \ReferenceLinkDB();
68 $this->refDB->write(self::$testDatastore); 70 $this->refDB->write(self::$testDatastore);
69
70 $refHistory = new \ReferenceHistory(); 71 $refHistory = new \ReferenceHistory();
71 $refHistory->write(self::$testHistory); 72 $refHistory->write(self::$testHistory);
72 $this->history = new History(self::$testHistory); 73 $this->history = new History(self::$testHistory);
74 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true);
73 75
74 $this->container = new Container(); 76 $this->container = new Container();
75 $this->container['conf'] = $this->conf; 77 $this->container['conf'] = $this->conf;
76 $this->linkDB = new LinkDB(self::$testDatastore, true, false); 78 $this->container['db'] = $this->bookmarkService;
77 $this->container['db'] = $this->linkDB;
78 $this->container['history'] = $this->history; 79 $this->container['history'] = $this->history;
79 80
80 $this->controller = new Tags($this->container); 81 $this->controller = new Tags($this->container);
@@ -109,7 +110,7 @@ class PutTagTest extends \PHPUnit\Framework\TestCase
109 $this->assertEquals($newName, $data['name']); 110 $this->assertEquals($newName, $data['name']);
110 $this->assertEquals(2, $data['occurrences']); 111 $this->assertEquals(2, $data['occurrences']);
111 112
112 $tags = $this->linkDB->linksCountPerTag(); 113 $tags = $this->bookmarkService->bookmarksCountPerTag();
113 $this->assertNotTrue(isset($tags[$tagName])); 114 $this->assertNotTrue(isset($tags[$tagName]));
114 $this->assertEquals(2, $tags[$newName]); 115 $this->assertEquals(2, $tags[$newName]);
115 116
@@ -133,7 +134,7 @@ class PutTagTest extends \PHPUnit\Framework\TestCase
133 $tagName = 'gnu'; 134 $tagName = 'gnu';
134 $newName = 'w3c'; 135 $newName = 'w3c';
135 136
136 $tags = $this->linkDB->linksCountPerTag(); 137 $tags = $this->bookmarkService->bookmarksCountPerTag();
137 $this->assertEquals(1, $tags[$newName]); 138 $this->assertEquals(1, $tags[$newName]);
138 $this->assertEquals(2, $tags[$tagName]); 139 $this->assertEquals(2, $tags[$tagName]);
139 140
@@ -151,7 +152,7 @@ class PutTagTest extends \PHPUnit\Framework\TestCase
151 $this->assertEquals($newName, $data['name']); 152 $this->assertEquals($newName, $data['name']);
152 $this->assertEquals(3, $data['occurrences']); 153 $this->assertEquals(3, $data['occurrences']);
153 154
154 $tags = $this->linkDB->linksCountPerTag(); 155 $tags = $this->bookmarkService->bookmarksCountPerTag();
155 $this->assertNotTrue(isset($tags[$tagName])); 156 $this->assertNotTrue(isset($tags[$tagName]));
156 $this->assertEquals(3, $tags[$newName]); 157 $this->assertEquals(3, $tags[$newName]);
157 } 158 }
@@ -167,7 +168,7 @@ class PutTagTest extends \PHPUnit\Framework\TestCase
167 $tagName = 'gnu'; 168 $tagName = 'gnu';
168 $newName = ''; 169 $newName = '';
169 170
170 $tags = $this->linkDB->linksCountPerTag(); 171 $tags = $this->bookmarkService->bookmarksCountPerTag();
171 $this->assertEquals(2, $tags[$tagName]); 172 $this->assertEquals(2, $tags[$tagName]);
172 173
173 $env = Environment::mock([ 174 $env = Environment::mock([
@@ -185,7 +186,7 @@ class PutTagTest extends \PHPUnit\Framework\TestCase
185 try { 186 try {
186 $this->controller->putTag($request, new Response(), ['tagName' => $tagName]); 187 $this->controller->putTag($request, new Response(), ['tagName' => $tagName]);
187 } catch (ApiBadParametersException $e) { 188 } catch (ApiBadParametersException $e) {
188 $tags = $this->linkDB->linksCountPerTag(); 189 $tags = $this->bookmarkService->bookmarksCountPerTag();
189 $this->assertEquals(2, $tags[$tagName]); 190 $this->assertEquals(2, $tags[$tagName]);
190 throw $e; 191 throw $e;
191 } 192 }
diff --git a/tests/bookmark/BookmarkArrayTest.php b/tests/bookmark/BookmarkArrayTest.php
new file mode 100644
index 00000000..0f8f04c5
--- /dev/null
+++ b/tests/bookmark/BookmarkArrayTest.php
@@ -0,0 +1,239 @@
1<?php
2
3namespace Shaarli\Bookmark;
4
5use PHPUnit\Framework\TestCase;
6use Shaarli\Bookmark\Exception\InvalidBookmarkException;
7use Shaarli\Config\ConfigManager;
8use Shaarli\History;
9
10/**
11 * Class BookmarkArrayTest
12 */
13class BookmarkArrayTest extends TestCase
14{
15 /**
16 * Test the constructor and make sure that the instance is properly initialized
17 */
18 public function testArrayConstructorEmpty()
19 {
20 $array = new BookmarkArray();
21 $this->assertTrue(is_iterable($array));
22 $this->assertEmpty($array);
23 }
24
25 /**
26 * Test adding entries to the array, specifying the key offset or not.
27 */
28 public function testArrayAccessAddEntries()
29 {
30 $array = new BookmarkArray();
31 $bookmark = new Bookmark();
32 $bookmark->setId(11)->validate();
33 $array[] = $bookmark;
34 $this->assertCount(1, $array);
35 $this->assertTrue(isset($array[11]));
36 $this->assertNull($array[0]);
37 $this->assertEquals($bookmark, $array[11]);
38
39 $bookmark = new Bookmark();
40 $bookmark->setId(14)->validate();
41 $array[14] = $bookmark;
42 $this->assertCount(2, $array);
43 $this->assertTrue(isset($array[14]));
44 $this->assertNull($array[0]);
45 $this->assertEquals($bookmark, $array[14]);
46 }
47
48 /**
49 * Test adding a bad entry: wrong type
50 *
51 * @expectedException Shaarli\Bookmark\Exception\InvalidBookmarkException
52 */
53 public function testArrayAccessAddBadEntryInstance()
54 {
55 $array = new BookmarkArray();
56 $array[] = 'nope';
57 }
58
59 /**
60 * Test adding a bad entry: no id
61 *
62 * @expectedException Shaarli\Bookmark\Exception\InvalidBookmarkException
63 */
64 public function testArrayAccessAddBadEntryNoId()
65 {
66 $array = new BookmarkArray();
67 $bookmark = new Bookmark();
68 $array[] = $bookmark;
69 }
70
71 /**
72 * Test adding a bad entry: no url
73 *
74 * @expectedException Shaarli\Bookmark\Exception\InvalidBookmarkException
75 */
76 public function testArrayAccessAddBadEntryNoUrl()
77 {
78 $array = new BookmarkArray();
79 $bookmark = (new Bookmark())->setId(11);
80 $array[] = $bookmark;
81 }
82
83 /**
84 * Test adding a bad entry: invalid offset
85 *
86 * @expectedException Shaarli\Bookmark\Exception\InvalidBookmarkException
87 */
88 public function testArrayAccessAddBadEntryOffset()
89 {
90 $array = new BookmarkArray();
91 $bookmark = (new Bookmark())->setId(11);
92 $bookmark->validate();
93 $array['nope'] = $bookmark;
94 }
95
96 /**
97 * Test adding a bad entry: invalid ID type
98 *
99 * @expectedException Shaarli\Bookmark\Exception\InvalidBookmarkException
100 */
101 public function testArrayAccessAddBadEntryIdType()
102 {
103 $array = new BookmarkArray();
104 $bookmark = (new Bookmark())->setId('nope');
105 $bookmark->validate();
106 $array[] = $bookmark;
107 }
108
109 /**
110 * Test adding a bad entry: ID/offset not consistent
111 *
112 * @expectedException Shaarli\Bookmark\Exception\InvalidBookmarkException
113 */
114 public function testArrayAccessAddBadEntryIdOffset()
115 {
116 $array = new BookmarkArray();
117 $bookmark = (new Bookmark())->setId(11);
118 $bookmark->validate();
119 $array[14] = $bookmark;
120 }
121
122 /**
123 * Test update entries through array access.
124 */
125 public function testArrayAccessUpdateEntries()
126 {
127 $array = new BookmarkArray();
128 $bookmark = new Bookmark();
129 $bookmark->setId(11)->validate();
130 $bookmark->setTitle('old');
131 $array[] = $bookmark;
132 $bookmark = new Bookmark();
133 $bookmark->setId(11)->validate();
134 $bookmark->setTitle('test');
135 $array[] = $bookmark;
136 $this->assertCount(1, $array);
137 $this->assertEquals('test', $array[11]->getTitle());
138
139 $bookmark = new Bookmark();
140 $bookmark->setId(11)->validate();
141 $bookmark->setTitle('test2');
142 $array[11] = $bookmark;
143 $this->assertCount(1, $array);
144 $this->assertEquals('test2', $array[11]->getTitle());
145 }
146
147 /**
148 * Test delete entries through array access.
149 */
150 public function testArrayAccessDeleteEntries()
151 {
152 $array = new BookmarkArray();
153 $bookmark11 = new Bookmark();
154 $bookmark11->setId(11)->validate();
155 $array[] = $bookmark11;
156 $bookmark14 = new Bookmark();
157 $bookmark14->setId(14)->validate();
158 $array[] = $bookmark14;
159 $bookmark23 = new Bookmark();
160 $bookmark23->setId(23)->validate();
161 $array[] = $bookmark23;
162 $bookmark0 = new Bookmark();
163 $bookmark0->setId(0)->validate();
164 $array[] = $bookmark0;
165 $this->assertCount(4, $array);
166
167 unset($array[14]);
168 $this->assertCount(3, $array);
169 $this->assertEquals($bookmark11, $array[11]);
170 $this->assertEquals($bookmark23, $array[23]);
171 $this->assertEquals($bookmark0, $array[0]);
172
173 unset($array[23]);
174 $this->assertCount(2, $array);
175 $this->assertEquals($bookmark11, $array[11]);
176 $this->assertEquals($bookmark0, $array[0]);
177
178 unset($array[11]);
179 $this->assertCount(1, $array);
180 $this->assertEquals($bookmark0, $array[0]);
181
182 unset($array[0]);
183 $this->assertCount(0, $array);
184 }
185
186 /**
187 * Test iterating through array access.
188 */
189 public function testArrayAccessIterate()
190 {
191 $array = new BookmarkArray();
192 $bookmark11 = new Bookmark();
193 $bookmark11->setId(11)->validate();
194 $array[] = $bookmark11;
195 $bookmark14 = new Bookmark();
196 $bookmark14->setId(14)->validate();
197 $array[] = $bookmark14;
198 $bookmark23 = new Bookmark();
199 $bookmark23->setId(23)->validate();
200 $array[] = $bookmark23;
201 $this->assertCount(3, $array);
202
203 foreach ($array as $id => $bookmark) {
204 $this->assertEquals(${'bookmark'. $id}, $bookmark);
205 }
206 }
207
208 /**
209 * Test reordering the array.
210 */
211 public function testReorder()
212 {
213 $refDB = new \ReferenceLinkDB();
214 $refDB->write('sandbox/datastore.php');
215
216
217 $bookmarks = $refDB->getLinks();
218 $bookmarks->reorder('ASC');
219 $this->assertInstanceOf(BookmarkArray::class, $bookmarks);
220
221 $stickyIds = [11, 10];
222 $standardIds = [42, 4, 9, 1, 0, 7, 6, 8, 41];
223 $linkIds = array_merge($stickyIds, $standardIds);
224 $cpt = 0;
225 foreach ($bookmarks as $key => $value) {
226 $this->assertEquals($linkIds[$cpt++], $key);
227 }
228
229 $bookmarks = $refDB->getLinks();
230 $bookmarks->reorder('DESC');
231 $this->assertInstanceOf(BookmarkArray::class, $bookmarks);
232
233 $linkIds = array_merge(array_reverse($stickyIds), array_reverse($standardIds));
234 $cpt = 0;
235 foreach ($bookmarks as $key => $value) {
236 $this->assertEquals($linkIds[$cpt++], $key);
237 }
238 }
239}
diff --git a/tests/bookmark/BookmarkFileServiceTest.php b/tests/bookmark/BookmarkFileServiceTest.php
new file mode 100644
index 00000000..1b438a7f
--- /dev/null
+++ b/tests/bookmark/BookmarkFileServiceTest.php
@@ -0,0 +1,1042 @@
1<?php
2/**
3 * Link datastore tests
4 */
5
6namespace Shaarli\Bookmark;
7
8use DateTime;
9use PHPUnit\Framework\TestCase;
10use ReferenceLinkDB;
11use ReflectionClass;
12use Shaarli;
13use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
14use Shaarli\Config\ConfigManager;
15use Shaarli\History;
16
17/**
18 * Unitary tests for LegacyLinkDBTest
19 */
20class BookmarkFileServiceTest extends TestCase
21{
22 // datastore to test write operations
23 protected static $testDatastore = 'sandbox/datastore.php';
24
25 protected static $testConf = 'sandbox/config';
26
27 protected static $testUpdates = 'sandbox/updates.txt';
28
29 /**
30 * @var ConfigManager instance.
31 */
32 protected $conf;
33
34 /**
35 * @var History instance.
36 */
37 protected $history;
38
39 /**
40 * @var ReferenceLinkDB instance.
41 */
42 protected $refDB = null;
43
44 /**
45 * @var BookmarkFileService public LinkDB instance.
46 */
47 protected $publicLinkDB = null;
48
49 /**
50 * @var BookmarkFileService private LinkDB instance.
51 */
52 protected $privateLinkDB = null;
53
54 /**
55 * Instantiates public and private LinkDBs with test data
56 *
57 * The reference datastore contains public and private bookmarks that
58 * will be used to test LinkDB's methods:
59 * - access filtering (public/private),
60 * - link searches:
61 * - by day,
62 * - by tag,
63 * - by text,
64 * - etc.
65 *
66 * Resets test data for each test
67 */
68 protected function setUp()
69 {
70 if (file_exists(self::$testDatastore)) {
71 unlink(self::$testDatastore);
72 }
73
74 if (file_exists(self::$testConf .'.json.php')) {
75 unlink(self::$testConf .'.json.php');
76 }
77
78 if (file_exists(self::$testUpdates)) {
79 unlink(self::$testUpdates);
80 }
81
82 copy('tests/utils/config/configJson.json.php', self::$testConf .'.json.php');
83 $this->conf = new ConfigManager(self::$testConf);
84 $this->conf->set('resource.datastore', self::$testDatastore);
85 $this->conf->set('resource.updates', self::$testUpdates);
86 $this->refDB = new \ReferenceLinkDB();
87 $this->refDB->write(self::$testDatastore);
88 $this->history = new History('sandbox/history.php');
89 $this->publicLinkDB = new BookmarkFileService($this->conf, $this->history, false);
90 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, true);
91 }
92
93 /**
94 * Test migrate() method with a legacy datastore.
95 */
96 public function testDatabaseMigration()
97 {
98 if (!defined('SHAARLI_VERSION')) {
99 define('SHAARLI_VERSION', 'dev');
100 }
101
102 $this->refDB = new \ReferenceLinkDB(true);
103 $this->refDB->write(self::$testDatastore);
104 $db = self::getMethod('migrate');
105 $db->invokeArgs($this->privateLinkDB, []);
106
107 $db = new \FakeBookmarkService($this->conf, $this->history, true);
108 $this->assertInstanceOf(BookmarkArray::class, $db->getBookmarks());
109 $this->assertEquals($this->refDB->countLinks(), $db->count());
110 }
111
112 /**
113 * Test get() method for a defined and saved bookmark
114 */
115 public function testGetDefinedSaved()
116 {
117 $bookmark = $this->privateLinkDB->get(42);
118 $this->assertEquals(42, $bookmark->getId());
119 $this->assertEquals('Note: I have a big ID but an old date', $bookmark->getTitle());
120 }
121
122 /**
123 * Test get() method for a defined and not saved bookmark
124 */
125 public function testGetDefinedNotSaved()
126 {
127 $bookmark = new Bookmark();
128 $this->privateLinkDB->add($bookmark);
129 $createdBookmark = $this->privateLinkDB->get(43);
130 $this->assertEquals(43, $createdBookmark->getId());
131 $this->assertEmpty($createdBookmark->getDescription());
132 }
133
134 /**
135 * Test get() method for an undefined bookmark
136 *
137 * @expectedException Shaarli\Bookmark\Exception\BookmarkNotFoundException
138 */
139 public function testGetUndefined()
140 {
141 $this->privateLinkDB->get(666);
142 }
143
144 /**
145 * Test add() method for a bookmark fully built
146 */
147 public function testAddFull()
148 {
149 $bookmark = new Bookmark();
150 $bookmark->setUrl($url = 'https://domain.tld/index.php');
151 $bookmark->setShortUrl('abc');
152 $bookmark->setTitle($title = 'This a brand new bookmark');
153 $bookmark->setDescription($desc = 'It should be created and written');
154 $bookmark->setTags($tags = ['tag1', 'tagssss']);
155 $bookmark->setThumbnail($thumb = 'http://thumb.tld/dle.png');
156 $bookmark->setPrivate(true);
157 $bookmark->setSticky(true);
158 $bookmark->setCreated($created = DateTime::createFromFormat('Ymd_His', '20190518_140354'));
159 $bookmark->setUpdated($updated = DateTime::createFromFormat('Ymd_His', '20190518_150354'));
160
161 $this->privateLinkDB->add($bookmark);
162 $bookmark = $this->privateLinkDB->get(43);
163 $this->assertEquals(43, $bookmark->getId());
164 $this->assertEquals($url, $bookmark->getUrl());
165 $this->assertEquals('abc', $bookmark->getShortUrl());
166 $this->assertEquals($title, $bookmark->getTitle());
167 $this->assertEquals($desc, $bookmark->getDescription());
168 $this->assertEquals($tags, $bookmark->getTags());
169 $this->assertEquals($thumb, $bookmark->getThumbnail());
170 $this->assertTrue($bookmark->isPrivate());
171 $this->assertTrue($bookmark->isSticky());
172 $this->assertEquals($created, $bookmark->getCreated());
173 $this->assertEquals($updated, $bookmark->getUpdated());
174
175 // reload from file
176 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, true);
177
178 $bookmark = $this->privateLinkDB->get(43);
179 $this->assertEquals(43, $bookmark->getId());
180 $this->assertEquals($url, $bookmark->getUrl());
181 $this->assertEquals('abc', $bookmark->getShortUrl());
182 $this->assertEquals($title, $bookmark->getTitle());
183 $this->assertEquals($desc, $bookmark->getDescription());
184 $this->assertEquals($tags, $bookmark->getTags());
185 $this->assertEquals($thumb, $bookmark->getThumbnail());
186 $this->assertTrue($bookmark->isPrivate());
187 $this->assertTrue($bookmark->isSticky());
188 $this->assertEquals($created, $bookmark->getCreated());
189 $this->assertEquals($updated, $bookmark->getUpdated());
190 }
191
192 /**
193 * Test add() method for a bookmark without any field set
194 */
195 public function testAddMinimal()
196 {
197 $bookmark = new Bookmark();
198 $this->privateLinkDB->add($bookmark);
199
200 $bookmark = $this->privateLinkDB->get(43);
201 $this->assertEquals(43, $bookmark->getId());
202 $this->assertRegExp('/\?[\w\-]{6}/', $bookmark->getUrl());
203 $this->assertRegExp('/[\w\-]{6}/', $bookmark->getShortUrl());
204 $this->assertEquals($bookmark->getUrl(), $bookmark->getTitle());
205 $this->assertEmpty($bookmark->getDescription());
206 $this->assertEmpty($bookmark->getTags());
207 $this->assertEmpty($bookmark->getThumbnail());
208 $this->assertFalse($bookmark->isPrivate());
209 $this->assertFalse($bookmark->isSticky());
210 $this->assertTrue(new \DateTime('5 seconds ago') < $bookmark->getCreated());
211 $this->assertNull($bookmark->getUpdated());
212
213 // reload from file
214 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, true);
215
216 $bookmark = $this->privateLinkDB->get(43);
217 $this->assertEquals(43, $bookmark->getId());
218 $this->assertRegExp('/\?[\w\-]{6}/', $bookmark->getUrl());
219 $this->assertRegExp('/[\w\-]{6}/', $bookmark->getShortUrl());
220 $this->assertEquals($bookmark->getUrl(), $bookmark->getTitle());
221 $this->assertEmpty($bookmark->getDescription());
222 $this->assertEmpty($bookmark->getTags());
223 $this->assertEmpty($bookmark->getThumbnail());
224 $this->assertFalse($bookmark->isPrivate());
225 $this->assertFalse($bookmark->isSticky());
226 $this->assertTrue(new \DateTime('5 seconds ago') < $bookmark->getCreated());
227 $this->assertNull($bookmark->getUpdated());
228 }
229
230 /**
231 * Test add() method for a bookmark without any field set and without writing the data store
232 *
233 * @expectedExceptionMessage Shaarli\Bookmark\Exception\BookmarkNotFoundException
234 */
235 public function testAddMinimalNoWrite()
236 {
237 $bookmark = new Bookmark();
238 $this->privateLinkDB->add($bookmark);
239
240 $bookmark = $this->privateLinkDB->get(43);
241 $this->assertEquals(43, $bookmark->getId());
242
243 // reload from file
244 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, true);
245
246 $this->privateLinkDB->get(43);
247 }
248
249 /**
250 * Test add() method while logged out
251 *
252 * @expectedException \Exception
253 * @expectedExceptionMessage You're not authorized to alter the datastore
254 */
255 public function testAddLoggedOut()
256 {
257 $this->publicLinkDB->add(new Bookmark());
258 }
259
260 /**
261 * Test add() method with an entry which is not a bookmark instance
262 *
263 * @expectedException \Exception
264 * @expectedExceptionMessage Provided data is invalid
265 */
266 public function testAddNotABookmark()
267 {
268 $this->privateLinkDB->add(['title' => 'hi!']);
269 }
270
271 /**
272 * Test add() method with a Bookmark already containing an ID
273 *
274 * @expectedException \Exception
275 * @expectedExceptionMessage This bookmarks already exists
276 */
277 public function testAddWithId()
278 {
279 $bookmark = new Bookmark();
280 $bookmark->setId(43);
281 $this->privateLinkDB->add($bookmark);
282 }
283
284 /**
285 * Test set() method for a bookmark fully built
286 */
287 public function testSetFull()
288 {
289 $bookmark = $this->privateLinkDB->get(42);
290 $bookmark->setUrl($url = 'https://domain.tld/index.php');
291 $bookmark->setShortUrl('abc');
292 $bookmark->setTitle($title = 'This a brand new bookmark');
293 $bookmark->setDescription($desc = 'It should be created and written');
294 $bookmark->setTags($tags = ['tag1', 'tagssss']);
295 $bookmark->setThumbnail($thumb = 'http://thumb.tld/dle.png');
296 $bookmark->setPrivate(true);
297 $bookmark->setSticky(true);
298 $bookmark->setCreated($created = DateTime::createFromFormat('Ymd_His', '20190518_140354'));
299 $bookmark->setUpdated($updated = DateTime::createFromFormat('Ymd_His', '20190518_150354'));
300
301 $this->privateLinkDB->set($bookmark);
302 $bookmark = $this->privateLinkDB->get(42);
303 $this->assertEquals(42, $bookmark->getId());
304 $this->assertEquals($url, $bookmark->getUrl());
305 $this->assertEquals('abc', $bookmark->getShortUrl());
306 $this->assertEquals($title, $bookmark->getTitle());
307 $this->assertEquals($desc, $bookmark->getDescription());
308 $this->assertEquals($tags, $bookmark->getTags());
309 $this->assertEquals($thumb, $bookmark->getThumbnail());
310 $this->assertTrue($bookmark->isPrivate());
311 $this->assertTrue($bookmark->isSticky());
312 $this->assertEquals($created, $bookmark->getCreated());
313 $this->assertTrue(new \DateTime('5 seconds ago') < $bookmark->getUpdated());
314
315 // reload from file
316 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, true);
317
318 $bookmark = $this->privateLinkDB->get(42);
319 $this->assertEquals(42, $bookmark->getId());
320 $this->assertEquals($url, $bookmark->getUrl());
321 $this->assertEquals('abc', $bookmark->getShortUrl());
322 $this->assertEquals($title, $bookmark->getTitle());
323 $this->assertEquals($desc, $bookmark->getDescription());
324 $this->assertEquals($tags, $bookmark->getTags());
325 $this->assertEquals($thumb, $bookmark->getThumbnail());
326 $this->assertTrue($bookmark->isPrivate());
327 $this->assertTrue($bookmark->isSticky());
328 $this->assertEquals($created, $bookmark->getCreated());
329 $this->assertTrue(new \DateTime('5 seconds ago') < $bookmark->getUpdated());
330 }
331
332 /**
333 * Test set() method for a bookmark without any field set
334 */
335 public function testSetMinimal()
336 {
337 $bookmark = $this->privateLinkDB->get(42);
338 $this->privateLinkDB->set($bookmark);
339
340 $bookmark = $this->privateLinkDB->get(42);
341 $this->assertEquals(42, $bookmark->getId());
342 $this->assertEquals('?WDWyig', $bookmark->getUrl());
343 $this->assertEquals('1eYJ1Q', $bookmark->getShortUrl());
344 $this->assertEquals('Note: I have a big ID but an old date', $bookmark->getTitle());
345 $this->assertEquals('Used to test bookmarks reordering.', $bookmark->getDescription());
346 $this->assertEquals(['ut'], $bookmark->getTags());
347 $this->assertFalse($bookmark->getThumbnail());
348 $this->assertFalse($bookmark->isPrivate());
349 $this->assertFalse($bookmark->isSticky());
350 $this->assertEquals(
351 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20100310_101010'),
352 $bookmark->getCreated()
353 );
354 $this->assertTrue(new \DateTime('5 seconds ago') < $bookmark->getUpdated());
355
356 // reload from file
357 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, true);
358
359 $bookmark = $this->privateLinkDB->get(42);
360 $this->assertEquals(42, $bookmark->getId());
361 $this->assertEquals('?WDWyig', $bookmark->getUrl());
362 $this->assertEquals('1eYJ1Q', $bookmark->getShortUrl());
363 $this->assertEquals('Note: I have a big ID but an old date', $bookmark->getTitle());
364 $this->assertEquals('Used to test bookmarks reordering.', $bookmark->getDescription());
365 $this->assertEquals(['ut'], $bookmark->getTags());
366 $this->assertFalse($bookmark->getThumbnail());
367 $this->assertFalse($bookmark->isPrivate());
368 $this->assertFalse($bookmark->isSticky());
369 $this->assertEquals(
370 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20100310_101010'),
371 $bookmark->getCreated()
372 );
373 $this->assertTrue(new \DateTime('5 seconds ago') < $bookmark->getUpdated());
374 }
375
376 /**
377 * Test set() method for a bookmark without any field set and without writing the data store
378 */
379 public function testSetMinimalNoWrite()
380 {
381 $bookmark = $this->privateLinkDB->get(42);
382 $bookmark->setTitle($title = 'hi!');
383 $this->privateLinkDB->set($bookmark, false);
384
385 $bookmark = $this->privateLinkDB->get(42);
386 $this->assertEquals(42, $bookmark->getId());
387 $this->assertEquals($title, $bookmark->getTitle());
388
389 // reload from file
390 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, true);
391
392 $bookmark = $this->privateLinkDB->get(42);
393 $this->assertEquals(42, $bookmark->getId());
394 $this->assertEquals('Note: I have a big ID but an old date', $bookmark->getTitle());
395 }
396
397 /**
398 * Test set() method while logged out
399 *
400 * @expectedException \Exception
401 * @expectedExceptionMessage You're not authorized to alter the datastore
402 */
403 public function testSetLoggedOut()
404 {
405 $this->publicLinkDB->set(new Bookmark());
406 }
407
408 /**
409 * Test set() method with an entry which is not a bookmark instance
410 *
411 * @expectedException \Exception
412 * @expectedExceptionMessage Provided data is invalid
413 */
414 public function testSetNotABookmark()
415 {
416 $this->privateLinkDB->set(['title' => 'hi!']);
417 }
418
419 /**
420 * Test set() method with a Bookmark without an ID defined.
421 *
422 * @expectedException Shaarli\Bookmark\Exception\BookmarkNotFoundException
423 */
424 public function testSetWithoutId()
425 {
426 $bookmark = new Bookmark();
427 $this->privateLinkDB->set($bookmark);
428 }
429
430 /**
431 * Test set() method with a Bookmark with an unknow ID
432 *
433 * @expectedException Shaarli\Bookmark\Exception\BookmarkNotFoundException
434 */
435 public function testSetWithUnknownId()
436 {
437 $bookmark = new Bookmark();
438 $bookmark->setId(666);
439 $this->privateLinkDB->set($bookmark);
440 }
441
442 /**
443 * Test addOrSet() method with a new ID
444 */
445 public function testAddOrSetNew()
446 {
447 $bookmark = new Bookmark();
448 $this->privateLinkDB->addOrSet($bookmark);
449
450 $bookmark = $this->privateLinkDB->get(43);
451 $this->assertEquals(43, $bookmark->getId());
452
453 // reload from file
454 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, true);
455
456 $bookmark = $this->privateLinkDB->get(43);
457 $this->assertEquals(43, $bookmark->getId());
458 }
459
460 /**
461 * Test addOrSet() method with an existing ID
462 */
463 public function testAddOrSetExisting()
464 {
465 $bookmark = $this->privateLinkDB->get(42);
466 $bookmark->setTitle($title = 'hi!');
467 $this->privateLinkDB->addOrSet($bookmark);
468
469 $bookmark = $this->privateLinkDB->get(42);
470 $this->assertEquals(42, $bookmark->getId());
471 $this->assertEquals($title, $bookmark->getTitle());
472
473 // reload from file
474 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, true);
475
476 $bookmark = $this->privateLinkDB->get(42);
477 $this->assertEquals(42, $bookmark->getId());
478 $this->assertEquals($title, $bookmark->getTitle());
479 }
480
481 /**
482 * Test addOrSet() method while logged out
483 *
484 * @expectedException \Exception
485 * @expectedExceptionMessage You're not authorized to alter the datastore
486 */
487 public function testAddOrSetLoggedOut()
488 {
489 $this->publicLinkDB->addOrSet(new Bookmark());
490 }
491
492 /**
493 * Test addOrSet() method with an entry which is not a bookmark instance
494 *
495 * @expectedException \Exception
496 * @expectedExceptionMessage Provided data is invalid
497 */
498 public function testAddOrSetNotABookmark()
499 {
500 $this->privateLinkDB->addOrSet(['title' => 'hi!']);
501 }
502
503 /**
504 * Test addOrSet() method for a bookmark without any field set and without writing the data store
505 */
506 public function testAddOrSetMinimalNoWrite()
507 {
508 $bookmark = $this->privateLinkDB->get(42);
509 $bookmark->setTitle($title = 'hi!');
510 $this->privateLinkDB->addOrSet($bookmark, false);
511
512 $bookmark = $this->privateLinkDB->get(42);
513 $this->assertEquals(42, $bookmark->getId());
514 $this->assertEquals($title, $bookmark->getTitle());
515
516 // reload from file
517 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, true);
518
519 $bookmark = $this->privateLinkDB->get(42);
520 $this->assertEquals(42, $bookmark->getId());
521 $this->assertEquals('Note: I have a big ID but an old date', $bookmark->getTitle());
522 }
523
524 /**
525 * Test remove() method with an existing Bookmark
526 *
527 * @expectedException Shaarli\Bookmark\Exception\BookmarkNotFoundException
528 */
529 public function testRemoveExisting()
530 {
531 $bookmark = $this->privateLinkDB->get(42);
532 $this->privateLinkDB->remove($bookmark);
533
534 $exception = null;
535 try {
536 $this->privateLinkDB->get(42);
537 } catch (BookmarkNotFoundException $e) {
538 $exception = $e;
539 }
540 $this->assertInstanceOf(BookmarkNotFoundException::class, $exception);
541
542 // reload from file
543 $this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, true);
544
545 $this->privateLinkDB->get(42);
546 }
547
548 /**
549 * Test remove() method while logged out
550 *
551 * @expectedException \Exception
552 * @expectedExceptionMessage You're not authorized to alter the datastore
553 */
554 public function testRemoveLoggedOut()
555 {
556 $bookmark = $this->privateLinkDB->get(42);
557 $this->publicLinkDB->remove($bookmark);
558 }
559
560 /**
561 * Test remove() method with an entry which is not a bookmark instance
562 *
563 * @expectedException \Exception
564 * @expectedExceptionMessage Provided data is invalid
565 */
566 public function testRemoveNotABookmark()
567 {
568 $this->privateLinkDB->remove(['title' => 'hi!']);
569 }
570
571 /**
572 * Test remove() method with a Bookmark with an unknown ID
573 *
574 * @expectedException Shaarli\Bookmark\Exception\BookmarkNotFoundException
575 */
576 public function testRemoveWithUnknownId()
577 {
578 $bookmark = new Bookmark();
579 $bookmark->setId(666);
580 $this->privateLinkDB->remove($bookmark);
581 }
582
583 /**
584 * Test exists() method
585 */
586 public function testExists()
587 {
588 $this->assertTrue($this->privateLinkDB->exists(42)); // public
589 $this->assertTrue($this->privateLinkDB->exists(6)); // private
590
591 $this->assertTrue($this->privateLinkDB->exists(42, BookmarkFilter::$ALL));
592 $this->assertTrue($this->privateLinkDB->exists(6, BookmarkFilter::$ALL));
593
594 $this->assertTrue($this->privateLinkDB->exists(42, BookmarkFilter::$PUBLIC));
595 $this->assertFalse($this->privateLinkDB->exists(6, BookmarkFilter::$PUBLIC));
596
597 $this->assertFalse($this->privateLinkDB->exists(42, BookmarkFilter::$PRIVATE));
598 $this->assertTrue($this->privateLinkDB->exists(6, BookmarkFilter::$PRIVATE));
599
600 $this->assertTrue($this->publicLinkDB->exists(42));
601 $this->assertFalse($this->publicLinkDB->exists(6));
602
603 $this->assertTrue($this->publicLinkDB->exists(42, BookmarkFilter::$PUBLIC));
604 $this->assertFalse($this->publicLinkDB->exists(6, BookmarkFilter::$PUBLIC));
605
606 $this->assertFalse($this->publicLinkDB->exists(42, BookmarkFilter::$PRIVATE));
607 $this->assertTrue($this->publicLinkDB->exists(6, BookmarkFilter::$PRIVATE));
608 }
609
610 /**
611 * Test initialize() method
612 */
613 public function testInitialize()
614 {
615 $dbSize = $this->privateLinkDB->count();
616 $this->privateLinkDB->initialize();
617 $this->assertEquals($dbSize + 2, $this->privateLinkDB->count());
618 $this->assertEquals(
619 'My secret stuff... - Pastebin.com',
620 $this->privateLinkDB->get(43)->getTitle()
621 );
622 $this->assertEquals(
623 'The personal, minimalist, super-fast, database free, bookmarking service',
624 $this->privateLinkDB->get(44)->getTitle()
625 );
626 }
627
628 /*
629 * The following tests have been taken from the legacy LinkDB test and adapted
630 * to make sure that nothing have been broken in the migration process.
631 * They mostly cover search/filters. Some of them might be redundant with the previous ones.
632 */
633
634 /**
635 * Attempt to instantiate a LinkDB whereas the datastore is not writable
636 *
637 * @expectedException Shaarli\Bookmark\Exception\NotWritableDataStoreException
638 * @expectedExceptionMessageRegExp #Couldn't load data from the data store file "null".*#
639 */
640 public function testConstructDatastoreNotWriteable()
641 {
642 $conf = new ConfigManager('tests/utils/config/configJson');
643 $conf->set('resource.datastore', 'null/store.db');
644 new BookmarkFileService($conf, $this->history, true);
645 }
646
647 /**
648 * The DB doesn't exist, ensure it is created with an empty datastore
649 */
650 public function testCheckDBNewLoggedIn()
651 {
652 unlink(self::$testDatastore);
653 $this->assertFileNotExists(self::$testDatastore);
654 new BookmarkFileService($this->conf, $this->history, true);
655 $this->assertFileExists(self::$testDatastore);
656
657 // ensure the correct data has been written
658 $this->assertGreaterThan(0, filesize(self::$testDatastore));
659 }
660
661 /**
662 * The DB doesn't exist, but not logged in, ensure it initialized, but the file is not written
663 */
664 public function testCheckDBNewLoggedOut()
665 {
666 unlink(self::$testDatastore);
667 $this->assertFileNotExists(self::$testDatastore);
668 $db = new \FakeBookmarkService($this->conf, $this->history, false);
669 $this->assertFileNotExists(self::$testDatastore);
670 $this->assertInstanceOf(BookmarkArray::class, $db->getBookmarks());
671 $this->assertCount(0, $db->getBookmarks());
672 }
673
674 /**
675 * Load public bookmarks from the DB
676 */
677 public function testReadPublicDB()
678 {
679 $this->assertEquals(
680 $this->refDB->countPublicLinks(),
681 $this->publicLinkDB->count()
682 );
683 }
684
685 /**
686 * Load public and private bookmarks from the DB
687 */
688 public function testReadPrivateDB()
689 {
690 $this->assertEquals(
691 $this->refDB->countLinks(),
692 $this->privateLinkDB->count()
693 );
694 }
695
696 /**
697 * Save the bookmarks to the DB
698 */
699 public function testSave()
700 {
701 $testDB = new BookmarkFileService($this->conf, $this->history, true);
702 $dbSize = $testDB->count();
703
704 $bookmark = new Bookmark();
705 $testDB->add($bookmark);
706
707 $testDB = new BookmarkFileService($this->conf, $this->history, true);
708 $this->assertEquals($dbSize + 1, $testDB->count());
709 }
710
711 /**
712 * Count existing bookmarks - public bookmarks hidden
713 */
714 public function testCountHiddenPublic()
715 {
716 $this->conf->set('privacy.hide_public_links', true);
717 $linkDB = new BookmarkFileService($this->conf, $this->history, false);
718
719 $this->assertEquals(0, $linkDB->count());
720 }
721
722 /**
723 * List the days for which bookmarks have been posted
724 */
725 public function testDays()
726 {
727 $this->assertEquals(
728 ['20100309', '20100310', '20121206', '20121207', '20130614', '20150310'],
729 $this->publicLinkDB->days()
730 );
731
732 $this->assertEquals(
733 ['20100309', '20100310', '20121206', '20121207', '20130614', '20141125', '20150310'],
734 $this->privateLinkDB->days()
735 );
736 }
737
738 /**
739 * The URL corresponds to an existing entry in the DB
740 */
741 public function testGetKnownLinkFromURL()
742 {
743 $link = $this->publicLinkDB->findByUrl('http://mediagoblin.org/');
744
745 $this->assertNotEquals(false, $link);
746 $this->assertContains(
747 'A free software media publishing platform',
748 $link->getDescription()
749 );
750 }
751
752 /**
753 * The URL is not in the DB
754 */
755 public function testGetUnknownLinkFromURL()
756 {
757 $this->assertEquals(
758 false,
759 $this->publicLinkDB->findByUrl('http://dev.null')
760 );
761 }
762
763 /**
764 * Lists all tags
765 */
766 public function testAllTags()
767 {
768 $this->assertEquals(
769 [
770 'web' => 3,
771 'cartoon' => 2,
772 'gnu' => 2,
773 'dev' => 1,
774 'samba' => 1,
775 'media' => 1,
776 'software' => 1,
777 'stallman' => 1,
778 'free' => 1,
779 '-exclude' => 1,
780 'hashtag' => 2,
781 // The DB contains a link with `sTuff` and another one with `stuff` tag.
782 // They need to be grouped with the first case found - order by date DESC: `sTuff`.
783 'sTuff' => 2,
784 'ut' => 1,
785 ],
786 $this->publicLinkDB->bookmarksCountPerTag()
787 );
788
789 $this->assertEquals(
790 [
791 'web' => 4,
792 'cartoon' => 3,
793 'gnu' => 2,
794 'dev' => 2,
795 'samba' => 1,
796 'media' => 1,
797 'software' => 1,
798 'stallman' => 1,
799 'free' => 1,
800 'html' => 1,
801 'w3c' => 1,
802 'css' => 1,
803 'Mercurial' => 1,
804 'sTuff' => 2,
805 '-exclude' => 1,
806 '.hidden' => 1,
807 'hashtag' => 2,
808 'tag1' => 1,
809 'tag2' => 1,
810 'tag3' => 1,
811 'tag4' => 1,
812 'ut' => 1,
813 ],
814 $this->privateLinkDB->bookmarksCountPerTag()
815 );
816 $this->assertEquals(
817 [
818 'web' => 4,
819 'cartoon' => 2,
820 'gnu' => 1,
821 'dev' => 1,
822 'samba' => 1,
823 'media' => 1,
824 'html' => 1,
825 'w3c' => 1,
826 'css' => 1,
827 'Mercurial' => 1,
828 '.hidden' => 1,
829 'hashtag' => 1,
830 ],
831 $this->privateLinkDB->bookmarksCountPerTag(['web'])
832 );
833 $this->assertEquals(
834 [
835 'web' => 1,
836 'html' => 1,
837 'w3c' => 1,
838 'css' => 1,
839 'Mercurial' => 1,
840 ],
841 $this->privateLinkDB->bookmarksCountPerTag(['web'], 'private')
842 );
843 }
844
845 /**
846 * Test filter with string.
847 */
848 public function testFilterString()
849 {
850 $tags = 'dev cartoon';
851 $request = ['searchtags' => $tags];
852 $this->assertEquals(
853 2,
854 count($this->privateLinkDB->search($request, null, true))
855 );
856 }
857
858 /**
859 * Test filter with array.
860 */
861 public function testFilterArray()
862 {
863 $tags = ['dev', 'cartoon'];
864 $request = ['searchtags' => $tags];
865 $this->assertEquals(
866 2,
867 count($this->privateLinkDB->search($request, null, true))
868 );
869 }
870
871 /**
872 * Test hidden tags feature:
873 * tags starting with a dot '.' are only visible when logged in.
874 */
875 public function testHiddenTags()
876 {
877 $tags = '.hidden';
878 $request = ['searchtags' => $tags];
879 $this->assertEquals(
880 1,
881 count($this->privateLinkDB->search($request, 'all', true))
882 );
883
884 $this->assertEquals(
885 0,
886 count($this->publicLinkDB->search($request, 'public', true))
887 );
888 }
889
890 /**
891 * Test filterHash() with a valid smallhash.
892 */
893 public function testFilterHashValid()
894 {
895 $request = smallHash('20150310_114651');
896 $this->assertEquals(
897 1,
898 count($this->publicLinkDB->findByHash($request))
899 );
900 $request = smallHash('20150310_114633' . 8);
901 $this->assertEquals(
902 1,
903 count($this->publicLinkDB->findByHash($request))
904 );
905 }
906
907 /**
908 * Test filterHash() with an invalid smallhash.
909 *
910 * @expectedException \Shaarli\Bookmark\Exception\BookmarkNotFoundException
911 */
912 public function testFilterHashInValid1()
913 {
914 $request = 'blabla';
915 $this->publicLinkDB->findByHash($request);
916 }
917
918 /**
919 * Test filterHash() with an empty smallhash.
920 *
921 * @expectedException \Shaarli\Bookmark\Exception\BookmarkNotFoundException
922 */
923 public function testFilterHashInValid()
924 {
925 $this->publicLinkDB->findByHash('');
926 }
927
928 /**
929 * Test linksCountPerTag all tags without filter.
930 * Equal occurrences should be sorted alphabetically.
931 */
932 public function testCountLinkPerTagAllNoFilter()
933 {
934 $expected = [
935 'web' => 4,
936 'cartoon' => 3,
937 'dev' => 2,
938 'gnu' => 2,
939 'hashtag' => 2,
940 'sTuff' => 2,
941 '-exclude' => 1,
942 '.hidden' => 1,
943 'Mercurial' => 1,
944 'css' => 1,
945 'free' => 1,
946 'html' => 1,
947 'media' => 1,
948 'samba' => 1,
949 'software' => 1,
950 'stallman' => 1,
951 'tag1' => 1,
952 'tag2' => 1,
953 'tag3' => 1,
954 'tag4' => 1,
955 'ut' => 1,
956 'w3c' => 1,
957 ];
958 $tags = $this->privateLinkDB->bookmarksCountPerTag();
959
960 $this->assertEquals($expected, $tags, var_export($tags, true));
961 }
962
963 /**
964 * Test linksCountPerTag all tags with filter.
965 * Equal occurrences should be sorted alphabetically.
966 */
967 public function testCountLinkPerTagAllWithFilter()
968 {
969 $expected = [
970 'gnu' => 2,
971 'hashtag' => 2,
972 '-exclude' => 1,
973 '.hidden' => 1,
974 'free' => 1,
975 'media' => 1,
976 'software' => 1,
977 'stallman' => 1,
978 'stuff' => 1,
979 'web' => 1,
980 ];
981 $tags = $this->privateLinkDB->bookmarksCountPerTag(['gnu']);
982
983 $this->assertEquals($expected, $tags, var_export($tags, true));
984 }
985
986 /**
987 * Test linksCountPerTag public tags with filter.
988 * Equal occurrences should be sorted alphabetically.
989 */
990 public function testCountLinkPerTagPublicWithFilter()
991 {
992 $expected = [
993 'gnu' => 2,
994 'hashtag' => 2,
995 '-exclude' => 1,
996 '.hidden' => 1,
997 'free' => 1,
998 'media' => 1,
999 'software' => 1,
1000 'stallman' => 1,
1001 'stuff' => 1,
1002 'web' => 1,
1003 ];
1004 $tags = $this->privateLinkDB->bookmarksCountPerTag(['gnu'], 'public');
1005
1006 $this->assertEquals($expected, $tags, var_export($tags, true));
1007 }
1008
1009 /**
1010 * Test linksCountPerTag public tags with filter.
1011 * Equal occurrences should be sorted alphabetically.
1012 */
1013 public function testCountLinkPerTagPrivateWithFilter()
1014 {
1015 $expected = [
1016 'cartoon' => 1,
1017 'dev' => 1,
1018 'tag1' => 1,
1019 'tag2' => 1,
1020 'tag3' => 1,
1021 'tag4' => 1,
1022 ];
1023 $tags = $this->privateLinkDB->bookmarksCountPerTag(['dev'], 'private');
1024
1025 $this->assertEquals($expected, $tags, var_export($tags, true));
1026 }
1027
1028 /**
1029 * Allows to test LinkDB's private methods
1030 *
1031 * @see
1032 * https://sebastian-bergmann.de/archives/881-Testing-Your-Privates.html
1033 * http://stackoverflow.com/a/2798203
1034 */
1035 protected static function getMethod($name)
1036 {
1037 $class = new ReflectionClass('Shaarli\Bookmark\BookmarkFileService');
1038 $method = $class->getMethod($name);
1039 $method->setAccessible(true);
1040 return $method;
1041 }
1042}
diff --git a/tests/bookmark/BookmarkFilterTest.php b/tests/bookmark/BookmarkFilterTest.php
new file mode 100644
index 00000000..d4c71cb9
--- /dev/null
+++ b/tests/bookmark/BookmarkFilterTest.php
@@ -0,0 +1,514 @@
1<?php
2
3namespace Shaarli\Bookmark;
4
5use Exception;
6use PHPUnit\Framework\TestCase;
7use ReferenceLinkDB;
8use Shaarli\Config\ConfigManager;
9use Shaarli\Formatter\FormatterFactory;
10use Shaarli\History;
11
12/**
13 * Class BookmarkFilterTest.
14 */
15class BookmarkFilterTest extends TestCase
16{
17 /**
18 * @var string Test datastore path.
19 */
20 protected static $testDatastore = 'sandbox/datastore.php';
21 /**
22 * @var BookmarkFilter instance.
23 */
24 protected static $linkFilter;
25
26 /**
27 * @var ReferenceLinkDB instance
28 */
29 protected static $refDB;
30
31 /**
32 * @var BookmarkFileService instance
33 */
34 protected static $bookmarkService;
35
36 /**
37 * Instantiate linkFilter with ReferenceLinkDB data.
38 */
39 public static function setUpBeforeClass()
40 {
41 $conf = new ConfigManager('tests/utils/config/configJson');
42 $conf->set('resource.datastore', self::$testDatastore);
43 self::$refDB = new \ReferenceLinkDB();
44 self::$refDB->write(self::$testDatastore);
45 $history = new History('sandbox/history.php');
46 self::$bookmarkService = new \FakeBookmarkService($conf, $history, true);
47 self::$linkFilter = new BookmarkFilter(self::$bookmarkService->getBookmarks());
48 }
49
50 /**
51 * Blank filter.
52 */
53 public function testFilter()
54 {
55 $this->assertEquals(
56 self::$refDB->countLinks(),
57 count(self::$linkFilter->filter('', ''))
58 );
59
60 $this->assertEquals(
61 self::$refDB->countLinks(),
62 count(self::$linkFilter->filter('', '', 'all'))
63 );
64
65 $this->assertEquals(
66 self::$refDB->countLinks(),
67 count(self::$linkFilter->filter('', '', 'randomstr'))
68 );
69
70 // Private only.
71 $this->assertEquals(
72 self::$refDB->countPrivateLinks(),
73 count(self::$linkFilter->filter('', '', false, 'private'))
74 );
75
76 // Public only.
77 $this->assertEquals(
78 self::$refDB->countPublicLinks(),
79 count(self::$linkFilter->filter('', '', false, 'public'))
80 );
81
82 $this->assertEquals(
83 ReferenceLinkDB::$NB_LINKS_TOTAL,
84 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TAG, ''))
85 );
86
87 $this->assertEquals(
88 self::$refDB->countUntaggedLinks(),
89 count(
90 self::$linkFilter->filter(
91 BookmarkFilter::$FILTER_TAG,
92 /*$request=*/
93 '',
94 /*$casesensitive=*/
95 false,
96 /*$visibility=*/
97 'all',
98 /*$untaggedonly=*/
99 true
100 )
101 )
102 );
103
104 $this->assertEquals(
105 ReferenceLinkDB::$NB_LINKS_TOTAL,
106 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TEXT, ''))
107 );
108 }
109
110 /**
111 * Filter bookmarks using a tag
112 */
113 public function testFilterOneTag()
114 {
115 $this->assertEquals(
116 4,
117 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TAG, 'web', false))
118 );
119
120 $this->assertEquals(
121 4,
122 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TAG, 'web', false, 'all'))
123 );
124
125 $this->assertEquals(
126 4,
127 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TAG, 'web', false, 'default-blabla'))
128 );
129
130 // Private only.
131 $this->assertEquals(
132 1,
133 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TAG, 'web', false, 'private'))
134 );
135
136 // Public only.
137 $this->assertEquals(
138 3,
139 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TAG, 'web', false, 'public'))
140 );
141 }
142
143 /**
144 * Filter bookmarks using a tag - case-sensitive
145 */
146 public function testFilterCaseSensitiveTag()
147 {
148 $this->assertEquals(
149 0,
150 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TAG, 'mercurial', true))
151 );
152
153 $this->assertEquals(
154 1,
155 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TAG, 'Mercurial', true))
156 );
157 }
158
159 /**
160 * Filter bookmarks using a tag combination
161 */
162 public function testFilterMultipleTags()
163 {
164 $this->assertEquals(
165 2,
166 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TAG, 'dev cartoon', false))
167 );
168 }
169
170 /**
171 * Filter bookmarks using a non-existent tag
172 */
173 public function testFilterUnknownTag()
174 {
175 $this->assertEquals(
176 0,
177 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TAG, 'null', false))
178 );
179 }
180
181 /**
182 * Return bookmarks for a given day
183 */
184 public function testFilterDay()
185 {
186 $this->assertEquals(
187 4,
188 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_DAY, '20121206'))
189 );
190 }
191
192 /**
193 * 404 - day not found
194 */
195 public function testFilterUnknownDay()
196 {
197 $this->assertEquals(
198 0,
199 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_DAY, '19700101'))
200 );
201 }
202
203 /**
204 * Use an invalid date format
205 * @expectedException Exception
206 * @expectedExceptionMessageRegExp /Invalid date format/
207 */
208 public function testFilterInvalidDayWithChars()
209 {
210 self::$linkFilter->filter(BookmarkFilter::$FILTER_DAY, 'Rainy day, dream away');
211 }
212
213 /**
214 * Use an invalid date format
215 * @expectedException Exception
216 * @expectedExceptionMessageRegExp /Invalid date format/
217 */
218 public function testFilterInvalidDayDigits()
219 {
220 self::$linkFilter->filter(BookmarkFilter::$FILTER_DAY, '20');
221 }
222
223 /**
224 * Retrieve a link entry with its hash
225 */
226 public function testFilterSmallHash()
227 {
228 $links = self::$linkFilter->filter(BookmarkFilter::$FILTER_HASH, 'IuWvgA');
229
230 $this->assertEquals(
231 1,
232 count($links)
233 );
234
235 $this->assertEquals(
236 'MediaGoblin',
237 $links[7]->getTitle()
238 );
239 }
240
241 /**
242 * No link for this hash
243 *
244 * @expectedException \Shaarli\Bookmark\Exception\BookmarkNotFoundException
245 */
246 public function testFilterUnknownSmallHash()
247 {
248 self::$linkFilter->filter(BookmarkFilter::$FILTER_HASH, 'Iblaah');
249 }
250
251 /**
252 * Full-text search - no result found.
253 */
254 public function testFilterFullTextNoResult()
255 {
256 $this->assertEquals(
257 0,
258 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TEXT, 'azertyuiop'))
259 );
260 }
261
262 /**
263 * Full-text search - result from a link's URL
264 */
265 public function testFilterFullTextURL()
266 {
267 $this->assertEquals(
268 2,
269 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TEXT, 'ars.userfriendly.org'))
270 );
271
272 $this->assertEquals(
273 2,
274 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TEXT, 'ars org'))
275 );
276 }
277
278 /**
279 * Full-text search - result from a link's title only
280 */
281 public function testFilterFullTextTitle()
282 {
283 // use miscellaneous cases
284 $this->assertEquals(
285 2,
286 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TEXT, 'userfriendly -'))
287 );
288 $this->assertEquals(
289 2,
290 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TEXT, 'UserFriendly -'))
291 );
292 $this->assertEquals(
293 2,
294 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TEXT, 'uSeRFrIendlY -'))
295 );
296
297 // use miscellaneous case and offset
298 $this->assertEquals(
299 2,
300 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TEXT, 'RFrIendL'))
301 );
302 }
303
304 /**
305 * Full-text search - result from the link's description only
306 */
307 public function testFilterFullTextDescription()
308 {
309 $this->assertEquals(
310 1,
311 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TEXT, 'publishing media'))
312 );
313
314 $this->assertEquals(
315 1,
316 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TEXT, 'mercurial w3c'))
317 );
318
319 $this->assertEquals(
320 3,
321 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TEXT, '"free software"'))
322 );
323 }
324
325 /**
326 * Full-text search - result from the link's tags only
327 */
328 public function testFilterFullTextTags()
329 {
330 $this->assertEquals(
331 6,
332 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TEXT, 'web'))
333 );
334
335 $this->assertEquals(
336 6,
337 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TEXT, 'web', 'all'))
338 );
339
340 $this->assertEquals(
341 6,
342 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TEXT, 'web', 'bla'))
343 );
344
345 // Private only.
346 $this->assertEquals(
347 1,
348 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TEXT, 'web', false, 'private'))
349 );
350
351 // Public only.
352 $this->assertEquals(
353 5,
354 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TEXT, 'web', false, 'public'))
355 );
356 }
357
358 /**
359 * Full-text search - result set from mixed sources
360 */
361 public function testFilterFullTextMixed()
362 {
363 $this->assertEquals(
364 3,
365 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TEXT, 'free software'))
366 );
367 }
368
369 /**
370 * Full-text search - test exclusion with '-'.
371 */
372 public function testExcludeSearch()
373 {
374 $this->assertEquals(
375 1,
376 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TEXT, 'free -gnu'))
377 );
378
379 $this->assertEquals(
380 ReferenceLinkDB::$NB_LINKS_TOTAL - 1,
381 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TEXT, '-revolution'))
382 );
383 }
384
385 /**
386 * Full-text search - test AND, exact terms and exclusion combined, across fields.
387 */
388 public function testMultiSearch()
389 {
390 $this->assertEquals(
391 2,
392 count(self::$linkFilter->filter(
393 BookmarkFilter::$FILTER_TEXT,
394 '"Free Software " stallman "read this" @website stuff'
395 ))
396 );
397
398 $this->assertEquals(
399 1,
400 count(self::$linkFilter->filter(
401 BookmarkFilter::$FILTER_TEXT,
402 '"free software " stallman "read this" -beard @website stuff'
403 ))
404 );
405 }
406
407 /**
408 * Full-text search - make sure that exact search won't work across fields.
409 */
410 public function testSearchExactTermMultiFieldsKo()
411 {
412 $this->assertEquals(
413 0,
414 count(self::$linkFilter->filter(
415 BookmarkFilter::$FILTER_TEXT,
416 '"designer naming"'
417 ))
418 );
419
420 $this->assertEquals(
421 0,
422 count(self::$linkFilter->filter(
423 BookmarkFilter::$FILTER_TEXT,
424 '"designernaming"'
425 ))
426 );
427 }
428
429 /**
430 * Tag search with exclusion.
431 */
432 public function testTagFilterWithExclusion()
433 {
434 $this->assertEquals(
435 1,
436 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TAG, 'gnu -free'))
437 );
438
439 $this->assertEquals(
440 ReferenceLinkDB::$NB_LINKS_TOTAL - 1,
441 count(self::$linkFilter->filter(BookmarkFilter::$FILTER_TAG, '-free'))
442 );
443 }
444
445 /**
446 * Test crossed search (terms + tags).
447 */
448 public function testFilterCrossedSearch()
449 {
450 $terms = '"Free Software " stallman "read this" @website stuff';
451 $tags = 'free';
452 $this->assertEquals(
453 1,
454 count(self::$linkFilter->filter(
455 BookmarkFilter::$FILTER_TAG | BookmarkFilter::$FILTER_TEXT,
456 array($tags, $terms)
457 ))
458 );
459 $this->assertEquals(
460 2,
461 count(self::$linkFilter->filter(
462 BookmarkFilter::$FILTER_TAG | BookmarkFilter::$FILTER_TEXT,
463 array('', $terms)
464 ))
465 );
466 $this->assertEquals(
467 1,
468 count(self::$linkFilter->filter(
469 BookmarkFilter::$FILTER_TAG | BookmarkFilter::$FILTER_TEXT,
470 array(false, 'PSR-2')
471 ))
472 );
473 $this->assertEquals(
474 1,
475 count(self::$linkFilter->filter(
476 BookmarkFilter::$FILTER_TAG | BookmarkFilter::$FILTER_TEXT,
477 array($tags, '')
478 ))
479 );
480 $this->assertEquals(
481 ReferenceLinkDB::$NB_LINKS_TOTAL,
482 count(self::$linkFilter->filter(
483 BookmarkFilter::$FILTER_TAG | BookmarkFilter::$FILTER_TEXT,
484 ''
485 ))
486 );
487 }
488
489 /**
490 * Filter bookmarks by #hashtag.
491 */
492 public function testFilterByHashtag()
493 {
494 $hashtag = 'hashtag';
495 $this->assertEquals(
496 3,
497 count(self::$linkFilter->filter(
498 BookmarkFilter::$FILTER_TAG,
499 $hashtag
500 ))
501 );
502
503 $hashtag = 'private';
504 $this->assertEquals(
505 1,
506 count(self::$linkFilter->filter(
507 BookmarkFilter::$FILTER_TAG,
508 $hashtag,
509 false,
510 'private'
511 ))
512 );
513 }
514}
diff --git a/tests/bookmark/BookmarkInitializerTest.php b/tests/bookmark/BookmarkInitializerTest.php
new file mode 100644
index 00000000..d23eb069
--- /dev/null
+++ b/tests/bookmark/BookmarkInitializerTest.php
@@ -0,0 +1,120 @@
1<?php
2
3namespace Shaarli\Bookmark;
4
5use PHPUnit\Framework\TestCase;
6use ReferenceLinkDB;
7use Shaarli\Config\ConfigManager;
8use Shaarli\History;
9
10/**
11 * Class BookmarkInitializerTest
12 * @package Shaarli\Bookmark
13 */
14class BookmarkInitializerTest extends TestCase
15{
16 /** @var string Path of test data store */
17 protected static $testDatastore = 'sandbox/datastore.php';
18
19 /** @var string Path of test config file */
20 protected static $testConf = 'sandbox/config';
21
22 /**
23 * @var ConfigManager instance.
24 */
25 protected $conf;
26
27 /**
28 * @var History instance.
29 */
30 protected $history;
31
32 /** @var BookmarkServiceInterface instance */
33 protected $bookmarkService;
34
35 /** @var BookmarkInitializer instance */
36 protected $initializer;
37
38 /**
39 * Initialize an empty BookmarkFileService
40 */
41 public function setUp()
42 {
43 if (file_exists(self::$testDatastore)) {
44 unlink(self::$testDatastore);
45 }
46
47 copy('tests/utils/config/configJson.json.php', self::$testConf .'.json.php');
48 $this->conf = new ConfigManager(self::$testConf);
49 $this->conf->set('resource.datastore', self::$testDatastore);
50 $this->history = new History('sandbox/history.php');
51 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true);
52
53 $this->initializer = new BookmarkInitializer($this->bookmarkService);
54 }
55
56 /**
57 * Test initialize() with an empty data store.
58 */
59 public function testInitializeEmptyDataStore()
60 {
61 $refDB = new \ReferenceLinkDB();
62 $refDB->write(self::$testDatastore);
63 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true);
64 $this->initializer = new BookmarkInitializer($this->bookmarkService);
65
66 $this->initializer->initialize();
67
68 $this->assertEquals($refDB->countLinks() + 2, $this->bookmarkService->count());
69 $bookmark = $this->bookmarkService->get(43);
70 $this->assertEquals(43, $bookmark->getId());
71 $this->assertEquals('My secret stuff... - Pastebin.com', $bookmark->getTitle());
72 $this->assertTrue($bookmark->isPrivate());
73
74 $bookmark = $this->bookmarkService->get(44);
75 $this->assertEquals(44, $bookmark->getId());
76 $this->assertEquals(
77 'The personal, minimalist, super-fast, database free, bookmarking service',
78 $bookmark->getTitle()
79 );
80 $this->assertFalse($bookmark->isPrivate());
81
82 // Reload from file
83 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true);
84 $this->assertEquals($refDB->countLinks() + 2, $this->bookmarkService->count());
85 $bookmark = $this->bookmarkService->get(43);
86 $this->assertEquals(43, $bookmark->getId());
87 $this->assertEquals('My secret stuff... - Pastebin.com', $bookmark->getTitle());
88 $this->assertTrue($bookmark->isPrivate());
89
90 $bookmark = $this->bookmarkService->get(44);
91 $this->assertEquals(44, $bookmark->getId());
92 $this->assertEquals(
93 'The personal, minimalist, super-fast, database free, bookmarking service',
94 $bookmark->getTitle()
95 );
96 $this->assertFalse($bookmark->isPrivate());
97 }
98
99 /**
100 * Test initialize() with a data store containing bookmarks.
101 */
102 public function testInitializeNotEmptyDataStore()
103 {
104 $this->initializer->initialize();
105
106 $this->assertEquals(2, $this->bookmarkService->count());
107 $bookmark = $this->bookmarkService->get(0);
108 $this->assertEquals(0, $bookmark->getId());
109 $this->assertEquals('My secret stuff... - Pastebin.com', $bookmark->getTitle());
110 $this->assertTrue($bookmark->isPrivate());
111
112 $bookmark = $this->bookmarkService->get(1);
113 $this->assertEquals(1, $bookmark->getId());
114 $this->assertEquals(
115 'The personal, minimalist, super-fast, database free, bookmarking service',
116 $bookmark->getTitle()
117 );
118 $this->assertFalse($bookmark->isPrivate());
119 }
120}
diff --git a/tests/bookmark/BookmarkTest.php b/tests/bookmark/BookmarkTest.php
new file mode 100644
index 00000000..9a3bbbfc
--- /dev/null
+++ b/tests/bookmark/BookmarkTest.php
@@ -0,0 +1,388 @@
1<?php
2
3namespace Shaarli\Bookmark;
4
5use PHPUnit\Framework\TestCase;
6use Shaarli\Bookmark\Exception\InvalidBookmarkException;
7
8/**
9 * Class BookmarkTest
10 */
11class BookmarkTest extends TestCase
12{
13 /**
14 * Test fromArray() with a link with full data
15 */
16 public function testFromArrayFull()
17 {
18 $data = [
19 'id' => 1,
20 'shorturl' => 'abc',
21 'url' => 'https://domain.tld/oof.html?param=value#anchor',
22 'title' => 'This is an array link',
23 'description' => 'HTML desc<br><p>hi!</p>',
24 'thumbnail' => 'https://domain.tld/pic.png',
25 'sticky' => true,
26 'created' => new \DateTime('-1 minute'),
27 'tags' => ['tag1', 'tag2', 'chair'],
28 'updated' => new \DateTime(),
29 'private' => true,
30 ];
31
32 $bookmark = (new Bookmark())->fromArray($data);
33 $this->assertEquals($data['id'], $bookmark->getId());
34 $this->assertEquals($data['shorturl'], $bookmark->getShortUrl());
35 $this->assertEquals($data['url'], $bookmark->getUrl());
36 $this->assertEquals($data['title'], $bookmark->getTitle());
37 $this->assertEquals($data['description'], $bookmark->getDescription());
38 $this->assertEquals($data['thumbnail'], $bookmark->getThumbnail());
39 $this->assertEquals($data['sticky'], $bookmark->isSticky());
40 $this->assertEquals($data['created'], $bookmark->getCreated());
41 $this->assertEquals($data['tags'], $bookmark->getTags());
42 $this->assertEquals('tag1 tag2 chair', $bookmark->getTagsString());
43 $this->assertEquals($data['updated'], $bookmark->getUpdated());
44 $this->assertEquals($data['private'], $bookmark->isPrivate());
45 $this->assertFalse($bookmark->isNote());
46 }
47
48 /**
49 * Test fromArray() with a link with minimal data.
50 * Note that I use null values everywhere but this should not happen in the real world.
51 */
52 public function testFromArrayMinimal()
53 {
54 $data = [
55 'id' => null,
56 'shorturl' => null,
57 'url' => null,
58 'title' => null,
59 'description' => null,
60 'created' => null,
61 'tags' => null,
62 'private' => null,
63 ];
64
65 $bookmark = (new Bookmark())->fromArray($data);
66 $this->assertNull($bookmark->getId());
67 $this->assertNull($bookmark->getShortUrl());
68 $this->assertNull($bookmark->getUrl());
69 $this->assertNull($bookmark->getTitle());
70 $this->assertEquals('', $bookmark->getDescription());
71 $this->assertNull($bookmark->getCreated());
72 $this->assertEquals([], $bookmark->getTags());
73 $this->assertEquals('', $bookmark->getTagsString());
74 $this->assertNull($bookmark->getUpdated());
75 $this->assertFalse($bookmark->getThumbnail());
76 $this->assertFalse($bookmark->isSticky());
77 $this->assertFalse($bookmark->isPrivate());
78 $this->assertTrue($bookmark->isNote());
79 }
80
81 /**
82 * Test validate() with a valid minimal bookmark
83 */
84 public function testValidateValidFullBookmark()
85 {
86 $bookmark = new Bookmark();
87 $bookmark->setId(2);
88 $bookmark->setShortUrl('abc');
89 $bookmark->setCreated($date = \DateTime::createFromFormat('Ymd_His', '20190514_200102'));
90 $bookmark->setUpdated($dateUp = \DateTime::createFromFormat('Ymd_His', '20190514_210203'));
91 $bookmark->setUrl($url = 'https://domain.tld/oof.html?param=value#anchor');
92 $bookmark->setTitle($title = 'This is an array link');
93 $bookmark->setDescription($desc = 'HTML desc<br><p>hi!</p>');
94 $bookmark->setTags($tags = ['tag1', 'tag2', 'chair']);
95 $bookmark->setThumbnail($thumb = 'https://domain.tld/pic.png');
96 $bookmark->setPrivate(true);
97 $bookmark->validate();
98
99 $this->assertEquals(2, $bookmark->getId());
100 $this->assertEquals('abc', $bookmark->getShortUrl());
101 $this->assertEquals($date, $bookmark->getCreated());
102 $this->assertEquals($dateUp, $bookmark->getUpdated());
103 $this->assertEquals($url, $bookmark->getUrl());
104 $this->assertEquals($title, $bookmark->getTitle());
105 $this->assertEquals($desc, $bookmark->getDescription());
106 $this->assertEquals($tags, $bookmark->getTags());
107 $this->assertEquals(implode(' ', $tags), $bookmark->getTagsString());
108 $this->assertEquals($thumb, $bookmark->getThumbnail());
109 $this->assertTrue($bookmark->isPrivate());
110 $this->assertFalse($bookmark->isNote());
111 }
112
113 /**
114 * Test validate() with a valid minimal bookmark
115 */
116 public function testValidateValidMinimalBookmark()
117 {
118 $bookmark = new Bookmark();
119 $bookmark->setId(1);
120 $bookmark->setShortUrl('abc');
121 $bookmark->setCreated($date = \DateTime::createFromFormat('Ymd_His', '20190514_200102'));
122 $bookmark->validate();
123
124 $this->assertEquals(1, $bookmark->getId());
125 $this->assertEquals('abc', $bookmark->getShortUrl());
126 $this->assertEquals($date, $bookmark->getCreated());
127 $this->assertEquals('?abc', $bookmark->getUrl());
128 $this->assertEquals('?abc', $bookmark->getTitle());
129 $this->assertEquals('', $bookmark->getDescription());
130 $this->assertEquals([], $bookmark->getTags());
131 $this->assertEquals('', $bookmark->getTagsString());
132 $this->assertFalse($bookmark->getThumbnail());
133 $this->assertFalse($bookmark->isPrivate());
134 $this->assertTrue($bookmark->isNote());
135 $this->assertNull($bookmark->getUpdated());
136 }
137
138 /**
139 * Test validate() with a a bookmark without ID.
140 */
141 public function testValidateNotValidNoId()
142 {
143 $bookmark = new Bookmark();
144 $bookmark->setShortUrl('abc');
145 $bookmark->setCreated(\DateTime::createFromFormat('Ymd_His', '20190514_200102'));
146 $exception = null;
147 try {
148 $bookmark->validate();
149 } catch (InvalidBookmarkException $e) {
150 $exception = $e;
151 }
152 $this->assertNotNull($exception);
153 $this->assertContains('- ID: '. PHP_EOL, $exception->getMessage());
154 }
155
156 /**
157 * Test validate() with a a bookmark with a non integer ID.
158 */
159 public function testValidateNotValidStringId()
160 {
161 $bookmark = new Bookmark();
162 $bookmark->setId('str');
163 $bookmark->setShortUrl('abc');
164 $bookmark->setCreated(\DateTime::createFromFormat('Ymd_His', '20190514_200102'));
165 $exception = null;
166 try {
167 $bookmark->validate();
168 } catch (InvalidBookmarkException $e) {
169 $exception = $e;
170 }
171 $this->assertNotNull($exception);
172 $this->assertContains('- ID: str'. PHP_EOL, $exception->getMessage());
173 }
174
175 /**
176 * Test validate() with a a bookmark without short url.
177 */
178 public function testValidateNotValidNoShortUrl()
179 {
180 $bookmark = new Bookmark();
181 $bookmark->setId(1);
182 $bookmark->setCreated(\DateTime::createFromFormat('Ymd_His', '20190514_200102'));
183 $bookmark->setShortUrl(null);
184 $exception = null;
185 try {
186 $bookmark->validate();
187 } catch (InvalidBookmarkException $e) {
188 $exception = $e;
189 }
190 $this->assertNotNull($exception);
191 $this->assertContains('- ShortUrl: '. PHP_EOL, $exception->getMessage());
192 }
193
194 /**
195 * Test validate() with a a bookmark without created datetime.
196 */
197 public function testValidateNotValidNoCreated()
198 {
199 $bookmark = new Bookmark();
200 $bookmark->setId(1);
201 $bookmark->setShortUrl('abc');
202 $bookmark->setCreated(null);
203 $exception = null;
204 try {
205 $bookmark->validate();
206 } catch (InvalidBookmarkException $e) {
207 $exception = $e;
208 }
209 $this->assertNotNull($exception);
210 $this->assertContains('- Created: '. PHP_EOL, $exception->getMessage());
211 }
212
213 /**
214 * Test validate() with a a bookmark with a bad created datetime.
215 */
216 public function testValidateNotValidBadCreated()
217 {
218 $bookmark = new Bookmark();
219 $bookmark->setId(1);
220 $bookmark->setShortUrl('abc');
221 $bookmark->setCreated('hi!');
222 $exception = null;
223 try {
224 $bookmark->validate();
225 } catch (InvalidBookmarkException $e) {
226 $exception = $e;
227 }
228 $this->assertNotNull($exception);
229 $this->assertContains('- Created: Not a DateTime object'. PHP_EOL, $exception->getMessage());
230 }
231
232 /**
233 * Test setId() and make sure that default fields are generated.
234 */
235 public function testSetIdEmptyGeneratedFields()
236 {
237 $bookmark = new Bookmark();
238 $bookmark->setId(2);
239
240 $this->assertEquals(2, $bookmark->getId());
241 $this->assertRegExp('/[\w\-]{6}/', $bookmark->getShortUrl());
242 $this->assertTrue(new \DateTime('5 seconds ago') < $bookmark->getCreated());
243 }
244
245 /**
246 * Test setId() and with generated fields already set.
247 */
248 public function testSetIdSetGeneratedFields()
249 {
250 $bookmark = new Bookmark();
251 $bookmark->setShortUrl('abc');
252 $bookmark->setCreated($date = \DateTime::createFromFormat('Ymd_His', '20190514_200102'));
253 $bookmark->setId(2);
254
255 $this->assertEquals(2, $bookmark->getId());
256 $this->assertEquals('abc', $bookmark->getShortUrl());
257 $this->assertEquals($date, $bookmark->getCreated());
258 }
259
260 /**
261 * Test setUrl() and make sure it accepts custom protocols
262 */
263 public function testGetUrlWithValidProtocols()
264 {
265 $bookmark = new Bookmark();
266 $bookmark->setUrl($url = 'myprotocol://helloworld', ['myprotocol']);
267 $this->assertEquals($url, $bookmark->getUrl());
268
269 $bookmark->setUrl($url = 'https://helloworld.tld', ['myprotocol']);
270 $this->assertEquals($url, $bookmark->getUrl());
271 }
272
273 /**
274 * Test setUrl() and make sure it accepts custom protocols
275 */
276 public function testGetUrlWithNotValidProtocols()
277 {
278 $bookmark = new Bookmark();
279 $bookmark->setUrl('myprotocol://helloworld', []);
280 $this->assertEquals('http://helloworld', $bookmark->getUrl());
281
282 $bookmark->setUrl($url = 'https://helloworld.tld', []);
283 $this->assertEquals($url, $bookmark->getUrl());
284 }
285
286 /**
287 * Test setTagsString() with exotic data
288 */
289 public function testSetTagsString()
290 {
291 $bookmark = new Bookmark();
292
293 $str = 'tag1 tag2 tag3.tag3-2, tag4 , -tag5 ';
294 $bookmark->setTagsString($str);
295 $this->assertEquals(
296 [
297 'tag1',
298 'tag2',
299 'tag3.tag3-2',
300 'tag4',
301 'tag5',
302 ],
303 $bookmark->getTags()
304 );
305 }
306
307 /**
308 * Test setTags() with exotic data
309 */
310 public function testSetTags()
311 {
312 $bookmark = new Bookmark();
313
314 $array = [
315 'tag1 ',
316 ' tag2',
317 'tag3.tag3-2,',
318 ', tag4',
319 ', ',
320 '-tag5 ',
321 ];
322 $bookmark->setTags($array);
323 $this->assertEquals(
324 [
325 'tag1',
326 'tag2',
327 'tag3.tag3-2',
328 'tag4',
329 'tag5',
330 ],
331 $bookmark->getTags()
332 );
333 }
334
335 /**
336 * Test renameTag()
337 */
338 public function testRenameTag()
339 {
340 $bookmark = new Bookmark();
341 $bookmark->setTags(['tag1', 'tag2', 'chair']);
342 $bookmark->renameTag('chair', 'table');
343 $this->assertEquals(['tag1', 'tag2', 'table'], $bookmark->getTags());
344 $bookmark->renameTag('tag1', 'tag42');
345 $this->assertEquals(['tag42', 'tag2', 'table'], $bookmark->getTags());
346 $bookmark->renameTag('tag42', 'tag43');
347 $this->assertEquals(['tag43', 'tag2', 'table'], $bookmark->getTags());
348 $bookmark->renameTag('table', 'desk');
349 $this->assertEquals(['tag43', 'tag2', 'desk'], $bookmark->getTags());
350 }
351
352 /**
353 * Test renameTag() with a tag that is not present in the bookmark
354 */
355 public function testRenameTagNotExists()
356 {
357 $bookmark = new Bookmark();
358 $bookmark->setTags(['tag1', 'tag2', 'chair']);
359 $bookmark->renameTag('nope', 'table');
360 $this->assertEquals(['tag1', 'tag2', 'chair'], $bookmark->getTags());
361 }
362
363 /**
364 * Test deleteTag()
365 */
366 public function testDeleteTag()
367 {
368 $bookmark = new Bookmark();
369 $bookmark->setTags(['tag1', 'tag2', 'chair']);
370 $bookmark->deleteTag('chair');
371 $this->assertEquals(['tag1', 'tag2'], $bookmark->getTags());
372 $bookmark->deleteTag('tag1');
373 $this->assertEquals(['tag2'], $bookmark->getTags());
374 $bookmark->deleteTag('tag2');
375 $this->assertEquals([], $bookmark->getTags());
376 }
377
378 /**
379 * Test deleteTag() with a tag that is not present in the bookmark
380 */
381 public function testDeleteTagNotExists()
382 {
383 $bookmark = new Bookmark();
384 $bookmark->setTags(['tag1', 'tag2', 'chair']);
385 $bookmark->deleteTag('nope');
386 $this->assertEquals(['tag1', 'tag2', 'chair'], $bookmark->getTags());
387 }
388}
diff --git a/tests/bookmark/LinkUtilsTest.php b/tests/bookmark/LinkUtilsTest.php
index 78cb8f2a..591976f2 100644
--- a/tests/bookmark/LinkUtilsTest.php
+++ b/tests/bookmark/LinkUtilsTest.php
@@ -389,15 +389,6 @@ class LinkUtilsTest extends TestCase
389 } 389 }
390 390
391 /** 391 /**
392 * Test count_private.
393 */
394 public function testCountPrivateLinks()
395 {
396 $refDB = new ReferenceLinkDB();
397 $this->assertEquals($refDB->countPrivateLinks(), count_private($refDB->getLinks()));
398 }
399
400 /**
401 * Test text2clickable. 392 * Test text2clickable.
402 */ 393 */
403 public function testText2clickable() 394 public function testText2clickable()
diff --git a/tests/bootstrap.php b/tests/bootstrap.php
index d36d73cd..0afbcba6 100644
--- a/tests/bootstrap.php
+++ b/tests/bootstrap.php
@@ -4,3 +4,21 @@ require_once 'vendor/autoload.php';
4 4
5$conf = new \Shaarli\Config\ConfigManager('tests/utils/config/configJson'); 5$conf = new \Shaarli\Config\ConfigManager('tests/utils/config/configJson');
6new \Shaarli\Languages('en', $conf); 6new \Shaarli\Languages('en', $conf);
7
8// is_iterable is only compatible with PHP 7.1+
9if (!function_exists('is_iterable')) {
10 function is_iterable($var)
11 {
12 return is_array($var) || $var instanceof \Traversable;
13 }
14}
15
16// TODO: remove this after fixing UT
17require_once 'application/bookmark/LinkUtils.php';
18require_once 'application/Utils.php';
19require_once 'application/http/UrlUtils.php';
20require_once 'application/http/HttpUtils.php';
21require_once 'application/feed/Cache.php';
22require_once 'tests/utils/ReferenceLinkDB.php';
23require_once 'tests/utils/ReferenceHistory.php';
24require_once 'tests/utils/FakeBookmarkService.php';
diff --git a/tests/config/ConfigJsonTest.php b/tests/config/ConfigJsonTest.php
index 95ad060b..33160eb0 100644
--- a/tests/config/ConfigJsonTest.php
+++ b/tests/config/ConfigJsonTest.php
@@ -24,7 +24,7 @@ class ConfigJsonTest extends \PHPUnit\Framework\TestCase
24 $conf = $this->configIO->read('tests/utils/config/configJson.json.php'); 24 $conf = $this->configIO->read('tests/utils/config/configJson.json.php');
25 $this->assertEquals('root', $conf['credentials']['login']); 25 $this->assertEquals('root', $conf['credentials']['login']);
26 $this->assertEquals('lala', $conf['redirector']['url']); 26 $this->assertEquals('lala', $conf['redirector']['url']);
27 $this->assertEquals('tests/utils/config/datastore.php', $conf['resource']['datastore']); 27 $this->assertEquals('sandbox/datastore.php', $conf['resource']['datastore']);
28 $this->assertEquals('1', $conf['plugins']['WALLABAG_VERSION']); 28 $this->assertEquals('1', $conf['plugins']['WALLABAG_VERSION']);
29 } 29 }
30 30
diff --git a/tests/feed/FeedBuilderTest.php b/tests/feed/FeedBuilderTest.php
index b496cb4c..a43ff672 100644
--- a/tests/feed/FeedBuilderTest.php
+++ b/tests/feed/FeedBuilderTest.php
@@ -4,7 +4,12 @@ namespace Shaarli\Feed;
4 4
5use DateTime; 5use DateTime;
6use ReferenceLinkDB; 6use ReferenceLinkDB;
7use Shaarli\Bookmark\Bookmark;
8use Shaarli\Bookmark\BookmarkFileService;
7use Shaarli\Bookmark\LinkDB; 9use Shaarli\Bookmark\LinkDB;
10use Shaarli\Config\ConfigManager;
11use Shaarli\Formatter\FormatterFactory;
12use Shaarli\History;
8 13
9/** 14/**
10 * FeedBuilderTest class. 15 * FeedBuilderTest class.
@@ -30,7 +35,9 @@ class FeedBuilderTest extends \PHPUnit\Framework\TestCase
30 35
31 protected static $testDatastore = 'sandbox/datastore.php'; 36 protected static $testDatastore = 'sandbox/datastore.php';
32 37
33 public static $linkDB; 38 public static $bookmarkService;
39
40 public static $formatter;
34 41
35 public static $serverInfo; 42 public static $serverInfo;
36 43
@@ -39,9 +46,15 @@ class FeedBuilderTest extends \PHPUnit\Framework\TestCase
39 */ 46 */
40 public static function setUpBeforeClass() 47 public static function setUpBeforeClass()
41 { 48 {
42 $refLinkDB = new ReferenceLinkDB(); 49 $conf = new ConfigManager('tests/utils/config/configJson');
50 $conf->set('resource.datastore', self::$testDatastore);
51 $refLinkDB = new \ReferenceLinkDB();
43 $refLinkDB->write(self::$testDatastore); 52 $refLinkDB->write(self::$testDatastore);
44 self::$linkDB = new LinkDB(self::$testDatastore, true, false); 53 $history = new History('sandbox/history.php');
54 $factory = new FormatterFactory($conf);
55 self::$formatter = $factory->getFormatter();
56 self::$bookmarkService = new BookmarkFileService($conf, $history, true);
57
45 self::$serverInfo = array( 58 self::$serverInfo = array(
46 'HTTPS' => 'Off', 59 'HTTPS' => 'Off',
47 'SERVER_NAME' => 'host.tld', 60 'SERVER_NAME' => 'host.tld',
@@ -56,15 +69,15 @@ class FeedBuilderTest extends \PHPUnit\Framework\TestCase
56 */ 69 */
57 public function testGetTypeLanguage() 70 public function testGetTypeLanguage()
58 { 71 {
59 $feedBuilder = new FeedBuilder(null, FeedBuilder::$FEED_ATOM, null, null, false); 72 $feedBuilder = new FeedBuilder(null, self::$formatter, FeedBuilder::$FEED_ATOM, null, null, false);
60 $feedBuilder->setLocale(self::$LOCALE); 73 $feedBuilder->setLocale(self::$LOCALE);
61 $this->assertEquals(self::$ATOM_LANGUAGUE, $feedBuilder->getTypeLanguage()); 74 $this->assertEquals(self::$ATOM_LANGUAGUE, $feedBuilder->getTypeLanguage());
62 $feedBuilder = new FeedBuilder(null, FeedBuilder::$FEED_RSS, null, null, false); 75 $feedBuilder = new FeedBuilder(null, self::$formatter, FeedBuilder::$FEED_RSS, null, null, false);
63 $feedBuilder->setLocale(self::$LOCALE); 76 $feedBuilder->setLocale(self::$LOCALE);
64 $this->assertEquals(self::$RSS_LANGUAGE, $feedBuilder->getTypeLanguage()); 77 $this->assertEquals(self::$RSS_LANGUAGE, $feedBuilder->getTypeLanguage());
65 $feedBuilder = new FeedBuilder(null, FeedBuilder::$FEED_ATOM, null, null, false); 78 $feedBuilder = new FeedBuilder(null, self::$formatter, FeedBuilder::$FEED_ATOM, null, null, false);
66 $this->assertEquals('en', $feedBuilder->getTypeLanguage()); 79 $this->assertEquals('en', $feedBuilder->getTypeLanguage());
67 $feedBuilder = new FeedBuilder(null, FeedBuilder::$FEED_RSS, null, null, false); 80 $feedBuilder = new FeedBuilder(null, self::$formatter, FeedBuilder::$FEED_RSS, null, null, false);
68 $this->assertEquals('en-en', $feedBuilder->getTypeLanguage()); 81 $this->assertEquals('en-en', $feedBuilder->getTypeLanguage());
69 } 82 }
70 83
@@ -73,7 +86,14 @@ class FeedBuilderTest extends \PHPUnit\Framework\TestCase
73 */ 86 */
74 public function testRSSBuildData() 87 public function testRSSBuildData()
75 { 88 {
76 $feedBuilder = new FeedBuilder(self::$linkDB, FeedBuilder::$FEED_RSS, self::$serverInfo, null, false); 89 $feedBuilder = new FeedBuilder(
90 self::$bookmarkService,
91 self::$formatter,
92 FeedBuilder::$FEED_RSS,
93 self::$serverInfo,
94 null,
95 false
96 );
77 $feedBuilder->setLocale(self::$LOCALE); 97 $feedBuilder->setLocale(self::$LOCALE);
78 $data = $feedBuilder->buildData(); 98 $data = $feedBuilder->buildData();
79 // Test headers (RSS) 99 // Test headers (RSS)
@@ -88,7 +108,7 @@ class FeedBuilderTest extends \PHPUnit\Framework\TestCase
88 // Test first not pinned link (note link) 108 // Test first not pinned link (note link)
89 $link = $data['links'][array_keys($data['links'])[2]]; 109 $link = $data['links'][array_keys($data['links'])[2]];
90 $this->assertEquals(41, $link['id']); 110 $this->assertEquals(41, $link['id']);
91 $this->assertEquals(DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20150310_114651'), $link['created']); 111 $this->assertEquals(DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20150310_114651'), $link['created']);
92 $this->assertEquals('http://host.tld/?WDWyig', $link['guid']); 112 $this->assertEquals('http://host.tld/?WDWyig', $link['guid']);
93 $this->assertEquals('http://host.tld/?WDWyig', $link['url']); 113 $this->assertEquals('http://host.tld/?WDWyig', $link['url']);
94 $this->assertRegExp('/Tue, 10 Mar 2015 11:46:51 \+\d{4}/', $link['pub_iso_date']); 114 $this->assertRegExp('/Tue, 10 Mar 2015 11:46:51 \+\d{4}/', $link['pub_iso_date']);
@@ -117,7 +137,14 @@ class FeedBuilderTest extends \PHPUnit\Framework\TestCase
117 */ 137 */
118 public function testAtomBuildData() 138 public function testAtomBuildData()
119 { 139 {
120 $feedBuilder = new FeedBuilder(self::$linkDB, FeedBuilder::$FEED_ATOM, self::$serverInfo, null, false); 140 $feedBuilder = new FeedBuilder(
141 self::$bookmarkService,
142 self::$formatter,
143 FeedBuilder::$FEED_ATOM,
144 self::$serverInfo,
145 null,
146 false
147 );
121 $feedBuilder->setLocale(self::$LOCALE); 148 $feedBuilder->setLocale(self::$LOCALE);
122 $data = $feedBuilder->buildData(); 149 $data = $feedBuilder->buildData();
123 $this->assertEquals(ReferenceLinkDB::$NB_LINKS_TOTAL, count($data['links'])); 150 $this->assertEquals(ReferenceLinkDB::$NB_LINKS_TOTAL, count($data['links']));
@@ -136,13 +163,20 @@ class FeedBuilderTest extends \PHPUnit\Framework\TestCase
136 'searchtags' => 'stuff', 163 'searchtags' => 'stuff',
137 'searchterm' => 'beard', 164 'searchterm' => 'beard',
138 ); 165 );
139 $feedBuilder = new FeedBuilder(self::$linkDB, FeedBuilder::$FEED_ATOM, self::$serverInfo, $criteria, false); 166 $feedBuilder = new FeedBuilder(
167 self::$bookmarkService,
168 self::$formatter,
169 FeedBuilder::$FEED_ATOM,
170 self::$serverInfo,
171 $criteria,
172 false
173 );
140 $feedBuilder->setLocale(self::$LOCALE); 174 $feedBuilder->setLocale(self::$LOCALE);
141 $data = $feedBuilder->buildData(); 175 $data = $feedBuilder->buildData();
142 $this->assertEquals(1, count($data['links'])); 176 $this->assertEquals(1, count($data['links']));
143 $link = array_shift($data['links']); 177 $link = array_shift($data['links']);
144 $this->assertEquals(41, $link['id']); 178 $this->assertEquals(41, $link['id']);
145 $this->assertEquals(DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20150310_114651'), $link['created']); 179 $this->assertEquals(DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20150310_114651'), $link['created']);
146 } 180 }
147 181
148 /** 182 /**
@@ -153,13 +187,20 @@ class FeedBuilderTest extends \PHPUnit\Framework\TestCase
153 $criteria = array( 187 $criteria = array(
154 'nb' => '3', 188 'nb' => '3',
155 ); 189 );
156 $feedBuilder = new FeedBuilder(self::$linkDB, FeedBuilder::$FEED_ATOM, self::$serverInfo, $criteria, false); 190 $feedBuilder = new FeedBuilder(
191 self::$bookmarkService,
192 self::$formatter,
193 FeedBuilder::$FEED_ATOM,
194 self::$serverInfo,
195 $criteria,
196 false
197 );
157 $feedBuilder->setLocale(self::$LOCALE); 198 $feedBuilder->setLocale(self::$LOCALE);
158 $data = $feedBuilder->buildData(); 199 $data = $feedBuilder->buildData();
159 $this->assertEquals(3, count($data['links'])); 200 $this->assertEquals(3, count($data['links']));
160 $link = $data['links'][array_keys($data['links'])[2]]; 201 $link = $data['links'][array_keys($data['links'])[2]];
161 $this->assertEquals(41, $link['id']); 202 $this->assertEquals(41, $link['id']);
162 $this->assertEquals(DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20150310_114651'), $link['created']); 203 $this->assertEquals(DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20150310_114651'), $link['created']);
163 } 204 }
164 205
165 /** 206 /**
@@ -167,7 +208,14 @@ class FeedBuilderTest extends \PHPUnit\Framework\TestCase
167 */ 208 */
168 public function testBuildDataPermalinks() 209 public function testBuildDataPermalinks()
169 { 210 {
170 $feedBuilder = new FeedBuilder(self::$linkDB, FeedBuilder::$FEED_ATOM, self::$serverInfo, null, false); 211 $feedBuilder = new FeedBuilder(
212 self::$bookmarkService,
213 self::$formatter,
214 FeedBuilder::$FEED_ATOM,
215 self::$serverInfo,
216 null,
217 false
218 );
171 $feedBuilder->setLocale(self::$LOCALE); 219 $feedBuilder->setLocale(self::$LOCALE);
172 $feedBuilder->setUsePermalinks(true); 220 $feedBuilder->setUsePermalinks(true);
173 $data = $feedBuilder->buildData(); 221 $data = $feedBuilder->buildData();
@@ -176,7 +224,7 @@ class FeedBuilderTest extends \PHPUnit\Framework\TestCase
176 // First link is a permalink 224 // First link is a permalink
177 $link = $data['links'][array_keys($data['links'])[2]]; 225 $link = $data['links'][array_keys($data['links'])[2]];
178 $this->assertEquals(41, $link['id']); 226 $this->assertEquals(41, $link['id']);
179 $this->assertEquals(DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20150310_114651'), $link['created']); 227 $this->assertEquals(DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20150310_114651'), $link['created']);
180 $this->assertEquals('http://host.tld/?WDWyig', $link['guid']); 228 $this->assertEquals('http://host.tld/?WDWyig', $link['guid']);
181 $this->assertEquals('http://host.tld/?WDWyig', $link['url']); 229 $this->assertEquals('http://host.tld/?WDWyig', $link['url']);
182 $this->assertContains('Direct link', $link['description']); 230 $this->assertContains('Direct link', $link['description']);
@@ -184,7 +232,7 @@ class FeedBuilderTest extends \PHPUnit\Framework\TestCase
184 // Second link is a direct link 232 // Second link is a direct link
185 $link = $data['links'][array_keys($data['links'])[3]]; 233 $link = $data['links'][array_keys($data['links'])[3]];
186 $this->assertEquals(8, $link['id']); 234 $this->assertEquals(8, $link['id']);
187 $this->assertEquals(DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20150310_114633'), $link['created']); 235 $this->assertEquals(DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20150310_114633'), $link['created']);
188 $this->assertEquals('http://host.tld/?RttfEw', $link['guid']); 236 $this->assertEquals('http://host.tld/?RttfEw', $link['guid']);
189 $this->assertEquals('https://static.fsf.org/nosvn/faif-2.0.pdf', $link['url']); 237 $this->assertEquals('https://static.fsf.org/nosvn/faif-2.0.pdf', $link['url']);
190 $this->assertContains('Direct link', $link['description']); 238 $this->assertContains('Direct link', $link['description']);
@@ -196,7 +244,14 @@ class FeedBuilderTest extends \PHPUnit\Framework\TestCase
196 */ 244 */
197 public function testBuildDataHideDates() 245 public function testBuildDataHideDates()
198 { 246 {
199 $feedBuilder = new FeedBuilder(self::$linkDB, FeedBuilder::$FEED_ATOM, self::$serverInfo, null, false); 247 $feedBuilder = new FeedBuilder(
248 self::$bookmarkService,
249 self::$formatter,
250 FeedBuilder::$FEED_ATOM,
251 self::$serverInfo,
252 null,
253 false
254 );
200 $feedBuilder->setLocale(self::$LOCALE); 255 $feedBuilder->setLocale(self::$LOCALE);
201 $feedBuilder->setHideDates(true); 256 $feedBuilder->setHideDates(true);
202 $data = $feedBuilder->buildData(); 257 $data = $feedBuilder->buildData();
@@ -204,7 +259,14 @@ class FeedBuilderTest extends \PHPUnit\Framework\TestCase
204 $this->assertFalse($data['show_dates']); 259 $this->assertFalse($data['show_dates']);
205 260
206 // Show dates while logged in 261 // Show dates while logged in
207 $feedBuilder = new FeedBuilder(self::$linkDB, FeedBuilder::$FEED_ATOM, self::$serverInfo, null, true); 262 $feedBuilder = new FeedBuilder(
263 self::$bookmarkService,
264 self::$formatter,
265 FeedBuilder::$FEED_ATOM,
266 self::$serverInfo,
267 null,
268 true
269 );
208 $feedBuilder->setLocale(self::$LOCALE); 270 $feedBuilder->setLocale(self::$LOCALE);
209 $feedBuilder->setHideDates(true); 271 $feedBuilder->setHideDates(true);
210 $data = $feedBuilder->buildData(); 272 $data = $feedBuilder->buildData();
@@ -225,7 +287,8 @@ class FeedBuilderTest extends \PHPUnit\Framework\TestCase
225 'REQUEST_URI' => '/~user/shaarli/index.php?do=feed', 287 'REQUEST_URI' => '/~user/shaarli/index.php?do=feed',
226 ); 288 );
227 $feedBuilder = new FeedBuilder( 289 $feedBuilder = new FeedBuilder(
228 self::$linkDB, 290 self::$bookmarkService,
291 self::$formatter,
229 FeedBuilder::$FEED_ATOM, 292 FeedBuilder::$FEED_ATOM,
230 $serverInfo, 293 $serverInfo,
231 null, 294 null,
diff --git a/tests/formatter/BookmarkDefaultFormatterTest.php b/tests/formatter/BookmarkDefaultFormatterTest.php
new file mode 100644
index 00000000..fe42a208
--- /dev/null
+++ b/tests/formatter/BookmarkDefaultFormatterTest.php
@@ -0,0 +1,156 @@
1<?php
2
3namespace Shaarli\Formatter;
4
5use DateTime;
6use PHPUnit\Framework\TestCase;
7use Shaarli\Bookmark\Bookmark;
8use Shaarli\Config\ConfigManager;
9
10/**
11 * Class BookmarkDefaultFormatterTest
12 * @package Shaarli\Formatter
13 */
14class BookmarkDefaultFormatterTest extends TestCase
15{
16 /** @var string Path of test config file */
17 protected static $testConf = 'sandbox/config';
18
19 /** @var BookmarkFormatter */
20 protected $formatter;
21
22 /** @var ConfigManager instance */
23 protected $conf;
24
25 /**
26 * Initialize formatter instance.
27 */
28 public function setUp()
29 {
30 copy('tests/utils/config/configJson.json.php', self::$testConf .'.json.php');
31 $this->conf = new ConfigManager(self::$testConf);
32 $this->formatter = new BookmarkDefaultFormatter($this->conf);
33 }
34
35 /**
36 * Test formatting a bookmark with all its attribute filled.
37 */
38 public function testFormatFull()
39 {
40 $bookmark = new Bookmark();
41 $bookmark->setId($id = 11);
42 $bookmark->setShortUrl($short = 'abcdef');
43 $bookmark->setUrl('https://sub.domain.tld?query=here&for=real#hash');
44 $bookmark->setTitle($title = 'This is a <strong>bookmark</strong>');
45 $bookmark->setDescription($desc = '<h2>Content</h2><p>`Here is some content</p>');
46 $bookmark->setTags($tags = ['tag1', 'bookmark', 'other', '<script>alert("xss");</script>']);
47 $bookmark->setThumbnail('http://domain2.tdl2/?type=img&name=file.png');
48 $bookmark->setSticky(true);
49 $bookmark->setCreated($created = DateTime::createFromFormat('Ymd_His', '20190521_190412'));
50 $bookmark->setUpdated($updated = DateTime::createFromFormat('Ymd_His', '20190521_191213'));
51 $bookmark->setPrivate(true);
52
53 $link = $this->formatter->format($bookmark);
54 $this->assertEquals($id, $link['id']);
55 $this->assertEquals($short, $link['shorturl']);
56 $this->assertEquals('https://sub.domain.tld?query=here&amp;for=real#hash', $link['url']);
57 $this->assertEquals(
58 'https://sub.domain.tld?query=here&amp;for=real#hash',
59 $link['real_url']
60 );
61 $this->assertEquals('This is a &lt;strong&gt;bookmark&lt;/strong&gt;', $link['title']);
62 $this->assertEquals(
63 '&lt;h2&gt;Content&lt;/h2&gt;&lt;p&gt;`Here is some content&lt;/p&gt;',
64 $link['description']
65 );
66 $tags[3] = '&lt;script&gt;alert(&quot;xss&quot;);&lt;/script&gt;';
67 $this->assertEquals($tags, $link['taglist']);
68 $this->assertEquals(implode(' ', $tags), $link['tags']);
69 $this->assertEquals(
70 'http://domain2.tdl2/?type=img&amp;name=file.png',
71 $link['thumbnail']
72 );
73 $this->assertEquals($created, $link['created']);
74 $this->assertEquals($created->getTimestamp(), $link['timestamp']);
75 $this->assertEquals($updated, $link['updated']);
76 $this->assertEquals($updated->getTimestamp(), $link['updated_timestamp']);
77 $this->assertTrue($link['private']);
78 $this->assertTrue($link['sticky']);
79 $this->assertEquals('private', $link['class']);
80 }
81
82 /**
83 * Test formatting a bookmark with all its attribute filled.
84 */
85 public function testFormatMinimal()
86 {
87 $bookmark = new Bookmark();
88
89 $link = $this->formatter->format($bookmark);
90 $this->assertEmpty($link['id']);
91 $this->assertEmpty($link['shorturl']);
92 $this->assertEmpty($link['url']);
93 $this->assertEmpty($link['real_url']);
94 $this->assertEmpty($link['title']);
95 $this->assertEmpty($link['description']);
96 $this->assertEmpty($link['taglist']);
97 $this->assertEmpty($link['tags']);
98 $this->assertEmpty($link['thumbnail']);
99 $this->assertEmpty($link['created']);
100 $this->assertEmpty($link['timestamp']);
101 $this->assertEmpty($link['updated']);
102 $this->assertEmpty($link['updated_timestamp']);
103 $this->assertFalse($link['private']);
104 $this->assertFalse($link['sticky']);
105 $this->assertEmpty($link['class']);
106 }
107
108 /**
109 * Make sure that the description is properly formatted by the default formatter.
110 */
111 public function testFormatDescription()
112 {
113 $description = [];
114 $description[] = 'This a <strong>description</strong>' . PHP_EOL;
115 $description[] = 'text https://sub.domain.tld?query=here&for=real#hash more text'. PHP_EOL;
116 $description[] = 'Also, there is an #hashtag added'. PHP_EOL;
117 $description[] = ' A N D KEEP SPACES ! '. PHP_EOL;
118
119 $bookmark = new Bookmark();
120 $bookmark->setDescription(implode('', $description));
121 $link = $this->formatter->format($bookmark);
122
123 $description[0] = 'This a &lt;strong&gt;description&lt;/strong&gt;<br />';
124 $url = 'https://sub.domain.tld?query=here&amp;for=real#hash';
125 $description[1] = 'text <a href="'. $url .'">'. $url .'</a> more text<br />';
126 $description[2] = 'Also, there is an <a href="?addtag=hashtag" '.
127 'title="Hashtag hashtag">#hashtag</a> added<br />';
128 $description[3] = '&nbsp; &nbsp; A &nbsp;N &nbsp;D KEEP &nbsp; &nbsp; '.
129 'SPACES &nbsp; &nbsp;! &nbsp; <br />';
130
131 $this->assertEquals(implode(PHP_EOL, $description) . PHP_EOL, $link['description']);
132 }
133
134 /**
135 * Test formatting URL with an index_url set
136 * It should prepend relative links.
137 */
138 public function testFormatNoteWithIndexUrl()
139 {
140 $bookmark = new Bookmark();
141 $bookmark->setUrl($short = '?abcdef');
142 $description = 'Text #hashtag more text';
143 $bookmark->setDescription($description);
144
145 $this->formatter->addContextData('index_url', $root = 'https://domain.tld/hithere/');
146
147 $link = $this->formatter->format($bookmark);
148 $this->assertEquals($root . $short, $link['url']);
149 $this->assertEquals($root . $short, $link['real_url']);
150 $this->assertEquals(
151 'Text <a href="'. $root .'?addtag=hashtag" title="Hashtag hashtag">'.
152 '#hashtag</a> more text',
153 $link['description']
154 );
155 }
156}
diff --git a/tests/formatter/BookmarkMarkdownFormatterTest.php b/tests/formatter/BookmarkMarkdownFormatterTest.php
new file mode 100644
index 00000000..0ca7f802
--- /dev/null
+++ b/tests/formatter/BookmarkMarkdownFormatterTest.php
@@ -0,0 +1,160 @@
1<?php
2
3namespace Shaarli\Formatter;
4
5use DateTime;
6use PHPUnit\Framework\TestCase;
7use Shaarli\Bookmark\Bookmark;
8use Shaarli\Config\ConfigManager;
9
10/**
11 * Class BookmarkMarkdownFormatterTest
12 * @package Shaarli\Formatter
13 */
14class BookmarkMarkdownFormatterTest extends TestCase
15{
16 /** @var string Path of test config file */
17 protected static $testConf = 'sandbox/config';
18
19 /** @var BookmarkFormatter */
20 protected $formatter;
21
22 /** @var ConfigManager instance */
23 protected $conf;
24
25 /**
26 * Initialize formatter instance.
27 */
28 public function setUp()
29 {
30 copy('tests/utils/config/configJson.json.php', self::$testConf .'.json.php');
31 $this->conf = new ConfigManager(self::$testConf);
32 $this->formatter = new BookmarkMarkdownFormatter($this->conf);
33 }
34
35 /**
36 * Test formatting a bookmark with all its attribute filled.
37 */
38 public function testFormatFull()
39 {
40 $bookmark = new Bookmark();
41 $bookmark->setId($id = 11);
42 $bookmark->setShortUrl($short = 'abcdef');
43 $bookmark->setUrl('https://sub.domain.tld?query=here&for=real#hash');
44 $bookmark->setTitle($title = 'This is a <strong>bookmark</strong>');
45 $bookmark->setDescription('<h2>Content</h2><p>`Here is some content</p>');
46 $bookmark->setTags($tags = ['tag1', 'bookmark', 'other', '<script>alert("xss");</script>']);
47 $bookmark->setThumbnail('http://domain2.tdl2/?type=img&name=file.png');
48 $bookmark->setSticky(true);
49 $bookmark->setCreated($created = DateTime::createFromFormat('Ymd_His', '20190521_190412'));
50 $bookmark->setUpdated($updated = DateTime::createFromFormat('Ymd_His', '20190521_191213'));
51 $bookmark->setPrivate(true);
52
53 $link = $this->formatter->format($bookmark);
54 $this->assertEquals($id, $link['id']);
55 $this->assertEquals($short, $link['shorturl']);
56 $this->assertEquals('https://sub.domain.tld?query=here&amp;for=real#hash', $link['url']);
57 $this->assertEquals(
58 'https://sub.domain.tld?query=here&amp;for=real#hash',
59 $link['real_url']
60 );
61 $this->assertEquals('This is a &lt;strong&gt;bookmark&lt;/strong&gt;', $link['title']);
62 $this->assertEquals(
63 '<div class="markdown"><p>'.
64 '&lt;h2&gt;Content&lt;/h2&gt;&lt;p&gt;`Here is some content&lt;/p&gt;'.
65 '</p></div>',
66 $link['description']
67 );
68 $tags[3] = '&lt;script&gt;alert(&quot;xss&quot;);&lt;/script&gt;';
69 $this->assertEquals($tags, $link['taglist']);
70 $this->assertEquals(implode(' ', $tags), $link['tags']);
71 $this->assertEquals(
72 'http://domain2.tdl2/?type=img&amp;name=file.png',
73 $link['thumbnail']
74 );
75 $this->assertEquals($created, $link['created']);
76 $this->assertEquals($created->getTimestamp(), $link['timestamp']);
77 $this->assertEquals($updated, $link['updated']);
78 $this->assertEquals($updated->getTimestamp(), $link['updated_timestamp']);
79 $this->assertTrue($link['private']);
80 $this->assertTrue($link['sticky']);
81 $this->assertEquals('private', $link['class']);
82 }
83
84 /**
85 * Test formatting a bookmark with all its attribute filled.
86 */
87 public function testFormatMinimal()
88 {
89 $bookmark = new Bookmark();
90
91 $link = $this->formatter->format($bookmark);
92 $this->assertEmpty($link['id']);
93 $this->assertEmpty($link['shorturl']);
94 $this->assertEmpty($link['url']);
95 $this->assertEmpty($link['real_url']);
96 $this->assertEmpty($link['title']);
97 $this->assertEmpty($link['description']);
98 $this->assertEmpty($link['taglist']);
99 $this->assertEmpty($link['tags']);
100 $this->assertEmpty($link['thumbnail']);
101 $this->assertEmpty($link['created']);
102 $this->assertEmpty($link['timestamp']);
103 $this->assertEmpty($link['updated']);
104 $this->assertEmpty($link['updated_timestamp']);
105 $this->assertFalse($link['private']);
106 $this->assertFalse($link['sticky']);
107 $this->assertEmpty($link['class']);
108 }
109
110 /**
111 * Make sure that the description is properly formatted by the default formatter.
112 */
113 public function testFormatDescription()
114 {
115 $description = 'This a <strong>description</strong>'. PHP_EOL;
116 $description .= 'text https://sub.domain.tld?query=here&for=real#hash more text'. PHP_EOL;
117 $description .= 'Also, there is an #hashtag added'. PHP_EOL;
118 $description .= ' A N D KEEP SPACES ! '. PHP_EOL;
119
120 $bookmark = new Bookmark();
121 $bookmark->setDescription($description);
122 $link = $this->formatter->format($bookmark);
123
124 $description = '<div class="markdown"><p>';
125 $description .= 'This a &lt;strong&gt;description&lt;/strong&gt;<br />'. PHP_EOL;
126 $url = 'https://sub.domain.tld?query=here&amp;for=real#hash';
127 $description .= 'text <a href="'. $url .'">'. $url .'</a> more text<br />'. PHP_EOL;
128 $description .= 'Also, there is an <a href="?addtag=hashtag">#hashtag</a> added<br />'. PHP_EOL;
129 $description .= 'A N D KEEP SPACES ! ';
130 $description .= '</p></div>';
131
132 $this->assertEquals($description, $link['description']);
133 }
134
135 /**
136 * Test formatting URL with an index_url set
137 * It should prepend relative links.
138 */
139 public function testFormatNoteWithIndexUrl()
140 {
141 $bookmark = new Bookmark();
142 $bookmark->setUrl($short = '?abcdef');
143 $description = 'Text #hashtag more text';
144 $bookmark->setDescription($description);
145
146 $this->formatter->addContextData('index_url', $root = 'https://domain.tld/hithere/');
147
148 $description = '<div class="markdown"><p>';
149 $description .= 'Text <a href="'. $root .'?addtag=hashtag">#hashtag</a> more text';
150 $description .= '</p></div>';
151
152 $link = $this->formatter->format($bookmark);
153 $this->assertEquals($root . $short, $link['url']);
154 $this->assertEquals($root . $short, $link['real_url']);
155 $this->assertEquals(
156 $description,
157 $link['description']
158 );
159 }
160}
diff --git a/tests/formatter/BookmarkRawFormatterTest.php b/tests/formatter/BookmarkRawFormatterTest.php
new file mode 100644
index 00000000..ceb6fb73
--- /dev/null
+++ b/tests/formatter/BookmarkRawFormatterTest.php
@@ -0,0 +1,97 @@
1<?php
2
3namespace Shaarli\Formatter;
4
5use DateTime;
6use PHPUnit\Framework\TestCase;
7use Shaarli\Bookmark\Bookmark;
8use Shaarli\Config\ConfigManager;
9
10/**
11 * Class BookmarkRawFormatterTest
12 * @package Shaarli\Formatter
13 */
14class BookmarkRawFormatterTest extends TestCase
15{
16 /** @var string Path of test config file */
17 protected static $testConf = 'sandbox/config';
18
19 /** @var BookmarkFormatter */
20 protected $formatter;
21
22 /** @var ConfigManager instance */
23 protected $conf;
24
25 /**
26 * Initialize formatter instance.
27 */
28 public function setUp()
29 {
30 copy('tests/utils/config/configJson.json.php', self::$testConf .'.json.php');
31 $this->conf = new ConfigManager(self::$testConf);
32 $this->formatter = new BookmarkRawFormatter($this->conf);
33 }
34
35 /**
36 * Test formatting a bookmark with all its attribute filled.
37 */
38 public function testFormatFull()
39 {
40 $bookmark = new Bookmark();
41 $bookmark->setId($id = 11);
42 $bookmark->setShortUrl($short = 'abcdef');
43 $bookmark->setUrl($url = 'https://sub.domain.tld?query=here&for=real#hash');
44 $bookmark->setTitle($title = 'This is a <strong>bookmark</strong>');
45 $bookmark->setDescription($desc = '<h2>Content</h2><p>`Here is some content</p>');
46 $bookmark->setTags($tags = ['tag1', 'bookmark', 'other', '<script>alert("xss");</script>']);
47 $bookmark->setThumbnail($thumb = 'http://domain2.tdl2/file.png');
48 $bookmark->setSticky(true);
49 $bookmark->setCreated($created = DateTime::createFromFormat('Ymd_His', '20190521_190412'));
50 $bookmark->setUpdated($updated = DateTime::createFromFormat('Ymd_His', '20190521_191213'));
51 $bookmark->setPrivate(true);
52
53 $link = $this->formatter->format($bookmark);
54 $this->assertEquals($id, $link['id']);
55 $this->assertEquals($short, $link['shorturl']);
56 $this->assertEquals($url, $link['url']);
57 $this->assertEquals($url, $link['real_url']);
58 $this->assertEquals($title, $link['title']);
59 $this->assertEquals($desc, $link['description']);
60 $this->assertEquals($tags, $link['taglist']);
61 $this->assertEquals(implode(' ', $tags), $link['tags']);
62 $this->assertEquals($thumb, $link['thumbnail']);
63 $this->assertEquals($created, $link['created']);
64 $this->assertEquals($created->getTimestamp(), $link['timestamp']);
65 $this->assertEquals($updated, $link['updated']);
66 $this->assertEquals($updated->getTimestamp(), $link['updated_timestamp']);
67 $this->assertTrue($link['private']);
68 $this->assertTrue($link['sticky']);
69 $this->assertEquals('private', $link['class']);
70 }
71
72 /**
73 * Test formatting a bookmark with all its attribute filled.
74 */
75 public function testFormatMinimal()
76 {
77 $bookmark = new Bookmark();
78
79 $link = $this->formatter->format($bookmark);
80 $this->assertEmpty($link['id']);
81 $this->assertEmpty($link['shorturl']);
82 $this->assertEmpty($link['url']);
83 $this->assertEmpty($link['real_url']);
84 $this->assertEmpty($link['title']);
85 $this->assertEmpty($link['description']);
86 $this->assertEmpty($link['taglist']);
87 $this->assertEmpty($link['tags']);
88 $this->assertEmpty($link['thumbnail']);
89 $this->assertEmpty($link['created']);
90 $this->assertEmpty($link['timestamp']);
91 $this->assertEmpty($link['updated']);
92 $this->assertEmpty($link['updated_timestamp']);
93 $this->assertFalse($link['private']);
94 $this->assertFalse($link['sticky']);
95 $this->assertEmpty($link['class']);
96 }
97}
diff --git a/tests/formatter/FormatterFactoryTest.php b/tests/formatter/FormatterFactoryTest.php
new file mode 100644
index 00000000..317c0b2d
--- /dev/null
+++ b/tests/formatter/FormatterFactoryTest.php
@@ -0,0 +1,101 @@
1<?php
2
3namespace Shaarli\Formatter;
4
5use PHPUnit\Framework\TestCase;
6use Shaarli\Config\ConfigManager;
7
8/**
9 * Class FormatterFactoryTest
10 *
11 * @package Shaarli\Formatter
12 */
13class FormatterFactoryTest extends TestCase
14{
15 /** @var string Path of test config file */
16 protected static $testConf = 'sandbox/config';
17
18 /** @var FormatterFactory instance */
19 protected $factory;
20
21 /** @var ConfigManager instance */
22 protected $conf;
23
24 /**
25 * Initialize FormatterFactory instance
26 */
27 public function setUp()
28 {
29 copy('tests/utils/config/configJson.json.php', self::$testConf .'.json.php');
30 $this->conf = new ConfigManager(self::$testConf);
31 $this->factory = new FormatterFactory($this->conf);
32 }
33
34 /**
35 * Test creating an instance of BookmarkFormatter without any setting -> default formatter
36 */
37 public function testCreateInstanceDefault()
38 {
39 $this->assertInstanceOf(BookmarkDefaultFormatter::class, $this->factory->getFormatter());
40 }
41
42 /**
43 * Test creating an instance of BookmarkDefaultFormatter from settings
44 */
45 public function testCreateInstanceDefaultSetting()
46 {
47 $this->conf->set('formatter', 'default');
48 $this->assertInstanceOf(BookmarkDefaultFormatter::class, $this->factory->getFormatter());
49 }
50
51 /**
52 * Test creating an instance of BookmarkDefaultFormatter from parameter
53 */
54 public function testCreateInstanceDefaultParameter()
55 {
56 $this->assertInstanceOf(
57 BookmarkDefaultFormatter::class,
58 $this->factory->getFormatter('default')
59 );
60 }
61
62 /**
63 * Test creating an instance of BookmarkRawFormatter from settings
64 */
65 public function testCreateInstanceRawSetting()
66 {
67 $this->conf->set('formatter', 'raw');
68 $this->assertInstanceOf(BookmarkRawFormatter::class, $this->factory->getFormatter());
69 }
70
71 /**
72 * Test creating an instance of BookmarkRawFormatter from parameter
73 */
74 public function testCreateInstanceRawParameter()
75 {
76 $this->assertInstanceOf(
77 BookmarkRawFormatter::class,
78 $this->factory->getFormatter('raw')
79 );
80 }
81
82 /**
83 * Test creating an instance of BookmarkMarkdownFormatter from settings
84 */
85 public function testCreateInstanceMarkdownSetting()
86 {
87 $this->conf->set('formatter', 'markdown');
88 $this->assertInstanceOf(BookmarkMarkdownFormatter::class, $this->factory->getFormatter());
89 }
90
91 /**
92 * Test creating an instance of BookmarkMarkdownFormatter from parameter
93 */
94 public function testCreateInstanceMarkdownParameter()
95 {
96 $this->assertInstanceOf(
97 BookmarkMarkdownFormatter::class,
98 $this->factory->getFormatter('markdown')
99 );
100 }
101}
diff --git a/tests/legacy/LegacyDummyUpdater.php b/tests/legacy/LegacyDummyUpdater.php
new file mode 100644
index 00000000..10e0a5b7
--- /dev/null
+++ b/tests/legacy/LegacyDummyUpdater.php
@@ -0,0 +1,74 @@
1<?php
2namespace Shaarli\Updater;
3
4use Exception;
5use ReflectionClass;
6use ReflectionMethod;
7use Shaarli\Config\ConfigManager;
8use Shaarli\Legacy\LegacyLinkDB;
9use Shaarli\Legacy\LegacyUpdater;
10
11/**
12 * Class LegacyDummyUpdater.
13 * Extends updater to add update method designed for unit tests.
14 */
15class LegacyDummyUpdater extends LegacyUpdater
16{
17 /**
18 * Object constructor.
19 *
20 * @param array $doneUpdates Updates which are already done.
21 * @param LegacyLinkDB $linkDB LinkDB instance.
22 * @param ConfigManager $conf Configuration Manager instance.
23 * @param boolean $isLoggedIn True if the user is logged in.
24 */
25 public function __construct($doneUpdates, $linkDB, $conf, $isLoggedIn)
26 {
27 parent::__construct($doneUpdates, $linkDB, $conf, $isLoggedIn);
28
29 // Retrieve all update methods.
30 // For unit test, only retrieve final methods,
31 $class = new ReflectionClass($this);
32 $this->methods = $class->getMethods(ReflectionMethod::IS_FINAL);
33 }
34
35 /**
36 * Update method 1.
37 *
38 * @return bool true.
39 */
40 final private function updateMethodDummy1()
41 {
42 return true;
43 }
44
45 /**
46 * Update method 2.
47 *
48 * @return bool true.
49 */
50 final private function updateMethodDummy2()
51 {
52 return true;
53 }
54
55 /**
56 * Update method 3.
57 *
58 * @return bool true.
59 */
60 final private function updateMethodDummy3()
61 {
62 return true;
63 }
64
65 /**
66 * Update method 4, raise an exception.
67 *
68 * @throws Exception error.
69 */
70 final private function updateMethodException()
71 {
72 throw new Exception('whatever');
73 }
74}
diff --git a/tests/bookmark/LinkDBTest.php b/tests/legacy/LegacyLinkDBTest.php
index ffe03cc5..17b2b0e6 100644
--- a/tests/bookmark/LinkDBTest.php
+++ b/tests/legacy/LegacyLinkDBTest.php
@@ -3,12 +3,13 @@
3 * Link datastore tests 3 * Link datastore tests
4 */ 4 */
5 5
6namespace Shaarli\Bookmark; 6namespace Shaarli\Legacy;
7 7
8use DateTime; 8use DateTime;
9use ReferenceLinkDB; 9use ReferenceLinkDB;
10use ReflectionClass; 10use ReflectionClass;
11use Shaarli; 11use Shaarli;
12use Shaarli\Bookmark\Bookmark;
12 13
13require_once 'application/feed/Cache.php'; 14require_once 'application/feed/Cache.php';
14require_once 'application/Utils.php'; 15require_once 'application/Utils.php';
@@ -16,9 +17,9 @@ require_once 'tests/utils/ReferenceLinkDB.php';
16 17
17 18
18/** 19/**
19 * Unitary tests for LinkDB 20 * Unitary tests for LegacyLinkDBTest
20 */ 21 */
21class LinkDBTest extends \PHPUnit\Framework\TestCase 22class LegacyLinkDBTest extends \PHPUnit\Framework\TestCase
22{ 23{
23 // datastore to test write operations 24 // datastore to test write operations
24 protected static $testDatastore = 'sandbox/datastore.php'; 25 protected static $testDatastore = 'sandbox/datastore.php';
@@ -29,19 +30,19 @@ class LinkDBTest extends \PHPUnit\Framework\TestCase
29 protected static $refDB = null; 30 protected static $refDB = null;
30 31
31 /** 32 /**
32 * @var LinkDB public LinkDB instance. 33 * @var LegacyLinkDB public LinkDB instance.
33 */ 34 */
34 protected static $publicLinkDB = null; 35 protected static $publicLinkDB = null;
35 36
36 /** 37 /**
37 * @var LinkDB private LinkDB instance. 38 * @var LegacyLinkDB private LinkDB instance.
38 */ 39 */
39 protected static $privateLinkDB = null; 40 protected static $privateLinkDB = null;
40 41
41 /** 42 /**
42 * Instantiates public and private LinkDBs with test data 43 * Instantiates public and private LinkDBs with test data
43 * 44 *
44 * The reference datastore contains public and private links that 45 * The reference datastore contains public and private bookmarks that
45 * will be used to test LinkDB's methods: 46 * will be used to test LinkDB's methods:
46 * - access filtering (public/private), 47 * - access filtering (public/private),
47 * - link searches: 48 * - link searches:
@@ -58,11 +59,10 @@ class LinkDBTest extends \PHPUnit\Framework\TestCase
58 unlink(self::$testDatastore); 59 unlink(self::$testDatastore);
59 } 60 }
60 61
61 self::$refDB = new ReferenceLinkDB(); 62 self::$refDB = new ReferenceLinkDB(true);
62 self::$refDB->write(self::$testDatastore); 63 self::$refDB->write(self::$testDatastore);
63 64 self::$publicLinkDB = new LegacyLinkDB(self::$testDatastore, false, false);
64 self::$publicLinkDB = new LinkDB(self::$testDatastore, false, false); 65 self::$privateLinkDB = new LegacyLinkDB(self::$testDatastore, true, false);
65 self::$privateLinkDB = new LinkDB(self::$testDatastore, true, false);
66 } 66 }
67 67
68 /** 68 /**
@@ -74,7 +74,7 @@ class LinkDBTest extends \PHPUnit\Framework\TestCase
74 */ 74 */
75 protected static function getMethod($name) 75 protected static function getMethod($name)
76 { 76 {
77 $class = new ReflectionClass('Shaarli\Bookmark\LinkDB'); 77 $class = new ReflectionClass('Shaarli\Legacy\LegacyLinkDB');
78 $method = $class->getMethod($name); 78 $method = $class->getMethod($name);
79 $method->setAccessible(true); 79 $method->setAccessible(true);
80 return $method; 80 return $method;
@@ -85,7 +85,7 @@ class LinkDBTest extends \PHPUnit\Framework\TestCase
85 */ 85 */
86 public function testConstructLoggedIn() 86 public function testConstructLoggedIn()
87 { 87 {
88 new LinkDB(self::$testDatastore, true, false); 88 new LegacyLinkDB(self::$testDatastore, true, false);
89 $this->assertFileExists(self::$testDatastore); 89 $this->assertFileExists(self::$testDatastore);
90 } 90 }
91 91
@@ -94,7 +94,7 @@ class LinkDBTest extends \PHPUnit\Framework\TestCase
94 */ 94 */
95 public function testConstructLoggedOut() 95 public function testConstructLoggedOut()
96 { 96 {
97 new LinkDB(self::$testDatastore, false, false); 97 new LegacyLinkDB(self::$testDatastore, false, false);
98 $this->assertFileExists(self::$testDatastore); 98 $this->assertFileExists(self::$testDatastore);
99 } 99 }
100 100
@@ -106,7 +106,7 @@ class LinkDBTest extends \PHPUnit\Framework\TestCase
106 */ 106 */
107 public function testConstructDatastoreNotWriteable() 107 public function testConstructDatastoreNotWriteable()
108 { 108 {
109 new LinkDB('null/store.db', false, false); 109 new LegacyLinkDB('null/store.db', false, false);
110 } 110 }
111 111
112 /** 112 /**
@@ -114,7 +114,7 @@ class LinkDBTest extends \PHPUnit\Framework\TestCase
114 */ 114 */
115 public function testCheckDBNew() 115 public function testCheckDBNew()
116 { 116 {
117 $linkDB = new LinkDB(self::$testDatastore, false, false); 117 $linkDB = new LegacyLinkDB(self::$testDatastore, false, false);
118 unlink(self::$testDatastore); 118 unlink(self::$testDatastore);
119 $this->assertFileNotExists(self::$testDatastore); 119 $this->assertFileNotExists(self::$testDatastore);
120 120
@@ -131,7 +131,7 @@ class LinkDBTest extends \PHPUnit\Framework\TestCase
131 */ 131 */
132 public function testCheckDBLoad() 132 public function testCheckDBLoad()
133 { 133 {
134 $linkDB = new LinkDB(self::$testDatastore, false, false); 134 $linkDB = new LegacyLinkDB(self::$testDatastore, false, false);
135 $datastoreSize = filesize(self::$testDatastore); 135 $datastoreSize = filesize(self::$testDatastore);
136 $this->assertGreaterThan(0, $datastoreSize); 136 $this->assertGreaterThan(0, $datastoreSize);
137 137
@@ -151,13 +151,13 @@ class LinkDBTest extends \PHPUnit\Framework\TestCase
151 public function testReadEmptyDB() 151 public function testReadEmptyDB()
152 { 152 {
153 file_put_contents(self::$testDatastore, '<?php /* S7QysKquBQA= */ ?>'); 153 file_put_contents(self::$testDatastore, '<?php /* S7QysKquBQA= */ ?>');
154 $emptyDB = new LinkDB(self::$testDatastore, false, false); 154 $emptyDB = new LegacyLinkDB(self::$testDatastore, false, false);
155 $this->assertEquals(0, sizeof($emptyDB)); 155 $this->assertEquals(0, sizeof($emptyDB));
156 $this->assertEquals(0, count($emptyDB)); 156 $this->assertEquals(0, count($emptyDB));
157 } 157 }
158 158
159 /** 159 /**
160 * Load public links from the DB 160 * Load public bookmarks from the DB
161 */ 161 */
162 public function testReadPublicDB() 162 public function testReadPublicDB()
163 { 163 {
@@ -168,7 +168,7 @@ class LinkDBTest extends \PHPUnit\Framework\TestCase
168 } 168 }
169 169
170 /** 170 /**
171 * Load public and private links from the DB 171 * Load public and private bookmarks from the DB
172 */ 172 */
173 public function testReadPrivateDB() 173 public function testReadPrivateDB()
174 { 174 {
@@ -179,11 +179,11 @@ class LinkDBTest extends \PHPUnit\Framework\TestCase
179 } 179 }
180 180
181 /** 181 /**
182 * Save the links to the DB 182 * Save the bookmarks to the DB
183 */ 183 */
184 public function testSave() 184 public function testSave()
185 { 185 {
186 $testDB = new LinkDB(self::$testDatastore, true, false); 186 $testDB = new LegacyLinkDB(self::$testDatastore, true, false);
187 $dbSize = sizeof($testDB); 187 $dbSize = sizeof($testDB);
188 188
189 $link = array( 189 $link = array(
@@ -192,18 +192,18 @@ class LinkDBTest extends \PHPUnit\Framework\TestCase
192 'url' => 'http://dum.my', 192 'url' => 'http://dum.my',
193 'description' => 'One more', 193 'description' => 'One more',
194 'private' => 0, 194 'private' => 0,
195 'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20150518_190000'), 195 'created' => DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20150518_190000'),
196 'tags' => 'unit test' 196 'tags' => 'unit test'
197 ); 197 );
198 $testDB[$link['id']] = $link; 198 $testDB[$link['id']] = $link;
199 $testDB->save('tests'); 199 $testDB->save('tests');
200 200
201 $testDB = new LinkDB(self::$testDatastore, true, false); 201 $testDB = new LegacyLinkDB(self::$testDatastore, true, false);
202 $this->assertEquals($dbSize + 1, sizeof($testDB)); 202 $this->assertEquals($dbSize + 1, sizeof($testDB));
203 } 203 }
204 204
205 /** 205 /**
206 * Count existing links 206 * Count existing bookmarks
207 */ 207 */
208 public function testCount() 208 public function testCount()
209 { 209 {
@@ -218,11 +218,11 @@ class LinkDBTest extends \PHPUnit\Framework\TestCase
218 } 218 }
219 219
220 /** 220 /**
221 * Count existing links - public links hidden 221 * Count existing bookmarks - public bookmarks hidden
222 */ 222 */
223 public function testCountHiddenPublic() 223 public function testCountHiddenPublic()
224 { 224 {
225 $linkDB = new LinkDB(self::$testDatastore, false, true); 225 $linkDB = new LegacyLinkDB(self::$testDatastore, false, true);
226 226
227 $this->assertEquals( 227 $this->assertEquals(
228 0, 228 0,
@@ -235,7 +235,7 @@ class LinkDBTest extends \PHPUnit\Framework\TestCase
235 } 235 }
236 236
237 /** 237 /**
238 * List the days for which links have been posted 238 * List the days for which bookmarks have been posted
239 */ 239 */
240 public function testDays() 240 public function testDays()
241 { 241 {
@@ -422,7 +422,7 @@ class LinkDBTest extends \PHPUnit\Framework\TestCase
422 /** 422 /**
423 * Test filterHash() with an invalid smallhash. 423 * Test filterHash() with an invalid smallhash.
424 * 424 *
425 * @expectedException \Shaarli\Bookmark\Exception\LinkNotFoundException 425 * @expectedException \Shaarli\Bookmark\Exception\BookmarkNotFoundException
426 */ 426 */
427 public function testFilterHashInValid1() 427 public function testFilterHashInValid1()
428 { 428 {
@@ -433,7 +433,7 @@ class LinkDBTest extends \PHPUnit\Framework\TestCase
433 /** 433 /**
434 * Test filterHash() with an empty smallhash. 434 * Test filterHash() with an empty smallhash.
435 * 435 *
436 * @expectedException \Shaarli\Bookmark\Exception\LinkNotFoundException 436 * @expectedException \Shaarli\Bookmark\Exception\BookmarkNotFoundException
437 */ 437 */
438 public function testFilterHashInValid() 438 public function testFilterHashInValid()
439 { 439 {
@@ -462,12 +462,12 @@ class LinkDBTest extends \PHPUnit\Framework\TestCase
462 } 462 }
463 463
464 /** 464 /**
465 * Test rename tag with a valid value present in multiple links 465 * Test rename tag with a valid value present in multiple bookmarks
466 */ 466 */
467 public function testRenameTagMultiple() 467 public function testRenameTagMultiple()
468 { 468 {
469 self::$refDB->write(self::$testDatastore); 469 self::$refDB->write(self::$testDatastore);
470 $linkDB = new LinkDB(self::$testDatastore, true, false); 470 $linkDB = new LegacyLinkDB(self::$testDatastore, true, false);
471 471
472 $res = $linkDB->renameTag('cartoon', 'Taz'); 472 $res = $linkDB->renameTag('cartoon', 'Taz');
473 $this->assertEquals(3, count($res)); 473 $this->assertEquals(3, count($res));
@@ -482,7 +482,7 @@ class LinkDBTest extends \PHPUnit\Framework\TestCase
482 public function testRenameTagCaseSensitive() 482 public function testRenameTagCaseSensitive()
483 { 483 {
484 self::$refDB->write(self::$testDatastore); 484 self::$refDB->write(self::$testDatastore);
485 $linkDB = new LinkDB(self::$testDatastore, true, false); 485 $linkDB = new LegacyLinkDB(self::$testDatastore, true, false);
486 486
487 $res = $linkDB->renameTag('sTuff', 'Taz'); 487 $res = $linkDB->renameTag('sTuff', 'Taz');
488 $this->assertEquals(1, count($res)); 488 $this->assertEquals(1, count($res));
@@ -494,7 +494,7 @@ class LinkDBTest extends \PHPUnit\Framework\TestCase
494 */ 494 */
495 public function testRenameTagInvalid() 495 public function testRenameTagInvalid()
496 { 496 {
497 $linkDB = new LinkDB(self::$testDatastore, false, false); 497 $linkDB = new LegacyLinkDB(self::$testDatastore, false, false);
498 498
499 $this->assertFalse($linkDB->renameTag('', 'test')); 499 $this->assertFalse($linkDB->renameTag('', 'test'));
500 $this->assertFalse($linkDB->renameTag('', '')); 500 $this->assertFalse($linkDB->renameTag('', ''));
@@ -509,7 +509,7 @@ class LinkDBTest extends \PHPUnit\Framework\TestCase
509 public function testDeleteTag() 509 public function testDeleteTag()
510 { 510 {
511 self::$refDB->write(self::$testDatastore); 511 self::$refDB->write(self::$testDatastore);
512 $linkDB = new LinkDB(self::$testDatastore, true, false); 512 $linkDB = new LegacyLinkDB(self::$testDatastore, true, false);
513 513
514 $res = $linkDB->renameTag('cartoon', null); 514 $res = $linkDB->renameTag('cartoon', null);
515 $this->assertEquals(3, count($res)); 515 $this->assertEquals(3, count($res));
@@ -624,7 +624,7 @@ class LinkDBTest extends \PHPUnit\Framework\TestCase
624 { 624 {
625 $nextId = 43; 625 $nextId = 43;
626 $creation = DateTime::createFromFormat('Ymd_His', '20190807_130444'); 626 $creation = DateTime::createFromFormat('Ymd_His', '20190807_130444');
627 $linkDB = new LinkDB(self::$testDatastore, true, false); 627 $linkDB = new LegacyLinkDB(self::$testDatastore, true, false);
628 for ($i = 0; $i < 4; ++$i) { 628 for ($i = 0; $i < 4; ++$i) {
629 $linkDB[$nextId + $i] = [ 629 $linkDB[$nextId + $i] = [
630 'id' => $nextId + $i, 630 'id' => $nextId + $i,
@@ -639,7 +639,7 @@ class LinkDBTest extends \PHPUnit\Framework\TestCase
639 // Check 4 new links 4 times 639 // Check 4 new links 4 times
640 for ($i = 0; $i < 4; ++$i) { 640 for ($i = 0; $i < 4; ++$i) {
641 $linkDB->save('tests'); 641 $linkDB->save('tests');
642 $linkDB = new LinkDB(self::$testDatastore, true, false); 642 $linkDB = new LegacyLinkDB(self::$testDatastore, true, false);
643 $count = 3; 643 $count = 3;
644 foreach ($linkDB as $link) { 644 foreach ($linkDB as $link) {
645 if ($link['sticky'] === true) { 645 if ($link['sticky'] === true) {
diff --git a/tests/bookmark/LinkFilterTest.php b/tests/legacy/LegacyLinkFilterTest.php
index 808f8122..ba9ec529 100644
--- a/tests/bookmark/LinkFilterTest.php
+++ b/tests/legacy/LegacyLinkFilterTest.php
@@ -4,18 +4,20 @@ namespace Shaarli\Bookmark;
4 4
5use Exception; 5use Exception;
6use ReferenceLinkDB; 6use ReferenceLinkDB;
7use Shaarli\Legacy\LegacyLinkDB;
8use Shaarli\Legacy\LegacyLinkFilter;
7 9
8/** 10/**
9 * Class LinkFilterTest. 11 * Class LegacyLinkFilterTest.
10 */ 12 */
11class LinkFilterTest extends \PHPUnit\Framework\TestCase 13class LegacyLinkFilterTest extends \PHPUnit\Framework\TestCase
12{ 14{
13 /** 15 /**
14 * @var string Test datastore path. 16 * @var string Test datastore path.
15 */ 17 */
16 protected static $testDatastore = 'sandbox/datastore.php'; 18 protected static $testDatastore = 'sandbox/datastore.php';
17 /** 19 /**
18 * @var LinkFilter instance. 20 * @var BookmarkFilter instance.
19 */ 21 */
20 protected static $linkFilter; 22 protected static $linkFilter;
21 23
@@ -25,7 +27,7 @@ class LinkFilterTest extends \PHPUnit\Framework\TestCase
25 protected static $refDB; 27 protected static $refDB;
26 28
27 /** 29 /**
28 * @var LinkDB instance 30 * @var LegacyLinkDB instance
29 */ 31 */
30 protected static $linkDB; 32 protected static $linkDB;
31 33
@@ -34,10 +36,10 @@ class LinkFilterTest extends \PHPUnit\Framework\TestCase
34 */ 36 */
35 public static function setUpBeforeClass() 37 public static function setUpBeforeClass()
36 { 38 {
37 self::$refDB = new ReferenceLinkDB(); 39 self::$refDB = new ReferenceLinkDB(true);
38 self::$refDB->write(self::$testDatastore); 40 self::$refDB->write(self::$testDatastore);
39 self::$linkDB = new LinkDB(self::$testDatastore, true, false); 41 self::$linkDB = new LegacyLinkDB(self::$testDatastore, true, false);
40 self::$linkFilter = new LinkFilter(self::$linkDB); 42 self::$linkFilter = new LegacyLinkFilter(self::$linkDB);
41 } 43 }
42 44
43 /** 45 /**
@@ -74,14 +76,14 @@ class LinkFilterTest extends \PHPUnit\Framework\TestCase
74 76
75 $this->assertEquals( 77 $this->assertEquals(
76 ReferenceLinkDB::$NB_LINKS_TOTAL, 78 ReferenceLinkDB::$NB_LINKS_TOTAL,
77 count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, '')) 79 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TAG, ''))
78 ); 80 );
79 81
80 $this->assertEquals( 82 $this->assertEquals(
81 self::$refDB->countUntaggedLinks(), 83 self::$refDB->countUntaggedLinks(),
82 count( 84 count(
83 self::$linkFilter->filter( 85 self::$linkFilter->filter(
84 LinkFilter::$FILTER_TAG, 86 LegacyLinkFilter::$FILTER_TAG,
85 /*$request=*/ 87 /*$request=*/
86 '', 88 '',
87 /*$casesensitive=*/ 89 /*$casesensitive=*/
@@ -96,89 +98,89 @@ class LinkFilterTest extends \PHPUnit\Framework\TestCase
96 98
97 $this->assertEquals( 99 $this->assertEquals(
98 ReferenceLinkDB::$NB_LINKS_TOTAL, 100 ReferenceLinkDB::$NB_LINKS_TOTAL,
99 count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, '')) 101 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TEXT, ''))
100 ); 102 );
101 } 103 }
102 104
103 /** 105 /**
104 * Filter links using a tag 106 * Filter bookmarks using a tag
105 */ 107 */
106 public function testFilterOneTag() 108 public function testFilterOneTag()
107 { 109 {
108 $this->assertEquals( 110 $this->assertEquals(
109 4, 111 4,
110 count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'web', false)) 112 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TAG, 'web', false))
111 ); 113 );
112 114
113 $this->assertEquals( 115 $this->assertEquals(
114 4, 116 4,
115 count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'web', false, 'all')) 117 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TAG, 'web', false, 'all'))
116 ); 118 );
117 119
118 $this->assertEquals( 120 $this->assertEquals(
119 4, 121 4,
120 count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'web', false, 'default-blabla')) 122 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TAG, 'web', false, 'default-blabla'))
121 ); 123 );
122 124
123 // Private only. 125 // Private only.
124 $this->assertEquals( 126 $this->assertEquals(
125 1, 127 1,
126 count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'web', false, 'private')) 128 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TAG, 'web', false, 'private'))
127 ); 129 );
128 130
129 // Public only. 131 // Public only.
130 $this->assertEquals( 132 $this->assertEquals(
131 3, 133 3,
132 count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'web', false, 'public')) 134 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TAG, 'web', false, 'public'))
133 ); 135 );
134 } 136 }
135 137
136 /** 138 /**
137 * Filter links using a tag - case-sensitive 139 * Filter bookmarks using a tag - case-sensitive
138 */ 140 */
139 public function testFilterCaseSensitiveTag() 141 public function testFilterCaseSensitiveTag()
140 { 142 {
141 $this->assertEquals( 143 $this->assertEquals(
142 0, 144 0,
143 count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'mercurial', true)) 145 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TAG, 'mercurial', true))
144 ); 146 );
145 147
146 $this->assertEquals( 148 $this->assertEquals(
147 1, 149 1,
148 count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'Mercurial', true)) 150 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TAG, 'Mercurial', true))
149 ); 151 );
150 } 152 }
151 153
152 /** 154 /**
153 * Filter links using a tag combination 155 * Filter bookmarks using a tag combination
154 */ 156 */
155 public function testFilterMultipleTags() 157 public function testFilterMultipleTags()
156 { 158 {
157 $this->assertEquals( 159 $this->assertEquals(
158 2, 160 2,
159 count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'dev cartoon', false)) 161 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TAG, 'dev cartoon', false))
160 ); 162 );
161 } 163 }
162 164
163 /** 165 /**
164 * Filter links using a non-existent tag 166 * Filter bookmarks using a non-existent tag
165 */ 167 */
166 public function testFilterUnknownTag() 168 public function testFilterUnknownTag()
167 { 169 {
168 $this->assertEquals( 170 $this->assertEquals(
169 0, 171 0,
170 count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'null', false)) 172 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TAG, 'null', false))
171 ); 173 );
172 } 174 }
173 175
174 /** 176 /**
175 * Return links for a given day 177 * Return bookmarks for a given day
176 */ 178 */
177 public function testFilterDay() 179 public function testFilterDay()
178 { 180 {
179 $this->assertEquals( 181 $this->assertEquals(
180 4, 182 4,
181 count(self::$linkFilter->filter(LinkFilter::$FILTER_DAY, '20121206')) 183 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_DAY, '20121206'))
182 ); 184 );
183 } 185 }
184 186
@@ -189,7 +191,7 @@ class LinkFilterTest extends \PHPUnit\Framework\TestCase
189 { 191 {
190 $this->assertEquals( 192 $this->assertEquals(
191 0, 193 0,
192 count(self::$linkFilter->filter(LinkFilter::$FILTER_DAY, '19700101')) 194 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_DAY, '19700101'))
193 ); 195 );
194 } 196 }
195 197
@@ -200,7 +202,7 @@ class LinkFilterTest extends \PHPUnit\Framework\TestCase
200 */ 202 */
201 public function testFilterInvalidDayWithChars() 203 public function testFilterInvalidDayWithChars()
202 { 204 {
203 self::$linkFilter->filter(LinkFilter::$FILTER_DAY, 'Rainy day, dream away'); 205 self::$linkFilter->filter(LegacyLinkFilter::$FILTER_DAY, 'Rainy day, dream away');
204 } 206 }
205 207
206 /** 208 /**
@@ -210,7 +212,7 @@ class LinkFilterTest extends \PHPUnit\Framework\TestCase
210 */ 212 */
211 public function testFilterInvalidDayDigits() 213 public function testFilterInvalidDayDigits()
212 { 214 {
213 self::$linkFilter->filter(LinkFilter::$FILTER_DAY, '20'); 215 self::$linkFilter->filter(LegacyLinkFilter::$FILTER_DAY, '20');
214 } 216 }
215 217
216 /** 218 /**
@@ -218,7 +220,7 @@ class LinkFilterTest extends \PHPUnit\Framework\TestCase
218 */ 220 */
219 public function testFilterSmallHash() 221 public function testFilterSmallHash()
220 { 222 {
221 $links = self::$linkFilter->filter(LinkFilter::$FILTER_HASH, 'IuWvgA'); 223 $links = self::$linkFilter->filter(LegacyLinkFilter::$FILTER_HASH, 'IuWvgA');
222 224
223 $this->assertEquals( 225 $this->assertEquals(
224 1, 226 1,
@@ -234,11 +236,11 @@ class LinkFilterTest extends \PHPUnit\Framework\TestCase
234 /** 236 /**
235 * No link for this hash 237 * No link for this hash
236 * 238 *
237 * @expectedException \Shaarli\Bookmark\Exception\LinkNotFoundException 239 * @expectedException \Shaarli\Bookmark\Exception\BookmarkNotFoundException
238 */ 240 */
239 public function testFilterUnknownSmallHash() 241 public function testFilterUnknownSmallHash()
240 { 242 {
241 self::$linkFilter->filter(LinkFilter::$FILTER_HASH, 'Iblaah'); 243 self::$linkFilter->filter(LegacyLinkFilter::$FILTER_HASH, 'Iblaah');
242 } 244 }
243 245
244 /** 246 /**
@@ -248,7 +250,7 @@ class LinkFilterTest extends \PHPUnit\Framework\TestCase
248 { 250 {
249 $this->assertEquals( 251 $this->assertEquals(
250 0, 252 0,
251 count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'azertyuiop')) 253 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TEXT, 'azertyuiop'))
252 ); 254 );
253 } 255 }
254 256
@@ -259,12 +261,12 @@ class LinkFilterTest extends \PHPUnit\Framework\TestCase
259 { 261 {
260 $this->assertEquals( 262 $this->assertEquals(
261 2, 263 2,
262 count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'ars.userfriendly.org')) 264 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TEXT, 'ars.userfriendly.org'))
263 ); 265 );
264 266
265 $this->assertEquals( 267 $this->assertEquals(
266 2, 268 2,
267 count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'ars org')) 269 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TEXT, 'ars org'))
268 ); 270 );
269 } 271 }
270 272
@@ -276,21 +278,21 @@ class LinkFilterTest extends \PHPUnit\Framework\TestCase
276 // use miscellaneous cases 278 // use miscellaneous cases
277 $this->assertEquals( 279 $this->assertEquals(
278 2, 280 2,
279 count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'userfriendly -')) 281 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TEXT, 'userfriendly -'))
280 ); 282 );
281 $this->assertEquals( 283 $this->assertEquals(
282 2, 284 2,
283 count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'UserFriendly -')) 285 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TEXT, 'UserFriendly -'))
284 ); 286 );
285 $this->assertEquals( 287 $this->assertEquals(
286 2, 288 2,
287 count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'uSeRFrIendlY -')) 289 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TEXT, 'uSeRFrIendlY -'))
288 ); 290 );
289 291
290 // use miscellaneous case and offset 292 // use miscellaneous case and offset
291 $this->assertEquals( 293 $this->assertEquals(
292 2, 294 2,
293 count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'RFrIendL')) 295 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TEXT, 'RFrIendL'))
294 ); 296 );
295 } 297 }
296 298
@@ -301,17 +303,17 @@ class LinkFilterTest extends \PHPUnit\Framework\TestCase
301 { 303 {
302 $this->assertEquals( 304 $this->assertEquals(
303 1, 305 1,
304 count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'publishing media')) 306 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TEXT, 'publishing media'))
305 ); 307 );
306 308
307 $this->assertEquals( 309 $this->assertEquals(
308 1, 310 1,
309 count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'mercurial w3c')) 311 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TEXT, 'mercurial w3c'))
310 ); 312 );
311 313
312 $this->assertEquals( 314 $this->assertEquals(
313 3, 315 3,
314 count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, '"free software"')) 316 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TEXT, '"free software"'))
315 ); 317 );
316 } 318 }
317 319
@@ -322,29 +324,29 @@ class LinkFilterTest extends \PHPUnit\Framework\TestCase
322 { 324 {
323 $this->assertEquals( 325 $this->assertEquals(
324 6, 326 6,
325 count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'web')) 327 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TEXT, 'web'))
326 ); 328 );
327 329
328 $this->assertEquals( 330 $this->assertEquals(
329 6, 331 6,
330 count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'web', 'all')) 332 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TEXT, 'web', 'all'))
331 ); 333 );
332 334
333 $this->assertEquals( 335 $this->assertEquals(
334 6, 336 6,
335 count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'web', 'bla')) 337 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TEXT, 'web', 'bla'))
336 ); 338 );
337 339
338 // Private only. 340 // Private only.
339 $this->assertEquals( 341 $this->assertEquals(
340 1, 342 1,
341 count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'web', false, 'private')) 343 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TEXT, 'web', false, 'private'))
342 ); 344 );
343 345
344 // Public only. 346 // Public only.
345 $this->assertEquals( 347 $this->assertEquals(
346 5, 348 5,
347 count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'web', false, 'public')) 349 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TEXT, 'web', false, 'public'))
348 ); 350 );
349 } 351 }
350 352
@@ -355,7 +357,7 @@ class LinkFilterTest extends \PHPUnit\Framework\TestCase
355 { 357 {
356 $this->assertEquals( 358 $this->assertEquals(
357 3, 359 3,
358 count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'free software')) 360 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TEXT, 'free software'))
359 ); 361 );
360 } 362 }
361 363
@@ -366,12 +368,12 @@ class LinkFilterTest extends \PHPUnit\Framework\TestCase
366 { 368 {
367 $this->assertEquals( 369 $this->assertEquals(
368 1, 370 1,
369 count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'free -gnu')) 371 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TEXT, 'free -gnu'))
370 ); 372 );
371 373
372 $this->assertEquals( 374 $this->assertEquals(
373 ReferenceLinkDB::$NB_LINKS_TOTAL - 1, 375 ReferenceLinkDB::$NB_LINKS_TOTAL - 1,
374 count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, '-revolution')) 376 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TEXT, '-revolution'))
375 ); 377 );
376 } 378 }
377 379
@@ -383,7 +385,7 @@ class LinkFilterTest extends \PHPUnit\Framework\TestCase
383 $this->assertEquals( 385 $this->assertEquals(
384 2, 386 2,
385 count(self::$linkFilter->filter( 387 count(self::$linkFilter->filter(
386 LinkFilter::$FILTER_TEXT, 388 LegacyLinkFilter::$FILTER_TEXT,
387 '"Free Software " stallman "read this" @website stuff' 389 '"Free Software " stallman "read this" @website stuff'
388 )) 390 ))
389 ); 391 );
@@ -391,7 +393,7 @@ class LinkFilterTest extends \PHPUnit\Framework\TestCase
391 $this->assertEquals( 393 $this->assertEquals(
392 1, 394 1,
393 count(self::$linkFilter->filter( 395 count(self::$linkFilter->filter(
394 LinkFilter::$FILTER_TEXT, 396 LegacyLinkFilter::$FILTER_TEXT,
395 '"free software " stallman "read this" -beard @website stuff' 397 '"free software " stallman "read this" -beard @website stuff'
396 )) 398 ))
397 ); 399 );
@@ -405,7 +407,7 @@ class LinkFilterTest extends \PHPUnit\Framework\TestCase
405 $this->assertEquals( 407 $this->assertEquals(
406 0, 408 0,
407 count(self::$linkFilter->filter( 409 count(self::$linkFilter->filter(
408 LinkFilter::$FILTER_TEXT, 410 LegacyLinkFilter::$FILTER_TEXT,
409 '"designer naming"' 411 '"designer naming"'
410 )) 412 ))
411 ); 413 );
@@ -413,7 +415,7 @@ class LinkFilterTest extends \PHPUnit\Framework\TestCase
413 $this->assertEquals( 415 $this->assertEquals(
414 0, 416 0,
415 count(self::$linkFilter->filter( 417 count(self::$linkFilter->filter(
416 LinkFilter::$FILTER_TEXT, 418 LegacyLinkFilter::$FILTER_TEXT,
417 '"designernaming"' 419 '"designernaming"'
418 )) 420 ))
419 ); 421 );
@@ -426,12 +428,12 @@ class LinkFilterTest extends \PHPUnit\Framework\TestCase
426 { 428 {
427 $this->assertEquals( 429 $this->assertEquals(
428 1, 430 1,
429 count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'gnu -free')) 431 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TAG, 'gnu -free'))
430 ); 432 );
431 433
432 $this->assertEquals( 434 $this->assertEquals(
433 ReferenceLinkDB::$NB_LINKS_TOTAL - 1, 435 ReferenceLinkDB::$NB_LINKS_TOTAL - 1,
434 count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, '-free')) 436 count(self::$linkFilter->filter(LegacyLinkFilter::$FILTER_TAG, '-free'))
435 ); 437 );
436 } 438 }
437 439
@@ -445,42 +447,42 @@ class LinkFilterTest extends \PHPUnit\Framework\TestCase
445 $this->assertEquals( 447 $this->assertEquals(
446 1, 448 1,
447 count(self::$linkFilter->filter( 449 count(self::$linkFilter->filter(
448 LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT, 450 LegacyLinkFilter::$FILTER_TAG | LegacyLinkFilter::$FILTER_TEXT,
449 array($tags, $terms) 451 array($tags, $terms)
450 )) 452 ))
451 ); 453 );
452 $this->assertEquals( 454 $this->assertEquals(
453 2, 455 2,
454 count(self::$linkFilter->filter( 456 count(self::$linkFilter->filter(
455 LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT, 457 LegacyLinkFilter::$FILTER_TAG | LegacyLinkFilter::$FILTER_TEXT,
456 array('', $terms) 458 array('', $terms)
457 )) 459 ))
458 ); 460 );
459 $this->assertEquals( 461 $this->assertEquals(
460 1, 462 1,
461 count(self::$linkFilter->filter( 463 count(self::$linkFilter->filter(
462 LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT, 464 LegacyLinkFilter::$FILTER_TAG | LegacyLinkFilter::$FILTER_TEXT,
463 array(false, 'PSR-2') 465 array(false, 'PSR-2')
464 )) 466 ))
465 ); 467 );
466 $this->assertEquals( 468 $this->assertEquals(
467 1, 469 1,
468 count(self::$linkFilter->filter( 470 count(self::$linkFilter->filter(
469 LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT, 471 LegacyLinkFilter::$FILTER_TAG | LegacyLinkFilter::$FILTER_TEXT,
470 array($tags, '') 472 array($tags, '')
471 )) 473 ))
472 ); 474 );
473 $this->assertEquals( 475 $this->assertEquals(
474 ReferenceLinkDB::$NB_LINKS_TOTAL, 476 ReferenceLinkDB::$NB_LINKS_TOTAL,
475 count(self::$linkFilter->filter( 477 count(self::$linkFilter->filter(
476 LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT, 478 LegacyLinkFilter::$FILTER_TAG | LegacyLinkFilter::$FILTER_TEXT,
477 '' 479 ''
478 )) 480 ))
479 ); 481 );
480 } 482 }
481 483
482 /** 484 /**
483 * Filter links by #hashtag. 485 * Filter bookmarks by #hashtag.
484 */ 486 */
485 public function testFilterByHashtag() 487 public function testFilterByHashtag()
486 { 488 {
@@ -488,7 +490,7 @@ class LinkFilterTest extends \PHPUnit\Framework\TestCase
488 $this->assertEquals( 490 $this->assertEquals(
489 3, 491 3,
490 count(self::$linkFilter->filter( 492 count(self::$linkFilter->filter(
491 LinkFilter::$FILTER_TAG, 493 LegacyLinkFilter::$FILTER_TAG,
492 $hashtag 494 $hashtag
493 )) 495 ))
494 ); 496 );
@@ -497,7 +499,7 @@ class LinkFilterTest extends \PHPUnit\Framework\TestCase
497 $this->assertEquals( 499 $this->assertEquals(
498 1, 500 1,
499 count(self::$linkFilter->filter( 501 count(self::$linkFilter->filter(
500 LinkFilter::$FILTER_TAG, 502 LegacyLinkFilter::$FILTER_TAG,
501 $hashtag, 503 $hashtag,
502 false, 504 false,
503 'private' 505 'private'
diff --git a/tests/legacy/LegacyUpdaterTest.php b/tests/legacy/LegacyUpdaterTest.php
new file mode 100644
index 00000000..7c429811
--- /dev/null
+++ b/tests/legacy/LegacyUpdaterTest.php
@@ -0,0 +1,886 @@
1<?php
2namespace Shaarli\Updater;
3
4use DateTime;
5use Exception;
6use Shaarli\Bookmark\Bookmark;
7use Shaarli\Config\ConfigJson;
8use Shaarli\Config\ConfigManager;
9use Shaarli\Config\ConfigPhp;
10use Shaarli\Legacy\LegacyLinkDB;
11use Shaarli\Legacy\LegacyUpdater;
12use Shaarli\Thumbnailer;
13
14require_once 'application/updater/UpdaterUtils.php';
15require_once 'tests/updater/DummyUpdater.php';
16require_once 'tests/utils/ReferenceLinkDB.php';
17require_once 'inc/rain.tpl.class.php';
18
19/**
20 * Class UpdaterTest.
21 * Runs unit tests against the updater class.
22 */
23class LegacyUpdaterTest extends \PHPUnit\Framework\TestCase
24{
25 /**
26 * @var string Path to test datastore.
27 */
28 protected static $testDatastore = 'sandbox/datastore.php';
29
30 /**
31 * @var string Config file path (without extension).
32 */
33 protected static $configFile = 'sandbox/config';
34
35 /**
36 * @var ConfigManager
37 */
38 protected $conf;
39
40 /**
41 * Executed before each test.
42 */
43 public function setUp()
44 {
45 copy('tests/utils/config/configJson.json.php', self::$configFile .'.json.php');
46 $this->conf = new ConfigManager(self::$configFile);
47 }
48
49 /**
50 * Test UpdaterUtils::read_updates_file with an empty/missing file.
51 */
52 public function testReadEmptyUpdatesFile()
53 {
54 $this->assertEquals(array(), UpdaterUtils::read_updates_file(''));
55 $updatesFile = $this->conf->get('resource.data_dir') . '/updates.txt';
56 touch($updatesFile);
57 $this->assertEquals(array(), UpdaterUtils::read_updates_file($updatesFile));
58 unlink($updatesFile);
59 }
60
61 /**
62 * Test read/write updates file.
63 */
64 public function testReadWriteUpdatesFile()
65 {
66 $updatesFile = $this->conf->get('resource.data_dir') . '/updates.txt';
67 $updatesMethods = array('m1', 'm2', 'm3');
68
69 UpdaterUtils::write_updates_file($updatesFile, $updatesMethods);
70 $readMethods = UpdaterUtils::read_updates_file($updatesFile);
71 $this->assertEquals($readMethods, $updatesMethods);
72
73 // Update
74 $updatesMethods[] = 'm4';
75 UpdaterUtils::write_updates_file($updatesFile, $updatesMethods);
76 $readMethods = UpdaterUtils::read_updates_file($updatesFile);
77 $this->assertEquals($readMethods, $updatesMethods);
78 unlink($updatesFile);
79 }
80
81 /**
82 * Test errors in UpdaterUtils::write_updates_file(): empty updates file.
83 *
84 * @expectedException Exception
85 * @expectedExceptionMessageRegExp /Updates file path is not set(.*)/
86 */
87 public function testWriteEmptyUpdatesFile()
88 {
89 UpdaterUtils::write_updates_file('', array('test'));
90 }
91
92 /**
93 * Test errors in UpdaterUtils::write_updates_file(): not writable updates file.
94 *
95 * @expectedException Exception
96 * @expectedExceptionMessageRegExp /Unable to write(.*)/
97 */
98 public function testWriteUpdatesFileNotWritable()
99 {
100 $updatesFile = $this->conf->get('resource.data_dir') . '/updates.txt';
101 touch($updatesFile);
102 chmod($updatesFile, 0444);
103 try {
104 @UpdaterUtils::write_updates_file($updatesFile, array('test'));
105 } catch (Exception $e) {
106 unlink($updatesFile);
107 throw $e;
108 }
109 }
110
111 /**
112 * Test the update() method, with no update to run.
113 * 1. Everything already run.
114 * 2. User is logged out.
115 */
116 public function testNoUpdates()
117 {
118 $updates = array(
119 'updateMethodDummy1',
120 'updateMethodDummy2',
121 'updateMethodDummy3',
122 'updateMethodException',
123 );
124 $updater = new DummyUpdater($updates, array(), $this->conf, true);
125 $this->assertEquals(array(), $updater->update());
126
127 $updater = new DummyUpdater(array(), array(), $this->conf, false);
128 $this->assertEquals(array(), $updater->update());
129 }
130
131 /**
132 * Test the update() method, with all updates to run (except the failing one).
133 */
134 public function testUpdatesFirstTime()
135 {
136 $updates = array('updateMethodException',);
137 $expectedUpdates = array(
138 'updateMethodDummy1',
139 'updateMethodDummy2',
140 'updateMethodDummy3',
141 );
142 $updater = new DummyUpdater($updates, array(), $this->conf, true);
143 $this->assertEquals($expectedUpdates, $updater->update());
144 }
145
146 /**
147 * Test the update() method, only one update to run.
148 */
149 public function testOneUpdate()
150 {
151 $updates = array(
152 'updateMethodDummy1',
153 'updateMethodDummy3',
154 'updateMethodException',
155 );
156 $expectedUpdate = array('updateMethodDummy2');
157
158 $updater = new DummyUpdater($updates, array(), $this->conf, true);
159 $this->assertEquals($expectedUpdate, $updater->update());
160 }
161
162 /**
163 * Test Update failed.
164 *
165 * @expectedException \Exception
166 */
167 public function testUpdateFailed()
168 {
169 $updates = array(
170 'updateMethodDummy1',
171 'updateMethodDummy2',
172 'updateMethodDummy3',
173 );
174
175 $updater = new DummyUpdater($updates, array(), $this->conf, true);
176 $updater->update();
177 }
178
179 /**
180 * Test update mergeDeprecatedConfig:
181 * 1. init a config file.
182 * 2. init a options.php file with update value.
183 * 3. merge.
184 * 4. check updated value in config file.
185 */
186 public function testUpdateMergeDeprecatedConfig()
187 {
188 $this->conf->setConfigFile('tests/utils/config/configPhp');
189 $this->conf->reset();
190
191 $optionsFile = 'tests/updater/options.php';
192 $options = '<?php
193$GLOBALS[\'privateLinkByDefault\'] = true;';
194 file_put_contents($optionsFile, $options);
195
196 // tmp config file.
197 $this->conf->setConfigFile('tests/updater/config');
198
199 // merge configs
200 $updater = new LegacyUpdater(array(), array(), $this->conf, true);
201 // This writes a new config file in tests/updater/config.php
202 $updater->updateMethodMergeDeprecatedConfigFile();
203
204 // make sure updated field is changed
205 $this->conf->reload();
206 $this->assertTrue($this->conf->get('privacy.default_private_links'));
207 $this->assertFalse(is_file($optionsFile));
208 // Delete the generated file.
209 unlink($this->conf->getConfigFileExt());
210 }
211
212 /**
213 * Test mergeDeprecatedConfig in without options file.
214 */
215 public function testMergeDeprecatedConfigNoFile()
216 {
217 $updater = new LegacyUpdater(array(), array(), $this->conf, true);
218 $updater->updateMethodMergeDeprecatedConfigFile();
219
220 $this->assertEquals('root', $this->conf->get('credentials.login'));
221 }
222
223 /**
224 * Test renameDashTags update method.
225 */
226 public function testRenameDashTags()
227 {
228 $refDB = new \ReferenceLinkDB(true);
229 $refDB->write(self::$testDatastore);
230 $linkDB = new LegacyLinkDB(self::$testDatastore, true, false);
231
232 $this->assertEmpty($linkDB->filterSearch(array('searchtags' => 'exclude')));
233 $updater = new LegacyUpdater(array(), $linkDB, $this->conf, true);
234 $updater->updateMethodRenameDashTags();
235 $this->assertNotEmpty($linkDB->filterSearch(array('searchtags' => 'exclude')));
236 }
237
238 /**
239 * Convert old PHP config file to JSON config.
240 */
241 public function testConfigToJson()
242 {
243 $configFile = 'tests/utils/config/configPhp';
244 $this->conf->setConfigFile($configFile);
245 $this->conf->reset();
246
247 // The ConfigIO is initialized with ConfigPhp.
248 $this->assertTrue($this->conf->getConfigIO() instanceof ConfigPhp);
249
250 $updater = new LegacyUpdater(array(), array(), $this->conf, false);
251 $done = $updater->updateMethodConfigToJson();
252 $this->assertTrue($done);
253
254 // The ConfigIO has been updated to ConfigJson.
255 $this->assertTrue($this->conf->getConfigIO() instanceof ConfigJson);
256 $this->assertTrue(file_exists($this->conf->getConfigFileExt()));
257
258 // Check JSON config data.
259 $this->conf->reload();
260 $this->assertEquals('root', $this->conf->get('credentials.login'));
261 $this->assertEquals('lala', $this->conf->get('redirector.url'));
262 $this->assertEquals('data/datastore.php', $this->conf->get('resource.datastore'));
263 $this->assertEquals('1', $this->conf->get('plugins.WALLABAG_VERSION'));
264
265 rename($configFile . '.save.php', $configFile . '.php');
266 unlink($this->conf->getConfigFileExt());
267 }
268
269 /**
270 * Launch config conversion update with an existing JSON file => nothing to do.
271 */
272 public function testConfigToJsonNothingToDo()
273 {
274 $filetime = filemtime($this->conf->getConfigFileExt());
275 $updater = new LegacyUpdater(array(), array(), $this->conf, false);
276 $done = $updater->updateMethodConfigToJson();
277 $this->assertTrue($done);
278 $expected = filemtime($this->conf->getConfigFileExt());
279 $this->assertEquals($expected, $filetime);
280 }
281
282 /**
283 * Test escapeUnescapedConfig with valid data.
284 */
285 public function testEscapeConfig()
286 {
287 $sandbox = 'sandbox/config';
288 copy(self::$configFile . '.json.php', $sandbox . '.json.php');
289 $this->conf = new ConfigManager($sandbox);
290 $title = '<script>alert("title");</script>';
291 $headerLink = '<script>alert("header_link");</script>';
292 $this->conf->set('general.title', $title);
293 $this->conf->set('general.header_link', $headerLink);
294 $updater = new LegacyUpdater(array(), array(), $this->conf, true);
295 $done = $updater->updateMethodEscapeUnescapedConfig();
296 $this->assertTrue($done);
297 $this->conf->reload();
298 $this->assertEquals(escape($title), $this->conf->get('general.title'));
299 $this->assertEquals(escape($headerLink), $this->conf->get('general.header_link'));
300 unlink($sandbox . '.json.php');
301 }
302
303 /**
304 * Test updateMethodApiSettings(): create default settings for the API (enabled + secret).
305 */
306 public function testUpdateApiSettings()
307 {
308 $confFile = 'sandbox/config';
309 copy(self::$configFile .'.json.php', $confFile .'.json.php');
310 $conf = new ConfigManager($confFile);
311 $updater = new LegacyUpdater(array(), array(), $conf, true);
312
313 $this->assertFalse($conf->exists('api.enabled'));
314 $this->assertFalse($conf->exists('api.secret'));
315 $updater->updateMethodApiSettings();
316 $conf->reload();
317 $this->assertTrue($conf->get('api.enabled'));
318 $this->assertTrue($conf->exists('api.secret'));
319 unlink($confFile .'.json.php');
320 }
321
322 /**
323 * Test updateMethodApiSettings(): already set, do nothing.
324 */
325 public function testUpdateApiSettingsNothingToDo()
326 {
327 $confFile = 'sandbox/config';
328 copy(self::$configFile .'.json.php', $confFile .'.json.php');
329 $conf = new ConfigManager($confFile);
330 $conf->set('api.enabled', false);
331 $conf->set('api.secret', '');
332 $updater = new LegacyUpdater(array(), array(), $conf, true);
333 $updater->updateMethodApiSettings();
334 $this->assertFalse($conf->get('api.enabled'));
335 $this->assertEmpty($conf->get('api.secret'));
336 unlink($confFile .'.json.php');
337 }
338
339 /**
340 * Test updateMethodDatastoreIds().
341 */
342 public function testDatastoreIds()
343 {
344 $links = array(
345 '20121206_182539' => array(
346 'linkdate' => '20121206_182539',
347 'title' => 'Geek and Poke',
348 'url' => 'http://geek-and-poke.com/',
349 'description' => 'desc',
350 'tags' => 'dev cartoon tag1 tag2 tag3 tag4 ',
351 'updated' => '20121206_190301',
352 'private' => false,
353 ),
354 '20121206_172539' => array(
355 'linkdate' => '20121206_172539',
356 'title' => 'UserFriendly - Samba',
357 'url' => 'http://ars.userfriendly.org/cartoons/?id=20010306',
358 'description' => '',
359 'tags' => 'samba cartoon web',
360 'private' => false,
361 ),
362 '20121206_142300' => array(
363 'linkdate' => '20121206_142300',
364 'title' => 'UserFriendly - Web Designer',
365 'url' => 'http://ars.userfriendly.org/cartoons/?id=20121206',
366 'description' => 'Naming conventions... #private',
367 'tags' => 'samba cartoon web',
368 'private' => true,
369 ),
370 );
371 $refDB = new \ReferenceLinkDB(true);
372 $refDB->setLinks($links);
373 $refDB->write(self::$testDatastore);
374 $linkDB = new LegacyLinkDB(self::$testDatastore, true, false);
375
376 $checksum = hash_file('sha1', self::$testDatastore);
377
378 $this->conf->set('resource.data_dir', 'sandbox');
379 $this->conf->set('resource.datastore', self::$testDatastore);
380
381 $updater = new LegacyUpdater(array(), $linkDB, $this->conf, true);
382 $this->assertTrue($updater->updateMethodDatastoreIds());
383
384 $linkDB = new LegacyLinkDB(self::$testDatastore, true, false);
385
386 $backupFiles = glob($this->conf->get('resource.data_dir') . '/datastore.'. date('YmdH') .'*.php');
387 $backup = null;
388 foreach ($backupFiles as $backupFile) {
389 if (strpos($backupFile, '_1') === false) {
390 $backup = $backupFile;
391 }
392 }
393 $this->assertNotNull($backup);
394 $this->assertFileExists($backup);
395 $this->assertEquals($checksum, hash_file('sha1', $backup));
396 unlink($backup);
397
398 $this->assertEquals(3, count($linkDB));
399 $this->assertTrue(isset($linkDB[0]));
400 $this->assertFalse(isset($linkDB[0]['linkdate']));
401 $this->assertEquals(0, $linkDB[0]['id']);
402 $this->assertEquals('UserFriendly - Web Designer', $linkDB[0]['title']);
403 $this->assertEquals('http://ars.userfriendly.org/cartoons/?id=20121206', $linkDB[0]['url']);
404 $this->assertEquals('Naming conventions... #private', $linkDB[0]['description']);
405 $this->assertEquals('samba cartoon web', $linkDB[0]['tags']);
406 $this->assertTrue($linkDB[0]['private']);
407 $this->assertEquals(
408 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20121206_142300'),
409 $linkDB[0]['created']
410 );
411
412 $this->assertTrue(isset($linkDB[1]));
413 $this->assertFalse(isset($linkDB[1]['linkdate']));
414 $this->assertEquals(1, $linkDB[1]['id']);
415 $this->assertEquals('UserFriendly - Samba', $linkDB[1]['title']);
416 $this->assertEquals(
417 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20121206_172539'),
418 $linkDB[1]['created']
419 );
420
421 $this->assertTrue(isset($linkDB[2]));
422 $this->assertFalse(isset($linkDB[2]['linkdate']));
423 $this->assertEquals(2, $linkDB[2]['id']);
424 $this->assertEquals('Geek and Poke', $linkDB[2]['title']);
425 $this->assertEquals(
426 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20121206_182539'),
427 $linkDB[2]['created']
428 );
429 $this->assertEquals(
430 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20121206_190301'),
431 $linkDB[2]['updated']
432 );
433 }
434
435 /**
436 * Test updateMethodDatastoreIds() with the update already applied: nothing to do.
437 */
438 public function testDatastoreIdsNothingToDo()
439 {
440 $refDB = new \ReferenceLinkDB(true);
441 $refDB->write(self::$testDatastore);
442 $linkDB = new LegacyLinkDB(self::$testDatastore, true, false);
443
444 $this->conf->set('resource.data_dir', 'sandbox');
445 $this->conf->set('resource.datastore', self::$testDatastore);
446
447 $checksum = hash_file('sha1', self::$testDatastore);
448 $updater = new LegacyUpdater(array(), $linkDB, $this->conf, true);
449 $this->assertTrue($updater->updateMethodDatastoreIds());
450 $this->assertEquals($checksum, hash_file('sha1', self::$testDatastore));
451 }
452
453 /**
454 * Test defaultTheme update with default settings: nothing to do.
455 */
456 public function testDefaultThemeWithDefaultSettings()
457 {
458 $sandbox = 'sandbox/config';
459 copy(self::$configFile . '.json.php', $sandbox . '.json.php');
460 $this->conf = new ConfigManager($sandbox);
461 $updater = new LegacyUpdater([], [], $this->conf, true);
462 $this->assertTrue($updater->updateMethodDefaultTheme());
463
464 $this->assertEquals('tpl/', $this->conf->get('resource.raintpl_tpl'));
465 $this->assertEquals('default', $this->conf->get('resource.theme'));
466 $this->conf = new ConfigManager($sandbox);
467 $this->assertEquals('tpl/', $this->conf->get('resource.raintpl_tpl'));
468 $this->assertEquals('default', $this->conf->get('resource.theme'));
469 unlink($sandbox . '.json.php');
470 }
471
472 /**
473 * Test defaultTheme update with a custom theme in a subfolder
474 */
475 public function testDefaultThemeWithCustomTheme()
476 {
477 $theme = 'iamanartist';
478 $sandbox = 'sandbox/config';
479 copy(self::$configFile . '.json.php', $sandbox . '.json.php');
480 $this->conf = new ConfigManager($sandbox);
481 mkdir('sandbox/'. $theme);
482 touch('sandbox/'. $theme .'/linklist.html');
483 $this->conf->set('resource.raintpl_tpl', 'sandbox/'. $theme .'/');
484 $updater = new LegacyUpdater([], [], $this->conf, true);
485 $this->assertTrue($updater->updateMethodDefaultTheme());
486
487 $this->assertEquals('sandbox', $this->conf->get('resource.raintpl_tpl'));
488 $this->assertEquals($theme, $this->conf->get('resource.theme'));
489 $this->conf = new ConfigManager($sandbox);
490 $this->assertEquals('sandbox', $this->conf->get('resource.raintpl_tpl'));
491 $this->assertEquals($theme, $this->conf->get('resource.theme'));
492 unlink($sandbox . '.json.php');
493 unlink('sandbox/'. $theme .'/linklist.html');
494 rmdir('sandbox/'. $theme);
495 }
496
497 /**
498 * Test updateMethodEscapeMarkdown with markdown plugin enabled
499 * => setting markdown_escape set to false.
500 */
501 public function testEscapeMarkdownSettingToFalse()
502 {
503 $sandboxConf = 'sandbox/config';
504 copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
505 $this->conf = new ConfigManager($sandboxConf);
506
507 $this->conf->set('general.enabled_plugins', ['markdown']);
508 $updater = new LegacyUpdater([], [], $this->conf, true);
509 $this->assertTrue($updater->updateMethodEscapeMarkdown());
510 $this->assertFalse($this->conf->get('security.markdown_escape'));
511
512 // reload from file
513 $this->conf = new ConfigManager($sandboxConf);
514 $this->assertFalse($this->conf->get('security.markdown_escape'));
515 }
516
517
518 /**
519 * Test updateMethodEscapeMarkdown with markdown plugin disabled
520 * => setting markdown_escape set to true.
521 */
522 public function testEscapeMarkdownSettingToTrue()
523 {
524 $sandboxConf = 'sandbox/config';
525 copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
526 $this->conf = new ConfigManager($sandboxConf);
527
528 $this->conf->set('general.enabled_plugins', []);
529 $updater = new LegacyUpdater([], [], $this->conf, true);
530 $this->assertTrue($updater->updateMethodEscapeMarkdown());
531 $this->assertTrue($this->conf->get('security.markdown_escape'));
532
533 // reload from file
534 $this->conf = new ConfigManager($sandboxConf);
535 $this->assertTrue($this->conf->get('security.markdown_escape'));
536 }
537
538 /**
539 * Test updateMethodEscapeMarkdown with nothing to do (setting already enabled)
540 */
541 public function testEscapeMarkdownSettingNothingToDoEnabled()
542 {
543 $sandboxConf = 'sandbox/config';
544 copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
545 $this->conf = new ConfigManager($sandboxConf);
546 $this->conf->set('security.markdown_escape', true);
547 $updater = new LegacyUpdater([], [], $this->conf, true);
548 $this->assertTrue($updater->updateMethodEscapeMarkdown());
549 $this->assertTrue($this->conf->get('security.markdown_escape'));
550 }
551
552 /**
553 * Test updateMethodEscapeMarkdown with nothing to do (setting already disabled)
554 */
555 public function testEscapeMarkdownSettingNothingToDoDisabled()
556 {
557 $this->conf->set('security.markdown_escape', false);
558 $updater = new LegacyUpdater([], [], $this->conf, true);
559 $this->assertTrue($updater->updateMethodEscapeMarkdown());
560 $this->assertFalse($this->conf->get('security.markdown_escape'));
561 }
562
563 /**
564 * Test updateMethodPiwikUrl with valid data
565 */
566 public function testUpdatePiwikUrlValid()
567 {
568 $sandboxConf = 'sandbox/config';
569 copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
570 $this->conf = new ConfigManager($sandboxConf);
571 $url = 'mypiwik.tld';
572 $this->conf->set('plugins.PIWIK_URL', $url);
573 $updater = new LegacyUpdater([], [], $this->conf, true);
574 $this->assertTrue($updater->updateMethodPiwikUrl());
575 $this->assertEquals('http://'. $url, $this->conf->get('plugins.PIWIK_URL'));
576
577 // reload from file
578 $this->conf = new ConfigManager($sandboxConf);
579 $this->assertEquals('http://'. $url, $this->conf->get('plugins.PIWIK_URL'));
580 }
581
582 /**
583 * Test updateMethodPiwikUrl without setting
584 */
585 public function testUpdatePiwikUrlEmpty()
586 {
587 $updater = new LegacyUpdater([], [], $this->conf, true);
588 $this->assertTrue($updater->updateMethodPiwikUrl());
589 $this->assertEmpty($this->conf->get('plugins.PIWIK_URL'));
590 }
591
592 /**
593 * Test updateMethodPiwikUrl: valid URL, nothing to do
594 */
595 public function testUpdatePiwikUrlNothingToDo()
596 {
597 $url = 'https://mypiwik.tld';
598 $this->conf->set('plugins.PIWIK_URL', $url);
599 $updater = new LegacyUpdater([], [], $this->conf, true);
600 $this->assertTrue($updater->updateMethodPiwikUrl());
601 $this->assertEquals($url, $this->conf->get('plugins.PIWIK_URL'));
602 }
603
604 /**
605 * Test updateMethodAtomDefault with show_atom set to false
606 * => update to true.
607 */
608 public function testUpdateMethodAtomDefault()
609 {
610 $sandboxConf = 'sandbox/config';
611 copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
612 $this->conf = new ConfigManager($sandboxConf);
613 $this->conf->set('feed.show_atom', false);
614 $updater = new LegacyUpdater([], [], $this->conf, true);
615 $this->assertTrue($updater->updateMethodAtomDefault());
616 $this->assertTrue($this->conf->get('feed.show_atom'));
617 // reload from file
618 $this->conf = new ConfigManager($sandboxConf);
619 $this->assertTrue($this->conf->get('feed.show_atom'));
620 }
621 /**
622 * Test updateMethodAtomDefault with show_atom not set.
623 * => nothing to do
624 */
625 public function testUpdateMethodAtomDefaultNoExist()
626 {
627 $sandboxConf = 'sandbox/config';
628 copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
629 $this->conf = new ConfigManager($sandboxConf);
630 $updater = new LegacyUpdater([], [], $this->conf, true);
631 $this->assertTrue($updater->updateMethodAtomDefault());
632 $this->assertTrue($this->conf->get('feed.show_atom'));
633 }
634 /**
635 * Test updateMethodAtomDefault with show_atom set to true.
636 * => nothing to do
637 */
638 public function testUpdateMethodAtomDefaultAlreadyTrue()
639 {
640 $sandboxConf = 'sandbox/config';
641 copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
642 $this->conf = new ConfigManager($sandboxConf);
643 $this->conf->set('feed.show_atom', true);
644 $updater = new LegacyUpdater([], [], $this->conf, true);
645 $this->assertTrue($updater->updateMethodAtomDefault());
646 $this->assertTrue($this->conf->get('feed.show_atom'));
647 }
648
649 /**
650 * Test updateMethodDownloadSizeAndTimeoutConf, it should be set if none is already defined.
651 */
652 public function testUpdateMethodDownloadSizeAndTimeoutConf()
653 {
654 $sandboxConf = 'sandbox/config';
655 copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
656 $this->conf = new ConfigManager($sandboxConf);
657 $updater = new LegacyUpdater([], [], $this->conf, true);
658 $this->assertTrue($updater->updateMethodDownloadSizeAndTimeoutConf());
659 $this->assertEquals(4194304, $this->conf->get('general.download_max_size'));
660 $this->assertEquals(30, $this->conf->get('general.download_timeout'));
661
662 $this->conf = new ConfigManager($sandboxConf);
663 $this->assertEquals(4194304, $this->conf->get('general.download_max_size'));
664 $this->assertEquals(30, $this->conf->get('general.download_timeout'));
665 }
666
667 /**
668 * Test updateMethodDownloadSizeAndTimeoutConf, it shouldn't be set if it is already defined.
669 */
670 public function testUpdateMethodDownloadSizeAndTimeoutConfIgnore()
671 {
672 $sandboxConf = 'sandbox/config';
673 copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
674 $this->conf = new ConfigManager($sandboxConf);
675 $this->conf->set('general.download_max_size', 38);
676 $this->conf->set('general.download_timeout', 70);
677 $updater = new LegacyUpdater([], [], $this->conf, true);
678 $this->assertTrue($updater->updateMethodDownloadSizeAndTimeoutConf());
679 $this->assertEquals(38, $this->conf->get('general.download_max_size'));
680 $this->assertEquals(70, $this->conf->get('general.download_timeout'));
681 }
682
683 /**
684 * Test updateMethodDownloadSizeAndTimeoutConf, only the maz size should be set here.
685 */
686 public function testUpdateMethodDownloadSizeAndTimeoutConfOnlySize()
687 {
688 $sandboxConf = 'sandbox/config';
689 copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
690 $this->conf = new ConfigManager($sandboxConf);
691 $this->conf->set('general.download_max_size', 38);
692 $updater = new LegacyUpdater([], [], $this->conf, true);
693 $this->assertTrue($updater->updateMethodDownloadSizeAndTimeoutConf());
694 $this->assertEquals(38, $this->conf->get('general.download_max_size'));
695 $this->assertEquals(30, $this->conf->get('general.download_timeout'));
696 }
697
698 /**
699 * Test updateMethodDownloadSizeAndTimeoutConf, only the time out should be set here.
700 */
701 public function testUpdateMethodDownloadSizeAndTimeoutConfOnlyTimeout()
702 {
703 $sandboxConf = 'sandbox/config';
704 copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
705 $this->conf = new ConfigManager($sandboxConf);
706 $this->conf->set('general.download_timeout', 3);
707 $updater = new LegacyUpdater([], [], $this->conf, true);
708 $this->assertTrue($updater->updateMethodDownloadSizeAndTimeoutConf());
709 $this->assertEquals(4194304, $this->conf->get('general.download_max_size'));
710 $this->assertEquals(3, $this->conf->get('general.download_timeout'));
711 }
712
713 /**
714 * Test updateMethodWebThumbnailer with thumbnails enabled.
715 */
716 public function testUpdateMethodWebThumbnailerEnabled()
717 {
718 $this->conf->remove('thumbnails');
719 $this->conf->set('thumbnail.enable_thumbnails', true);
720 $updater = new LegacyUpdater([], [], $this->conf, true, $_SESSION);
721 $this->assertTrue($updater->updateMethodWebThumbnailer());
722 $this->assertFalse($this->conf->exists('thumbnail'));
723 $this->assertEquals(\Shaarli\Thumbnailer::MODE_ALL, $this->conf->get('thumbnails.mode'));
724 $this->assertEquals(125, $this->conf->get('thumbnails.width'));
725 $this->assertEquals(90, $this->conf->get('thumbnails.height'));
726 $this->assertContains('You have enabled or changed thumbnails', $_SESSION['warnings'][0]);
727 }
728
729 /**
730 * Test updateMethodWebThumbnailer with thumbnails disabled.
731 */
732 public function testUpdateMethodWebThumbnailerDisabled()
733 {
734 if (isset($_SESSION['warnings'])) {
735 unset($_SESSION['warnings']);
736 }
737
738 $this->conf->remove('thumbnails');
739 $this->conf->set('thumbnail.enable_thumbnails', false);
740 $updater = new LegacyUpdater([], [], $this->conf, true, $_SESSION);
741 $this->assertTrue($updater->updateMethodWebThumbnailer());
742 $this->assertFalse($this->conf->exists('thumbnail'));
743 $this->assertEquals(Thumbnailer::MODE_NONE, $this->conf->get('thumbnails.mode'));
744 $this->assertEquals(125, $this->conf->get('thumbnails.width'));
745 $this->assertEquals(90, $this->conf->get('thumbnails.height'));
746 $this->assertTrue(empty($_SESSION['warnings']));
747 }
748
749 /**
750 * Test updateMethodWebThumbnailer with thumbnails disabled.
751 */
752 public function testUpdateMethodWebThumbnailerNothingToDo()
753 {
754 if (isset($_SESSION['warnings'])) {
755 unset($_SESSION['warnings']);
756 }
757
758 $updater = new LegacyUpdater([], [], $this->conf, true, $_SESSION);
759 $this->assertTrue($updater->updateMethodWebThumbnailer());
760 $this->assertFalse($this->conf->exists('thumbnail'));
761 $this->assertEquals(Thumbnailer::MODE_COMMON, $this->conf->get('thumbnails.mode'));
762 $this->assertEquals(90, $this->conf->get('thumbnails.width'));
763 $this->assertEquals(53, $this->conf->get('thumbnails.height'));
764 $this->assertTrue(empty($_SESSION['warnings']));
765 }
766
767 /**
768 * Test updateMethodSetSticky().
769 */
770 public function testUpdateStickyValid()
771 {
772 $blank = [
773 'id' => 1,
774 'url' => 'z',
775 'title' => '',
776 'description' => '',
777 'tags' => '',
778 'created' => new DateTime(),
779 ];
780 $links = [
781 1 => ['id' => 1] + $blank,
782 2 => ['id' => 2] + $blank,
783 ];
784 $refDB = new \ReferenceLinkDB(true);
785 $refDB->setLinks($links);
786 $refDB->write(self::$testDatastore);
787 $linkDB = new LegacyLinkDB(self::$testDatastore, true, false);
788
789 $updater = new LegacyUpdater(array(), $linkDB, $this->conf, true);
790 $this->assertTrue($updater->updateMethodSetSticky());
791
792 $linkDB = new LegacyLinkDB(self::$testDatastore, true, false);
793 foreach ($linkDB as $link) {
794 $this->assertFalse($link['sticky']);
795 }
796 }
797
798 /**
799 * Test updateMethodSetSticky().
800 */
801 public function testUpdateStickyNothingToDo()
802 {
803 $blank = [
804 'id' => 1,
805 'url' => 'z',
806 'title' => '',
807 'description' => '',
808 'tags' => '',
809 'created' => new DateTime(),
810 ];
811 $links = [
812 1 => ['id' => 1, 'sticky' => true] + $blank,
813 2 => ['id' => 2] + $blank,
814 ];
815 $refDB = new \ReferenceLinkDB(true);
816 $refDB->setLinks($links);
817 $refDB->write(self::$testDatastore);
818 $linkDB = new LegacyLinkDB(self::$testDatastore, true, false);
819
820 $updater = new LegacyUpdater(array(), $linkDB, $this->conf, true);
821 $this->assertTrue($updater->updateMethodSetSticky());
822
823 $linkDB = new LegacyLinkDB(self::$testDatastore, true, false);
824 $this->assertTrue($linkDB[1]['sticky']);
825 }
826
827 /**
828 * Test updateMethodRemoveRedirector().
829 */
830 public function testUpdateRemoveRedirector()
831 {
832 $sandboxConf = 'sandbox/config';
833 copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
834 $this->conf = new ConfigManager($sandboxConf);
835 $updater = new LegacyUpdater([], null, $this->conf, true);
836 $this->assertTrue($updater->updateMethodRemoveRedirector());
837 $this->assertFalse($this->conf->exists('redirector'));
838 $this->conf = new ConfigManager($sandboxConf);
839 $this->assertFalse($this->conf->exists('redirector'));
840 }
841
842 /**
843 * Test updateMethodFormatterSetting()
844 */
845 public function testUpdateMethodFormatterSettingDefault()
846 {
847 $sandboxConf = 'sandbox/config';
848 copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
849 $this->conf = new ConfigManager($sandboxConf);
850 $this->conf->set('formatter', 'default');
851 $updater = new LegacyUpdater([], null, $this->conf, true);
852 $enabledPlugins = $this->conf->get('general.enabled_plugins');
853 $this->assertFalse(in_array('markdown', $enabledPlugins));
854 $this->assertTrue($updater->updateMethodFormatterSetting());
855 $this->assertEquals('default', $this->conf->get('formatter'));
856 $this->assertEquals($enabledPlugins, $this->conf->get('general.enabled_plugins'));
857
858 $this->conf = new ConfigManager($sandboxConf);
859 $this->assertEquals('default', $this->conf->get('formatter'));
860 $this->assertEquals($enabledPlugins, $this->conf->get('general.enabled_plugins'));
861 }
862
863 /**
864 * Test updateMethodFormatterSetting()
865 */
866 public function testUpdateMethodFormatterSettingMarkdown()
867 {
868 $sandboxConf = 'sandbox/config';
869 copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
870 $this->conf = new ConfigManager($sandboxConf);
871 $this->conf->set('formatter', 'default');
872 $updater = new LegacyUpdater([], null, $this->conf, true);
873 $enabledPlugins = $this->conf->get('general.enabled_plugins');
874 $enabledPlugins[] = 'markdown';
875 $this->conf->set('general.enabled_plugins', $enabledPlugins);
876
877 $this->assertTrue(in_array('markdown', $this->conf->get('general.enabled_plugins')));
878 $this->assertTrue($updater->updateMethodFormatterSetting());
879 $this->assertEquals('markdown', $this->conf->get('formatter'));
880 $this->assertFalse(in_array('markdown', $this->conf->get('general.enabled_plugins')));
881
882 $this->conf = new ConfigManager($sandboxConf);
883 $this->assertEquals('markdown', $this->conf->get('formatter'));
884 $this->assertFalse(in_array('markdown', $this->conf->get('general.enabled_plugins')));
885 }
886}
diff --git a/tests/netscape/BookmarkExportTest.php b/tests/netscape/BookmarkExportTest.php
index 6de9876d..011d19ac 100644
--- a/tests/netscape/BookmarkExportTest.php
+++ b/tests/netscape/BookmarkExportTest.php
@@ -1,7 +1,12 @@
1<?php 1<?php
2namespace Shaarli\Netscape; 2namespace Shaarli\Netscape;
3 3
4use Shaarli\Bookmark\BookmarkFileService;
4use Shaarli\Bookmark\LinkDB; 5use Shaarli\Bookmark\LinkDB;
6use Shaarli\Config\ConfigManager;
7use Shaarli\Formatter\FormatterFactory;
8use Shaarli\Formatter\BookmarkFormatter;
9use Shaarli\History;
5 10
6require_once 'tests/utils/ReferenceLinkDB.php'; 11require_once 'tests/utils/ReferenceLinkDB.php';
7 12
@@ -21,18 +26,28 @@ class BookmarkExportTest extends \PHPUnit\Framework\TestCase
21 protected static $refDb = null; 26 protected static $refDb = null;
22 27
23 /** 28 /**
24 * @var LinkDB private LinkDB instance. 29 * @var BookmarkFileService private instance.
25 */ 30 */
26 protected static $linkDb = null; 31 protected static $bookmarkService = null;
32
33 /**
34 * @var BookmarkFormatter instance
35 */
36 protected static $formatter;
27 37
28 /** 38 /**
29 * Instantiate reference data 39 * Instantiate reference data
30 */ 40 */
31 public static function setUpBeforeClass() 41 public static function setUpBeforeClass()
32 { 42 {
43 $conf = new ConfigManager('tests/utils/config/configJson');
44 $conf->set('resource.datastore', self::$testDatastore);
33 self::$refDb = new \ReferenceLinkDB(); 45 self::$refDb = new \ReferenceLinkDB();
34 self::$refDb->write(self::$testDatastore); 46 self::$refDb->write(self::$testDatastore);
35 self::$linkDb = new LinkDB(self::$testDatastore, true, false); 47 $history = new History('sandbox/history.php');
48 self::$bookmarkService = new BookmarkFileService($conf, $history, true);
49 $factory = new FormatterFactory($conf);
50 self::$formatter = $factory->getFormatter('raw');
36 } 51 }
37 52
38 /** 53 /**
@@ -42,15 +57,27 @@ class BookmarkExportTest extends \PHPUnit\Framework\TestCase
42 */ 57 */
43 public function testFilterAndFormatInvalid() 58 public function testFilterAndFormatInvalid()
44 { 59 {
45 NetscapeBookmarkUtils::filterAndFormat(self::$linkDb, 'derp', false, ''); 60 NetscapeBookmarkUtils::filterAndFormat(
61 self::$bookmarkService,
62 self::$formatter,
63 'derp',
64 false,
65 ''
66 );
46 } 67 }
47 68
48 /** 69 /**
49 * Prepare all links for export 70 * Prepare all bookmarks for export
50 */ 71 */
51 public function testFilterAndFormatAll() 72 public function testFilterAndFormatAll()
52 { 73 {
53 $links = NetscapeBookmarkUtils::filterAndFormat(self::$linkDb, 'all', false, ''); 74 $links = NetscapeBookmarkUtils::filterAndFormat(
75 self::$bookmarkService,
76 self::$formatter,
77 'all',
78 false,
79 ''
80 );
54 $this->assertEquals(self::$refDb->countLinks(), sizeof($links)); 81 $this->assertEquals(self::$refDb->countLinks(), sizeof($links));
55 foreach ($links as $link) { 82 foreach ($links as $link) {
56 $date = $link['created']; 83 $date = $link['created'];
@@ -66,11 +93,17 @@ class BookmarkExportTest extends \PHPUnit\Framework\TestCase
66 } 93 }
67 94
68 /** 95 /**
69 * Prepare private links for export 96 * Prepare private bookmarks for export
70 */ 97 */
71 public function testFilterAndFormatPrivate() 98 public function testFilterAndFormatPrivate()
72 { 99 {
73 $links = NetscapeBookmarkUtils::filterAndFormat(self::$linkDb, 'private', false, ''); 100 $links = NetscapeBookmarkUtils::filterAndFormat(
101 self::$bookmarkService,
102 self::$formatter,
103 'private',
104 false,
105 ''
106 );
74 $this->assertEquals(self::$refDb->countPrivateLinks(), sizeof($links)); 107 $this->assertEquals(self::$refDb->countPrivateLinks(), sizeof($links));
75 foreach ($links as $link) { 108 foreach ($links as $link) {
76 $date = $link['created']; 109 $date = $link['created'];
@@ -86,11 +119,17 @@ class BookmarkExportTest extends \PHPUnit\Framework\TestCase
86 } 119 }
87 120
88 /** 121 /**
89 * Prepare public links for export 122 * Prepare public bookmarks for export
90 */ 123 */
91 public function testFilterAndFormatPublic() 124 public function testFilterAndFormatPublic()
92 { 125 {
93 $links = NetscapeBookmarkUtils::filterAndFormat(self::$linkDb, 'public', false, ''); 126 $links = NetscapeBookmarkUtils::filterAndFormat(
127 self::$bookmarkService,
128 self::$formatter,
129 'public',
130 false,
131 ''
132 );
94 $this->assertEquals(self::$refDb->countPublicLinks(), sizeof($links)); 133 $this->assertEquals(self::$refDb->countPublicLinks(), sizeof($links));
95 foreach ($links as $link) { 134 foreach ($links as $link) {
96 $date = $link['created']; 135 $date = $link['created'];
@@ -110,7 +149,13 @@ class BookmarkExportTest extends \PHPUnit\Framework\TestCase
110 */ 149 */
111 public function testFilterAndFormatDoNotPrependNoteUrl() 150 public function testFilterAndFormatDoNotPrependNoteUrl()
112 { 151 {
113 $links = NetscapeBookmarkUtils::filterAndFormat(self::$linkDb, 'public', false, ''); 152 $links = NetscapeBookmarkUtils::filterAndFormat(
153 self::$bookmarkService,
154 self::$formatter,
155 'public',
156 false,
157 ''
158 );
114 $this->assertEquals( 159 $this->assertEquals(
115 '?WDWyig', 160 '?WDWyig',
116 $links[2]['url'] 161 $links[2]['url']
@@ -124,7 +169,8 @@ class BookmarkExportTest extends \PHPUnit\Framework\TestCase
124 { 169 {
125 $indexUrl = 'http://localhost:7469/shaarli/'; 170 $indexUrl = 'http://localhost:7469/shaarli/';
126 $links = NetscapeBookmarkUtils::filterAndFormat( 171 $links = NetscapeBookmarkUtils::filterAndFormat(
127 self::$linkDb, 172 self::$bookmarkService,
173 self::$formatter,
128 'public', 174 'public',
129 true, 175 true,
130 $indexUrl 176 $indexUrl
diff --git a/tests/netscape/BookmarkImportTest.php b/tests/netscape/BookmarkImportTest.php
index ccafc161..fef7f6d1 100644
--- a/tests/netscape/BookmarkImportTest.php
+++ b/tests/netscape/BookmarkImportTest.php
@@ -2,6 +2,9 @@
2namespace Shaarli\Netscape; 2namespace Shaarli\Netscape;
3 3
4use DateTime; 4use DateTime;
5use Shaarli\Bookmark\Bookmark;
6use Shaarli\Bookmark\BookmarkFilter;
7use Shaarli\Bookmark\BookmarkFileService;
5use Shaarli\Bookmark\LinkDB; 8use Shaarli\Bookmark\LinkDB;
6use Shaarli\Config\ConfigManager; 9use Shaarli\Config\ConfigManager;
7use Shaarli\History; 10use Shaarli\History;
@@ -41,9 +44,9 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
41 protected static $historyFilePath = 'sandbox/history.php'; 44 protected static $historyFilePath = 'sandbox/history.php';
42 45
43 /** 46 /**
44 * @var LinkDB private LinkDB instance 47 * @var BookmarkFileService private LinkDB instance
45 */ 48 */
46 protected $linkDb = null; 49 protected $bookmarkService = null;
47 50
48 /** 51 /**
49 * @var string Dummy page cache 52 * @var string Dummy page cache
@@ -82,10 +85,12 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
82 } 85 }
83 // start with an empty datastore 86 // start with an empty datastore
84 file_put_contents(self::$testDatastore, '<?php /* S7QysKquBQA= */ ?>'); 87 file_put_contents(self::$testDatastore, '<?php /* S7QysKquBQA= */ ?>');
85 $this->linkDb = new LinkDB(self::$testDatastore, true, false); 88
86 $this->conf = new ConfigManager('tests/utils/config/configJson'); 89 $this->conf = new ConfigManager('tests/utils/config/configJson');
87 $this->conf->set('resource.page_cache', $this->pagecache); 90 $this->conf->set('resource.page_cache', $this->pagecache);
91 $this->conf->set('resource.datastore', self::$testDatastore);
88 $this->history = new History(self::$historyFilePath); 92 $this->history = new History(self::$historyFilePath);
93 $this->bookmarkService = new BookmarkFileService($this->conf, $this->history, true);
89 } 94 }
90 95
91 /** 96 /**
@@ -112,7 +117,7 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
112 .' Nothing was imported.', 117 .' Nothing was imported.',
113 NetscapeBookmarkUtils::import(null, $files, null, $this->conf, $this->history) 118 NetscapeBookmarkUtils::import(null, $files, null, $this->conf, $this->history)
114 ); 119 );
115 $this->assertEquals(0, count($this->linkDb)); 120 $this->assertEquals(0, $this->bookmarkService->count());
116 } 121 }
117 122
118 /** 123 /**
@@ -125,7 +130,7 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
125 'File no_doctype.htm (350 bytes) has an unknown file format. Nothing was imported.', 130 'File no_doctype.htm (350 bytes) has an unknown file format. Nothing was imported.',
126 NetscapeBookmarkUtils::import(null, $files, null, $this->conf, $this->history) 131 NetscapeBookmarkUtils::import(null, $files, null, $this->conf, $this->history)
127 ); 132 );
128 $this->assertEquals(0, count($this->linkDb)); 133 $this->assertEquals(0, $this->bookmarkService->count());
129 } 134 }
130 135
131 /** 136 /**
@@ -136,10 +141,10 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
136 $files = file2array('lowercase_doctype.htm'); 141 $files = file2array('lowercase_doctype.htm');
137 $this->assertStringMatchesFormat( 142 $this->assertStringMatchesFormat(
138 'File lowercase_doctype.htm (386 bytes) was successfully processed in %d seconds:' 143 'File lowercase_doctype.htm (386 bytes) was successfully processed in %d seconds:'
139 .' 2 links imported, 0 links overwritten, 0 links skipped.', 144 .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
140 NetscapeBookmarkUtils::import(null, $files, $this->linkDb, $this->conf, $this->history) 145 NetscapeBookmarkUtils::import(null, $files, $this->bookmarkService, $this->conf, $this->history)
141 ); 146 );
142 $this->assertEquals(2, count($this->linkDb)); 147 $this->assertEquals(2, $this->bookmarkService->count());
143 } 148 }
144 149
145 150
@@ -151,25 +156,24 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
151 $files = file2array('internet_explorer_encoding.htm'); 156 $files = file2array('internet_explorer_encoding.htm');
152 $this->assertStringMatchesFormat( 157 $this->assertStringMatchesFormat(
153 'File internet_explorer_encoding.htm (356 bytes) was successfully processed in %d seconds:' 158 'File internet_explorer_encoding.htm (356 bytes) was successfully processed in %d seconds:'
154 .' 1 links imported, 0 links overwritten, 0 links skipped.', 159 .' 1 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
155 NetscapeBookmarkUtils::import([], $files, $this->linkDb, $this->conf, $this->history) 160 NetscapeBookmarkUtils::import([], $files, $this->bookmarkService, $this->conf, $this->history)
156 ); 161 );
157 $this->assertEquals(1, count($this->linkDb)); 162 $this->assertEquals(1, $this->bookmarkService->count());
158 $this->assertEquals(0, count_private($this->linkDb)); 163 $this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
159 164
165 $bookmark = $this->bookmarkService->findByUrl('http://hginit.com/');
166 $this->assertEquals(0, $bookmark->getId());
160 $this->assertEquals( 167 $this->assertEquals(
161 array( 168 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20160618_203944'),
162 'id' => 0, 169 $bookmark->getCreated()
163 'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160618_203944'),
164 'title' => 'Hg Init a Mercurial tutorial by Joel Spolsky',
165 'url' => 'http://hginit.com/',
166 'description' => '',
167 'private' => 0,
168 'tags' => '',
169 'shorturl' => 'La37cg',
170 ),
171 $this->linkDb->getLinkFromUrl('http://hginit.com/')
172 ); 170 );
171 $this->assertEquals('Hg Init a Mercurial tutorial by Joel Spolsky', $bookmark->getTitle());
172 $this->assertEquals('http://hginit.com/', $bookmark->getUrl());
173 $this->assertEquals('', $bookmark->getDescription());
174 $this->assertFalse($bookmark->isPrivate());
175 $this->assertEquals('', $bookmark->getTagsString());
176 $this->assertEquals('La37cg', $bookmark->getShortUrl());
173 } 177 }
174 178
175 /** 179 /**
@@ -180,116 +184,115 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
180 $files = file2array('netscape_nested.htm'); 184 $files = file2array('netscape_nested.htm');
181 $this->assertStringMatchesFormat( 185 $this->assertStringMatchesFormat(
182 'File netscape_nested.htm (1337 bytes) was successfully processed in %d seconds:' 186 'File netscape_nested.htm (1337 bytes) was successfully processed in %d seconds:'
183 .' 8 links imported, 0 links overwritten, 0 links skipped.', 187 .' 8 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
184 NetscapeBookmarkUtils::import([], $files, $this->linkDb, $this->conf, $this->history) 188 NetscapeBookmarkUtils::import([], $files, $this->bookmarkService, $this->conf, $this->history)
185 );
186 $this->assertEquals(8, count($this->linkDb));
187 $this->assertEquals(2, count_private($this->linkDb));
188
189 $this->assertEquals(
190 array(
191 'id' => 0,
192 'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160225_235541'),
193 'title' => 'Nested 1',
194 'url' => 'http://nest.ed/1',
195 'description' => '',
196 'private' => 0,
197 'tags' => 'tag1 tag2',
198 'shorturl' => 'KyDNKA',
199 ),
200 $this->linkDb->getLinkFromUrl('http://nest.ed/1')
201 );
202 $this->assertEquals(
203 array(
204 'id' => 1,
205 'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160225_235542'),
206 'title' => 'Nested 1-1',
207 'url' => 'http://nest.ed/1-1',
208 'description' => '',
209 'private' => 0,
210 'tags' => 'folder1 tag1 tag2',
211 'shorturl' => 'T2LnXg',
212 ),
213 $this->linkDb->getLinkFromUrl('http://nest.ed/1-1')
214 );
215 $this->assertEquals(
216 array(
217 'id' => 2,
218 'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160225_235547'),
219 'title' => 'Nested 1-2',
220 'url' => 'http://nest.ed/1-2',
221 'description' => '',
222 'private' => 0,
223 'tags' => 'folder1 tag3 tag4',
224 'shorturl' => '46SZxA',
225 ),
226 $this->linkDb->getLinkFromUrl('http://nest.ed/1-2')
227 );
228 $this->assertEquals(
229 array(
230 'id' => 3,
231 'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160202_202222'),
232 'title' => 'Nested 2-1',
233 'url' => 'http://nest.ed/2-1',
234 'description' => 'First link of the second section',
235 'private' => 1,
236 'tags' => 'folder2',
237 'shorturl' => '4UHOSw',
238 ),
239 $this->linkDb->getLinkFromUrl('http://nest.ed/2-1')
240 );
241 $this->assertEquals(
242 array(
243 'id' => 4,
244 'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160119_230227'),
245 'title' => 'Nested 2-2',
246 'url' => 'http://nest.ed/2-2',
247 'description' => 'Second link of the second section',
248 'private' => 1,
249 'tags' => 'folder2',
250 'shorturl' => 'yfzwbw',
251 ),
252 $this->linkDb->getLinkFromUrl('http://nest.ed/2-2')
253 );
254 $this->assertEquals(
255 array(
256 'id' => 5,
257 'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160202_202222'),
258 'title' => 'Nested 3-1',
259 'url' => 'http://nest.ed/3-1',
260 'description' => '',
261 'private' => 0,
262 'tags' => 'folder3 folder3-1 tag3',
263 'shorturl' => 'UwxIUQ',
264 ),
265 $this->linkDb->getLinkFromUrl('http://nest.ed/3-1')
266 );
267 $this->assertEquals(
268 array(
269 'id' => 6,
270 'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160119_230227'),
271 'title' => 'Nested 3-2',
272 'url' => 'http://nest.ed/3-2',
273 'description' => '',
274 'private' => 0,
275 'tags' => 'folder3 folder3-1',
276 'shorturl' => 'p8dyZg',
277 ),
278 $this->linkDb->getLinkFromUrl('http://nest.ed/3-2')
279 );
280 $this->assertEquals(
281 array(
282 'id' => 7,
283 'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160229_111541'),
284 'title' => 'Nested 2',
285 'url' => 'http://nest.ed/2',
286 'description' => '',
287 'private' => 0,
288 'tags' => 'tag4',
289 'shorturl' => 'Gt3Uug',
290 ),
291 $this->linkDb->getLinkFromUrl('http://nest.ed/2')
292 ); 189 );
190 $this->assertEquals(8, $this->bookmarkService->count());
191 $this->assertEquals(2, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
192
193 $bookmark = $this->bookmarkService->findByUrl('http://nest.ed/1');
194 $this->assertEquals(0, $bookmark->getId());
195 $this->assertEquals(
196 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20160225_235541'),
197 $bookmark->getCreated()
198 );
199 $this->assertEquals('Nested 1', $bookmark->getTitle());
200 $this->assertEquals('http://nest.ed/1', $bookmark->getUrl());
201 $this->assertEquals('', $bookmark->getDescription());
202 $this->assertFalse($bookmark->isPrivate());
203 $this->assertEquals('tag1 tag2', $bookmark->getTagsString());
204 $this->assertEquals('KyDNKA', $bookmark->getShortUrl());
205
206 $bookmark = $this->bookmarkService->findByUrl('http://nest.ed/1-1');
207 $this->assertEquals(1, $bookmark->getId());
208 $this->assertEquals(
209 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20160225_235542'),
210 $bookmark->getCreated()
211 );
212 $this->assertEquals('Nested 1-1', $bookmark->getTitle());
213 $this->assertEquals('http://nest.ed/1-1', $bookmark->getUrl());
214 $this->assertEquals('', $bookmark->getDescription());
215 $this->assertFalse($bookmark->isPrivate());
216 $this->assertEquals('folder1 tag1 tag2', $bookmark->getTagsString());
217 $this->assertEquals('T2LnXg', $bookmark->getShortUrl());
218
219 $bookmark = $this->bookmarkService->findByUrl('http://nest.ed/1-2');
220 $this->assertEquals(2, $bookmark->getId());
221 $this->assertEquals(
222 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20160225_235547'),
223 $bookmark->getCreated()
224 );
225 $this->assertEquals('Nested 1-2', $bookmark->getTitle());
226 $this->assertEquals('http://nest.ed/1-2', $bookmark->getUrl());
227 $this->assertEquals('', $bookmark->getDescription());
228 $this->assertFalse($bookmark->isPrivate());
229 $this->assertEquals('folder1 tag3 tag4', $bookmark->getTagsString());
230 $this->assertEquals('46SZxA', $bookmark->getShortUrl());
231
232 $bookmark = $this->bookmarkService->findByUrl('http://nest.ed/2-1');
233 $this->assertEquals(3, $bookmark->getId());
234 $this->assertEquals(
235 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20160202_202222'),
236 $bookmark->getCreated()
237 );
238 $this->assertEquals('Nested 2-1', $bookmark->getTitle());
239 $this->assertEquals('http://nest.ed/2-1', $bookmark->getUrl());
240 $this->assertEquals('First link of the second section', $bookmark->getDescription());
241 $this->assertTrue($bookmark->isPrivate());
242 $this->assertEquals('folder2', $bookmark->getTagsString());
243 $this->assertEquals('4UHOSw', $bookmark->getShortUrl());
244
245 $bookmark = $this->bookmarkService->findByUrl('http://nest.ed/2-2');
246 $this->assertEquals(4, $bookmark->getId());
247 $this->assertEquals(
248 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20160119_230227'),
249 $bookmark->getCreated()
250 );
251 $this->assertEquals('Nested 2-2', $bookmark->getTitle());
252 $this->assertEquals('http://nest.ed/2-2', $bookmark->getUrl());
253 $this->assertEquals('Second link of the second section', $bookmark->getDescription());
254 $this->assertTrue($bookmark->isPrivate());
255 $this->assertEquals('folder2', $bookmark->getTagsString());
256 $this->assertEquals('yfzwbw', $bookmark->getShortUrl());
257
258 $bookmark = $this->bookmarkService->findByUrl('http://nest.ed/3-1');
259 $this->assertEquals(5, $bookmark->getId());
260 $this->assertEquals(
261 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20160202_202222'),
262 $bookmark->getCreated()
263 );
264 $this->assertEquals('Nested 3-1', $bookmark->getTitle());
265 $this->assertEquals('http://nest.ed/3-1', $bookmark->getUrl());
266 $this->assertEquals('', $bookmark->getDescription());
267 $this->assertFalse($bookmark->isPrivate());
268 $this->assertEquals('folder3 folder3-1 tag3', $bookmark->getTagsString());
269 $this->assertEquals('UwxIUQ', $bookmark->getShortUrl());
270
271 $bookmark = $this->bookmarkService->findByUrl('http://nest.ed/3-2');
272 $this->assertEquals(6, $bookmark->getId());
273 $this->assertEquals(
274 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20160119_230227'),
275 $bookmark->getCreated()
276 );
277 $this->assertEquals('Nested 3-2', $bookmark->getTitle());
278 $this->assertEquals('http://nest.ed/3-2', $bookmark->getUrl());
279 $this->assertEquals('', $bookmark->getDescription());
280 $this->assertFalse($bookmark->isPrivate());
281 $this->assertEquals('folder3 folder3-1', $bookmark->getTagsString());
282 $this->assertEquals('p8dyZg', $bookmark->getShortUrl());
283
284 $bookmark = $this->bookmarkService->findByUrl('http://nest.ed/2');
285 $this->assertEquals(7, $bookmark->getId());
286 $this->assertEquals(
287 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20160229_111541'),
288 $bookmark->getCreated()
289 );
290 $this->assertEquals('Nested 2', $bookmark->getTitle());
291 $this->assertEquals('http://nest.ed/2', $bookmark->getUrl());
292 $this->assertEquals('', $bookmark->getDescription());
293 $this->assertFalse($bookmark->isPrivate());
294 $this->assertEquals('tag4', $bookmark->getTagsString());
295 $this->assertEquals('Gt3Uug', $bookmark->getShortUrl());
293 } 296 }
294 297
295 /** 298 /**
@@ -302,40 +305,38 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
302 $files = file2array('netscape_basic.htm'); 305 $files = file2array('netscape_basic.htm');
303 $this->assertStringMatchesFormat( 306 $this->assertStringMatchesFormat(
304 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' 307 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
305 .' 2 links imported, 0 links overwritten, 0 links skipped.', 308 .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
306 NetscapeBookmarkUtils::import([], $files, $this->linkDb, $this->conf, $this->history) 309 NetscapeBookmarkUtils::import([], $files, $this->bookmarkService, $this->conf, $this->history)
307 ); 310 );
308 311
309 $this->assertEquals(2, count($this->linkDb)); 312 $this->assertEquals(2, $this->bookmarkService->count());
310 $this->assertEquals(1, count_private($this->linkDb)); 313 $this->assertEquals(1, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
311 314
315 $bookmark = $this->bookmarkService->findByUrl('https://private.tld');
316 $this->assertEquals(0, $bookmark->getId());
312 $this->assertEquals( 317 $this->assertEquals(
313 array( 318 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20001010_135536'),
314 'id' => 0, 319 $bookmark->getCreated()
315 // Old link - UTC+4 (note that TZ in the import file is ignored).
316 'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20001010_135536'),
317 'title' => 'Secret stuff',
318 'url' => 'https://private.tld',
319 'description' => "Super-secret stuff you're not supposed to know about",
320 'private' => 1,
321 'tags' => 'private secret',
322 'shorturl' => 'EokDtA',
323 ),
324 $this->linkDb->getLinkFromUrl('https://private.tld')
325 ); 320 );
321 $this->assertEquals('Secret stuff', $bookmark->getTitle());
322 $this->assertEquals('https://private.tld', $bookmark->getUrl());
323 $this->assertEquals('Super-secret stuff you\'re not supposed to know about', $bookmark->getDescription());
324 $this->assertTrue($bookmark->isPrivate());
325 $this->assertEquals('private secret', $bookmark->getTagsString());
326 $this->assertEquals('EokDtA', $bookmark->getShortUrl());
327
328 $bookmark = $this->bookmarkService->findByUrl('http://public.tld');
329 $this->assertEquals(1, $bookmark->getId());
326 $this->assertEquals( 330 $this->assertEquals(
327 array( 331 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20160225_235548'),
328 'id' => 1, 332 $bookmark->getCreated()
329 'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160225_235548'),
330 'title' => 'Public stuff',
331 'url' => 'http://public.tld',
332 'description' => '',
333 'private' => 0,
334 'tags' => 'public hello world',
335 'shorturl' => 'Er9ddA',
336 ),
337 $this->linkDb->getLinkFromUrl('http://public.tld')
338 ); 333 );
334 $this->assertEquals('Public stuff', $bookmark->getTitle());
335 $this->assertEquals('http://public.tld', $bookmark->getUrl());
336 $this->assertEquals('', $bookmark->getDescription());
337 $this->assertFalse($bookmark->isPrivate());
338 $this->assertEquals('public hello world', $bookmark->getTagsString());
339 $this->assertEquals('Er9ddA', $bookmark->getShortUrl());
339 } 340 }
340 341
341 /** 342 /**
@@ -347,43 +348,42 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
347 $files = file2array('netscape_basic.htm'); 348 $files = file2array('netscape_basic.htm');
348 $this->assertStringMatchesFormat( 349 $this->assertStringMatchesFormat(
349 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' 350 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
350 .' 2 links imported, 0 links overwritten, 0 links skipped.', 351 .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
351 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history) 352 NetscapeBookmarkUtils::import($post, $files, $this->bookmarkService, $this->conf, $this->history)
352 ); 353 );
353 $this->assertEquals(2, count($this->linkDb));
354 $this->assertEquals(1, count_private($this->linkDb));
355 354
355 $this->assertEquals(2, $this->bookmarkService->count());
356 $this->assertEquals(1, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
357
358 $bookmark = $this->bookmarkService->findByUrl('https://private.tld');
359 $this->assertEquals(0, $bookmark->getId());
356 $this->assertEquals( 360 $this->assertEquals(
357 array( 361 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20001010_135536'),
358 'id' => 0, 362 $bookmark->getCreated()
359 // Note that TZ in the import file is ignored.
360 'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20001010_135536'),
361 'title' => 'Secret stuff',
362 'url' => 'https://private.tld',
363 'description' => "Super-secret stuff you're not supposed to know about",
364 'private' => 1,
365 'tags' => 'private secret',
366 'shorturl' => 'EokDtA',
367 ),
368 $this->linkDb->getLinkFromUrl('https://private.tld')
369 ); 363 );
364 $this->assertEquals('Secret stuff', $bookmark->getTitle());
365 $this->assertEquals('https://private.tld', $bookmark->getUrl());
366 $this->assertEquals('Super-secret stuff you\'re not supposed to know about', $bookmark->getDescription());
367 $this->assertTrue($bookmark->isPrivate());
368 $this->assertEquals('private secret', $bookmark->getTagsString());
369 $this->assertEquals('EokDtA', $bookmark->getShortUrl());
370
371 $bookmark = $this->bookmarkService->findByUrl('http://public.tld');
372 $this->assertEquals(1, $bookmark->getId());
370 $this->assertEquals( 373 $this->assertEquals(
371 array( 374 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20160225_235548'),
372 'id' => 1, 375 $bookmark->getCreated()
373 'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160225_235548'),
374 'title' => 'Public stuff',
375 'url' => 'http://public.tld',
376 'description' => '',
377 'private' => 0,
378 'tags' => 'public hello world',
379 'shorturl' => 'Er9ddA',
380 ),
381 $this->linkDb->getLinkFromUrl('http://public.tld')
382 ); 376 );
377 $this->assertEquals('Public stuff', $bookmark->getTitle());
378 $this->assertEquals('http://public.tld', $bookmark->getUrl());
379 $this->assertEquals('', $bookmark->getDescription());
380 $this->assertFalse($bookmark->isPrivate());
381 $this->assertEquals('public hello world', $bookmark->getTagsString());
382 $this->assertEquals('Er9ddA', $bookmark->getShortUrl());
383 } 383 }
384 384
385 /** 385 /**
386 * Import links as public 386 * Import bookmarks as public
387 */ 387 */
388 public function testImportAsPublic() 388 public function testImportAsPublic()
389 { 389 {
@@ -391,23 +391,17 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
391 $files = file2array('netscape_basic.htm'); 391 $files = file2array('netscape_basic.htm');
392 $this->assertStringMatchesFormat( 392 $this->assertStringMatchesFormat(
393 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' 393 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
394 .' 2 links imported, 0 links overwritten, 0 links skipped.', 394 .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
395 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history) 395 NetscapeBookmarkUtils::import($post, $files, $this->bookmarkService, $this->conf, $this->history)
396 );
397 $this->assertEquals(2, count($this->linkDb));
398 $this->assertEquals(0, count_private($this->linkDb));
399 $this->assertEquals(
400 0,
401 $this->linkDb[0]['private']
402 );
403 $this->assertEquals(
404 0,
405 $this->linkDb[1]['private']
406 ); 396 );
397 $this->assertEquals(2, $this->bookmarkService->count());
398 $this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
399 $this->assertFalse($this->bookmarkService->get(0)->isPrivate());
400 $this->assertFalse($this->bookmarkService->get(1)->isPrivate());
407 } 401 }
408 402
409 /** 403 /**
410 * Import links as private 404 * Import bookmarks as private
411 */ 405 */
412 public function testImportAsPrivate() 406 public function testImportAsPrivate()
413 { 407 {
@@ -415,45 +409,34 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
415 $files = file2array('netscape_basic.htm'); 409 $files = file2array('netscape_basic.htm');
416 $this->assertStringMatchesFormat( 410 $this->assertStringMatchesFormat(
417 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' 411 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
418 .' 2 links imported, 0 links overwritten, 0 links skipped.', 412 .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
419 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history) 413 NetscapeBookmarkUtils::import($post, $files, $this->bookmarkService, $this->conf, $this->history)
420 );
421 $this->assertEquals(2, count($this->linkDb));
422 $this->assertEquals(2, count_private($this->linkDb));
423 $this->assertEquals(
424 1,
425 $this->linkDb['0']['private']
426 );
427 $this->assertEquals(
428 1,
429 $this->linkDb['1']['private']
430 ); 414 );
415 $this->assertEquals(2, $this->bookmarkService->count());
416 $this->assertEquals(2, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
417 $this->assertTrue($this->bookmarkService->get(0)->isPrivate());
418 $this->assertTrue($this->bookmarkService->get(1)->isPrivate());
431 } 419 }
432 420
433 /** 421 /**
434 * Overwrite private links so they become public 422 * Overwrite private bookmarks so they become public
435 */ 423 */
436 public function testOverwriteAsPublic() 424 public function testOverwriteAsPublic()
437 { 425 {
438 $files = file2array('netscape_basic.htm'); 426 $files = file2array('netscape_basic.htm');
439 427
440 // import links as private 428 // import bookmarks as private
441 $post = array('privacy' => 'private'); 429 $post = array('privacy' => 'private');
442 $this->assertStringMatchesFormat( 430 $this->assertStringMatchesFormat(
443 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' 431 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
444 .' 2 links imported, 0 links overwritten, 0 links skipped.', 432 .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
445 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history) 433 NetscapeBookmarkUtils::import($post, $files, $this->bookmarkService, $this->conf, $this->history)
446 );
447 $this->assertEquals(2, count($this->linkDb));
448 $this->assertEquals(2, count_private($this->linkDb));
449 $this->assertEquals(
450 1,
451 $this->linkDb[0]['private']
452 );
453 $this->assertEquals(
454 1,
455 $this->linkDb[1]['private']
456 ); 434 );
435 $this->assertEquals(2, $this->bookmarkService->count());
436 $this->assertEquals(2, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
437 $this->assertTrue($this->bookmarkService->get(0)->isPrivate());
438 $this->assertTrue($this->bookmarkService->get(1)->isPrivate());
439
457 // re-import as public, enable overwriting 440 // re-import as public, enable overwriting
458 $post = array( 441 $post = array(
459 'privacy' => 'public', 442 'privacy' => 'public',
@@ -461,45 +444,33 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
461 ); 444 );
462 $this->assertStringMatchesFormat( 445 $this->assertStringMatchesFormat(
463 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' 446 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
464 .' 2 links imported, 2 links overwritten, 0 links skipped.', 447 .' 2 bookmarks imported, 2 bookmarks overwritten, 0 bookmarks skipped.',
465 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history) 448 NetscapeBookmarkUtils::import($post, $files, $this->bookmarkService, $this->conf, $this->history)
466 );
467 $this->assertEquals(2, count($this->linkDb));
468 $this->assertEquals(0, count_private($this->linkDb));
469 $this->assertEquals(
470 0,
471 $this->linkDb[0]['private']
472 );
473 $this->assertEquals(
474 0,
475 $this->linkDb[1]['private']
476 ); 449 );
450 $this->assertEquals(2, $this->bookmarkService->count());
451 $this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
452 $this->assertFalse($this->bookmarkService->get(0)->isPrivate());
453 $this->assertFalse($this->bookmarkService->get(1)->isPrivate());
477 } 454 }
478 455
479 /** 456 /**
480 * Overwrite public links so they become private 457 * Overwrite public bookmarks so they become private
481 */ 458 */
482 public function testOverwriteAsPrivate() 459 public function testOverwriteAsPrivate()
483 { 460 {
484 $files = file2array('netscape_basic.htm'); 461 $files = file2array('netscape_basic.htm');
485 462
486 // import links as public 463 // import bookmarks as public
487 $post = array('privacy' => 'public'); 464 $post = array('privacy' => 'public');
488 $this->assertStringMatchesFormat( 465 $this->assertStringMatchesFormat(
489 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' 466 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
490 .' 2 links imported, 0 links overwritten, 0 links skipped.', 467 .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
491 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history) 468 NetscapeBookmarkUtils::import($post, $files, $this->bookmarkService, $this->conf, $this->history)
492 );
493 $this->assertEquals(2, count($this->linkDb));
494 $this->assertEquals(0, count_private($this->linkDb));
495 $this->assertEquals(
496 0,
497 $this->linkDb['0']['private']
498 );
499 $this->assertEquals(
500 0,
501 $this->linkDb['1']['private']
502 ); 469 );
470 $this->assertEquals(2, $this->bookmarkService->count());
471 $this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
472 $this->assertFalse($this->bookmarkService->get(0)->isPrivate());
473 $this->assertFalse($this->bookmarkService->get(1)->isPrivate());
503 474
504 // re-import as private, enable overwriting 475 // re-import as private, enable overwriting
505 $post = array( 476 $post = array(
@@ -508,23 +479,17 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
508 ); 479 );
509 $this->assertStringMatchesFormat( 480 $this->assertStringMatchesFormat(
510 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' 481 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
511 .' 2 links imported, 2 links overwritten, 0 links skipped.', 482 .' 2 bookmarks imported, 2 bookmarks overwritten, 0 bookmarks skipped.',
512 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history) 483 NetscapeBookmarkUtils::import($post, $files, $this->bookmarkService, $this->conf, $this->history)
513 );
514 $this->assertEquals(2, count($this->linkDb));
515 $this->assertEquals(2, count_private($this->linkDb));
516 $this->assertEquals(
517 1,
518 $this->linkDb['0']['private']
519 );
520 $this->assertEquals(
521 1,
522 $this->linkDb['1']['private']
523 ); 484 );
485 $this->assertEquals(2, $this->bookmarkService->count());
486 $this->assertEquals(2, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
487 $this->assertTrue($this->bookmarkService->get(0)->isPrivate());
488 $this->assertTrue($this->bookmarkService->get(1)->isPrivate());
524 } 489 }
525 490
526 /** 491 /**
527 * Attept to import the same links twice without enabling overwriting 492 * Attept to import the same bookmarks twice without enabling overwriting
528 */ 493 */
529 public function testSkipOverwrite() 494 public function testSkipOverwrite()
530 { 495 {
@@ -532,21 +497,21 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
532 $files = file2array('netscape_basic.htm'); 497 $files = file2array('netscape_basic.htm');
533 $this->assertStringMatchesFormat( 498 $this->assertStringMatchesFormat(
534 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' 499 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
535 .' 2 links imported, 0 links overwritten, 0 links skipped.', 500 .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
536 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history) 501 NetscapeBookmarkUtils::import($post, $files, $this->bookmarkService, $this->conf, $this->history)
537 ); 502 );
538 $this->assertEquals(2, count($this->linkDb)); 503 $this->assertEquals(2, $this->bookmarkService->count());
539 $this->assertEquals(0, count_private($this->linkDb)); 504 $this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
540 505
541 // re-import as private, DO NOT enable overwriting 506 // re-import as private, DO NOT enable overwriting
542 $post = array('privacy' => 'private'); 507 $post = array('privacy' => 'private');
543 $this->assertStringMatchesFormat( 508 $this->assertStringMatchesFormat(
544 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' 509 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
545 .' 0 links imported, 0 links overwritten, 2 links skipped.', 510 .' 0 bookmarks imported, 0 bookmarks overwritten, 2 bookmarks skipped.',
546 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history) 511 NetscapeBookmarkUtils::import($post, $files, $this->bookmarkService, $this->conf, $this->history)
547 ); 512 );
548 $this->assertEquals(2, count($this->linkDb)); 513 $this->assertEquals(2, $this->bookmarkService->count());
549 $this->assertEquals(0, count_private($this->linkDb)); 514 $this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
550 } 515 }
551 516
552 /** 517 /**
@@ -561,19 +526,13 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
561 $files = file2array('netscape_basic.htm'); 526 $files = file2array('netscape_basic.htm');
562 $this->assertStringMatchesFormat( 527 $this->assertStringMatchesFormat(
563 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' 528 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
564 .' 2 links imported, 0 links overwritten, 0 links skipped.', 529 .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
565 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history) 530 NetscapeBookmarkUtils::import($post, $files, $this->bookmarkService, $this->conf, $this->history)
566 );
567 $this->assertEquals(2, count($this->linkDb));
568 $this->assertEquals(0, count_private($this->linkDb));
569 $this->assertEquals(
570 'tag1 tag2 tag3 private secret',
571 $this->linkDb['0']['tags']
572 );
573 $this->assertEquals(
574 'tag1 tag2 tag3 public hello world',
575 $this->linkDb['1']['tags']
576 ); 531 );
532 $this->assertEquals(2, $this->bookmarkService->count());
533 $this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
534 $this->assertEquals('tag1 tag2 tag3 private secret', $this->bookmarkService->get(0)->getTagsString());
535 $this->assertEquals('tag1 tag2 tag3 public hello world', $this->bookmarkService->get(1)->getTagsString());
577 } 536 }
578 537
579 /** 538 /**
@@ -588,18 +547,18 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
588 $files = file2array('netscape_basic.htm'); 547 $files = file2array('netscape_basic.htm');
589 $this->assertStringMatchesFormat( 548 $this->assertStringMatchesFormat(
590 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:' 549 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
591 .' 2 links imported, 0 links overwritten, 0 links skipped.', 550 .' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
592 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history) 551 NetscapeBookmarkUtils::import($post, $files, $this->bookmarkService, $this->conf, $this->history)
593 ); 552 );
594 $this->assertEquals(2, count($this->linkDb)); 553 $this->assertEquals(2, $this->bookmarkService->count());
595 $this->assertEquals(0, count_private($this->linkDb)); 554 $this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
596 $this->assertEquals( 555 $this->assertEquals(
597 'tag1&amp; tag2 &quot;tag3&quot; private secret', 556 'tag1&amp; tag2 &quot;tag3&quot; private secret',
598 $this->linkDb['0']['tags'] 557 $this->bookmarkService->get(0)->getTagsString()
599 ); 558 );
600 $this->assertEquals( 559 $this->assertEquals(
601 'tag1&amp; tag2 &quot;tag3&quot; public hello world', 560 'tag1&amp; tag2 &quot;tag3&quot; public hello world',
602 $this->linkDb['1']['tags'] 561 $this->bookmarkService->get(1)->getTagsString()
603 ); 562 );
604 } 563 }
605 564
@@ -613,23 +572,14 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
613 $files = file2array('same_date.htm'); 572 $files = file2array('same_date.htm');
614 $this->assertStringMatchesFormat( 573 $this->assertStringMatchesFormat(
615 'File same_date.htm (453 bytes) was successfully processed in %d seconds:' 574 'File same_date.htm (453 bytes) was successfully processed in %d seconds:'
616 .' 3 links imported, 0 links overwritten, 0 links skipped.', 575 .' 3 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
617 NetscapeBookmarkUtils::import(array(), $files, $this->linkDb, $this->conf, $this->history) 576 NetscapeBookmarkUtils::import(array(), $files, $this->bookmarkService, $this->conf, $this->history)
618 ); 577 );
619 $this->assertEquals(3, count($this->linkDb)); 578 $this->assertEquals(3, $this->bookmarkService->count());
620 $this->assertEquals(0, count_private($this->linkDb)); 579 $this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
621 $this->assertEquals( 580 $this->assertEquals(0, $this->bookmarkService->get(0)->getId());
622 0, 581 $this->assertEquals(1, $this->bookmarkService->get(1)->getId());
623 $this->linkDb[0]['id'] 582 $this->assertEquals(2, $this->bookmarkService->get(2)->getId());
624 );
625 $this->assertEquals(
626 1,
627 $this->linkDb[1]['id']
628 );
629 $this->assertEquals(
630 2,
631 $this->linkDb[2]['id']
632 );
633 } 583 }
634 584
635 public function testImportCreateUpdateHistory() 585 public function testImportCreateUpdateHistory()
@@ -639,14 +589,14 @@ class BookmarkImportTest extends \PHPUnit\Framework\TestCase
639 'overwrite' => 'true', 589 'overwrite' => 'true',
640 ]; 590 ];
641 $files = file2array('netscape_basic.htm'); 591 $files = file2array('netscape_basic.htm');
642 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history); 592 NetscapeBookmarkUtils::import($post, $files, $this->bookmarkService, $this->conf, $this->history);
643 $history = $this->history->getHistory(); 593 $history = $this->history->getHistory();
644 $this->assertEquals(1, count($history)); 594 $this->assertEquals(1, count($history));
645 $this->assertEquals(History::IMPORT, $history[0]['event']); 595 $this->assertEquals(History::IMPORT, $history[0]['event']);
646 $this->assertTrue(new DateTime('-5 seconds') < $history[0]['datetime']); 596 $this->assertTrue(new DateTime('-5 seconds') < $history[0]['datetime']);
647 597
648 // re-import as private, enable overwriting 598 // re-import as private, enable overwriting
649 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history); 599 NetscapeBookmarkUtils::import($post, $files, $this->bookmarkService, $this->conf, $this->history);
650 $history = $this->history->getHistory(); 600 $history = $this->history->getHistory();
651 $this->assertEquals(2, count($history)); 601 $this->assertEquals(2, count($history));
652 $this->assertEquals(History::IMPORT, $history[0]['event']); 602 $this->assertEquals(History::IMPORT, $history[0]['event']);
diff --git a/tests/plugins/PluginArchiveorgTest.php b/tests/plugins/PluginArchiveorgTest.php
index 510288bb..b9a67adb 100644
--- a/tests/plugins/PluginArchiveorgTest.php
+++ b/tests/plugins/PluginArchiveorgTest.php
@@ -24,7 +24,7 @@ class PluginArchiveorgTest extends \PHPUnit\Framework\TestCase
24 } 24 }
25 25
26 /** 26 /**
27 * Test render_linklist hook on external links. 27 * Test render_linklist hook on external bookmarks.
28 */ 28 */
29 public function testArchiveorgLinklistOnExternalLinks() 29 public function testArchiveorgLinklistOnExternalLinks()
30 { 30 {
@@ -54,7 +54,7 @@ class PluginArchiveorgTest extends \PHPUnit\Framework\TestCase
54 } 54 }
55 55
56 /** 56 /**
57 * Test render_linklist hook on internal links. 57 * Test render_linklist hook on internal bookmarks.
58 */ 58 */
59 public function testArchiveorgLinklistOnInternalLinks() 59 public function testArchiveorgLinklistOnInternalLinks()
60 { 60 {
diff --git a/tests/plugins/PluginIssoTest.php b/tests/plugins/PluginIssoTest.php
index bdfab439..99477205 100644
--- a/tests/plugins/PluginIssoTest.php
+++ b/tests/plugins/PluginIssoTest.php
@@ -2,7 +2,7 @@
2namespace Shaarli\Plugin\Isso; 2namespace Shaarli\Plugin\Isso;
3 3
4use DateTime; 4use DateTime;
5use Shaarli\Bookmark\LinkDB; 5use Shaarli\Bookmark\Bookmark;
6use Shaarli\Config\ConfigManager; 6use Shaarli\Config\ConfigManager;
7use Shaarli\Plugin\PluginManager; 7use Shaarli\Plugin\PluginManager;
8 8
@@ -60,7 +60,7 @@ class PluginIssoTest extends \PHPUnit\Framework\TestCase
60 array( 60 array(
61 'id' => 12, 61 'id' => 12,
62 'url' => $str, 62 'url' => $str,
63 'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $date), 63 'created' => DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, $date),
64 ) 64 )
65 ) 65 )
66 ); 66 );
@@ -85,7 +85,7 @@ class PluginIssoTest extends \PHPUnit\Framework\TestCase
85 } 85 }
86 86
87 /** 87 /**
88 * Test isso plugin when multiple links are displayed (shouldn't be displayed). 88 * Test isso plugin when multiple bookmarks are displayed (shouldn't be displayed).
89 */ 89 */
90 public function testIssoMultipleLinks() 90 public function testIssoMultipleLinks()
91 { 91 {
@@ -102,13 +102,13 @@ class PluginIssoTest extends \PHPUnit\Framework\TestCase
102 'id' => 12, 102 'id' => 12,
103 'url' => $str, 103 'url' => $str,
104 'shorturl' => $short1 = 'abcd', 104 'shorturl' => $short1 = 'abcd',
105 'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $date1), 105 'created' => DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, $date1),
106 ), 106 ),
107 array( 107 array(
108 'id' => 13, 108 'id' => 13,
109 'url' => $str . '2', 109 'url' => $str . '2',
110 'shorturl' => $short2 = 'efgh', 110 'shorturl' => $short2 = 'efgh',
111 'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $date2), 111 'created' => DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, $date2),
112 ), 112 ),
113 ) 113 )
114 ); 114 );
@@ -136,7 +136,7 @@ class PluginIssoTest extends \PHPUnit\Framework\TestCase
136 'id' => 12, 136 'id' => 12,
137 'url' => $str, 137 'url' => $str,
138 'shorturl' => $short1 = 'abcd', 138 'shorturl' => $short1 = 'abcd',
139 'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $date), 139 'created' => DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, $date),
140 ) 140 )
141 ), 141 ),
142 'search_term' => $str 142 'search_term' => $str
diff --git a/tests/plugins/PluginMarkdownTest.php b/tests/plugins/PluginMarkdownTest.php
deleted file mode 100644
index 15fa9ba5..00000000
--- a/tests/plugins/PluginMarkdownTest.php
+++ /dev/null
@@ -1,316 +0,0 @@
1<?php
2namespace Shaarli\Plugin\Markdown;
3
4use Shaarli\Config\ConfigManager;
5use Shaarli\Plugin\PluginManager;
6
7/**
8 * PluginMarkdownTest.php
9 */
10
11require_once 'application/bookmark/LinkUtils.php';
12require_once 'application/Utils.php';
13require_once 'plugins/markdown/markdown.php';
14
15/**
16 * Class PluginMarkdownTest
17 * Unit test for the Markdown plugin
18 */
19class PluginMarkdownTest extends \PHPUnit\Framework\TestCase
20{
21 /**
22 * @var ConfigManager instance.
23 */
24 protected $conf;
25
26 /**
27 * Reset plugin path
28 */
29 public function setUp()
30 {
31 PluginManager::$PLUGINS_PATH = 'plugins';
32 $this->conf = new ConfigManager('tests/utils/config/configJson');
33 $this->conf->set('security.allowed_protocols', ['ftp', 'magnet']);
34 }
35
36 /**
37 * Test render_linklist hook.
38 * Only check that there is basic markdown rendering.
39 */
40 public function testMarkdownLinklist()
41 {
42 $markdown = '# My title' . PHP_EOL . 'Very interesting content.';
43 $data = array(
44 'links' => array(
45 0 => array(
46 'description' => $markdown,
47 ),
48 ),
49 );
50
51 $data = hook_markdown_render_linklist($data, $this->conf);
52 $this->assertNotFalse(strpos($data['links'][0]['description'], '<h1>'));
53 $this->assertNotFalse(strpos($data['links'][0]['description'], '<p>'));
54
55 $this->assertEquals($markdown, $data['links'][0]['description_src']);
56 }
57
58 /**
59 * Test render_feed hook.
60 */
61 public function testMarkdownFeed()
62 {
63 $markdown = '# My title' . PHP_EOL . 'Very interesting content.';
64 $markdown .= '&#8212; <a href="http://domain.tld/?0oc_VQ" title="Permalien">Permalien</a>';
65 $data = array(
66 'links' => array(
67 0 => array(
68 'description' => $markdown,
69 ),
70 ),
71 );
72
73 $data = hook_markdown_render_feed($data, $this->conf);
74 $this->assertNotFalse(strpos($data['links'][0]['description'], '<h1>'));
75 $this->assertNotFalse(strpos($data['links'][0]['description'], '<p>'));
76 $this->assertStringEndsWith(
77 '&#8212; <a href="http://domain.tld/?0oc_VQ">Permalien</a></p></div>',
78 $data['links'][0]['description']
79 );
80 }
81
82 /**
83 * Test render_daily hook.
84 * Only check that there is basic markdown rendering.
85 */
86 public function testMarkdownDaily()
87 {
88 $markdown = '# My title' . PHP_EOL . 'Very interesting content.';
89 $data = array(
90 // Columns data
91 'linksToDisplay' => array(
92 // nth link
93 0 => array(
94 'formatedDescription' => $markdown,
95 ),
96 ),
97 );
98
99 $data = hook_markdown_render_daily($data, $this->conf);
100 $this->assertNotFalse(strpos($data['linksToDisplay'][0]['formatedDescription'], '<h1>'));
101 $this->assertNotFalse(strpos($data['linksToDisplay'][0]['formatedDescription'], '<p>'));
102 }
103
104 /**
105 * Test reverse_text2clickable().
106 */
107 public function testReverseText2clickable()
108 {
109 $text = 'stuff http://hello.there/is=someone#here otherstuff';
110 $clickableText = text2clickable($text);
111 $reversedText = reverse_text2clickable($clickableText);
112 $this->assertEquals($text, $reversedText);
113 }
114
115 /**
116 * Test reverse_text2clickable().
117 */
118 public function testReverseText2clickableHashtags()
119 {
120 $text = file_get_contents('tests/plugins/resources/hashtags.raw');
121 $md = file_get_contents('tests/plugins/resources/hashtags.md');
122 $clickableText = hashtag_autolink($text);
123 $reversedText = reverse_text2clickable($clickableText);
124 $this->assertEquals($md, $reversedText);
125 }
126
127 /**
128 * Test reverse_nl2br().
129 */
130 public function testReverseNl2br()
131 {
132 $text = 'stuff' . PHP_EOL . 'otherstuff';
133 $processedText = nl2br($text);
134 $reversedText = reverse_nl2br($processedText);
135 $this->assertEquals($text, $reversedText);
136 }
137
138 /**
139 * Test reverse_space2nbsp().
140 */
141 public function testReverseSpace2nbsp()
142 {
143 $text = ' stuff' . PHP_EOL . ' otherstuff and another';
144 $processedText = space2nbsp($text);
145 $reversedText = reverse_space2nbsp($processedText);
146 $this->assertEquals($text, $reversedText);
147 }
148
149 public function testReverseFeedPermalink()
150 {
151 $text = 'Description... ';
152 $text .= '&#8212; <a href="http://domain.tld/?0oc_VQ" title="Permalien">Permalien</a>';
153 $expected = 'Description... &#8212; [Permalien](http://domain.tld/?0oc_VQ)';
154 $processedText = reverse_feed_permalink($text);
155
156 $this->assertEquals($expected, $processedText);
157 }
158
159 public function testReverseFeedDirectLink()
160 {
161 $text = 'Description... ';
162 $text .= '&#8212; <a href="http://domain.tld/?0oc_VQ" title="Direct link">Direct link</a>';
163 $expected = 'Description... &#8212; [Direct link](http://domain.tld/?0oc_VQ)';
164 $processedText = reverse_feed_permalink($text);
165
166 $this->assertEquals($expected, $processedText);
167 }
168
169 public function testReverseLastFeedPermalink()
170 {
171 $text = 'Description... ';
172 $text .= '<br>&#8212; <a href="http://domain.tld/?0oc_VQ" title="Permalien">Permalien</a>';
173 $expected = $text;
174 $text .= '<br>&#8212; <a href="http://domain.tld/?0oc_VQ" title="Permalien">Permalien</a>';
175 $expected .= '<br>&#8212; [Permalien](http://domain.tld/?0oc_VQ)';
176 $processedText = reverse_feed_permalink($text);
177
178 $this->assertEquals($expected, $processedText);
179 }
180
181 public function testReverseNoFeedPermalink()
182 {
183 $text = 'Hello! Where are you from?';
184 $expected = $text;
185 $processedText = reverse_feed_permalink($text);
186
187 $this->assertEquals($expected, $processedText);
188 }
189
190 /**
191 * Test sanitize_html().
192 */
193 public function testSanitizeHtml()
194 {
195 $input = '< script src="js.js"/>';
196 $input .= '< script attr>alert(\'xss\');</script>';
197 $input .= '<style> * { display: none }</style>';
198 $output = escape($input);
199 $input .= '<a href="#" onmouseHover="alert(\'xss\');" attr="tt">link</a>';
200 $output .= '<a href="#" attr="tt">link</a>';
201 $input .= '<a href="#" onmouseHover=alert(\'xss\'); attr="tt">link</a>';
202 $output .= '<a href="#" attr="tt">link</a>';
203 $this->assertEquals($output, sanitize_html($input));
204 // Do not touch escaped HTML.
205 $input = escape($input);
206 $this->assertEquals($input, sanitize_html($input));
207 }
208
209 /**
210 * Test the no markdown tag.
211 */
212 public function testNoMarkdownTag()
213 {
214 $str = 'All _work_ and `no play` makes Jack a *dull* boy.';
215 $data = array(
216 'links' => array(array(
217 'description' => $str,
218 'tags' => NO_MD_TAG,
219 'taglist' => array(NO_MD_TAG),
220 ))
221 );
222
223 $processed = hook_markdown_render_linklist($data, $this->conf);
224 $this->assertEquals($str, $processed['links'][0]['description']);
225
226 $processed = hook_markdown_render_feed($data, $this->conf);
227 $this->assertEquals($str, $processed['links'][0]['description']);
228
229 $data = array(
230 // Columns data
231 'linksToDisplay' => array(
232 // nth link
233 0 => array(
234 'formatedDescription' => $str,
235 'tags' => NO_MD_TAG,
236 'taglist' => array(),
237 ),
238 ),
239 );
240
241 $data = hook_markdown_render_daily($data, $this->conf);
242 $this->assertEquals($str, $data['linksToDisplay'][0]['formatedDescription']);
243 }
244
245 /**
246 * Test that a close value to nomarkdown is not understand as nomarkdown (previous value `.nomarkdown`).
247 */
248 public function testNoMarkdownNotExcactlyMatching()
249 {
250 $str = 'All _work_ and `no play` makes Jack a *dull* boy.';
251 $data = array(
252 'links' => array(array(
253 'description' => $str,
254 'tags' => '.' . NO_MD_TAG,
255 'taglist' => array('.'. NO_MD_TAG),
256 ))
257 );
258
259 $data = hook_markdown_render_feed($data, $this->conf);
260 $this->assertContains('<em>', $data['links'][0]['description']);
261 }
262
263 /**
264 * Make sure that the generated HTML match the reference HTML file.
265 */
266 public function testMarkdownGlobalProcessDescription()
267 {
268 $md = file_get_contents('tests/plugins/resources/markdown.md');
269 $md = format_description($md);
270 $html = file_get_contents('tests/plugins/resources/markdown.html');
271
272 $data = process_markdown(
273 $md,
274 $this->conf->get('security.markdown_escape', true),
275 $this->conf->get('security.allowed_protocols')
276 );
277 $this->assertEquals($html, $data . PHP_EOL);
278 }
279
280 /**
281 * Make sure that the HTML tags are escaped.
282 */
283 public function testMarkdownWithHtmlEscape()
284 {
285 $md = '**strong** <strong>strong</strong>';
286 $html = '<div class="markdown"><p><strong>strong</strong> &lt;strong&gt;strong&lt;/strong&gt;</p></div>';
287 $data = array(
288 'links' => array(
289 0 => array(
290 'description' => $md,
291 ),
292 ),
293 );
294 $data = hook_markdown_render_linklist($data, $this->conf);
295 $this->assertEquals($html, $data['links'][0]['description']);
296 }
297
298 /**
299 * Make sure that the HTML tags aren't escaped with the setting set to false.
300 */
301 public function testMarkdownWithHtmlNoEscape()
302 {
303 $this->conf->set('security.markdown_escape', false);
304 $md = '**strong** <strong>strong</strong>';
305 $html = '<div class="markdown"><p><strong>strong</strong> <strong>strong</strong></p></div>';
306 $data = array(
307 'links' => array(
308 0 => array(
309 'description' => $md,
310 ),
311 ),
312 );
313 $data = hook_markdown_render_linklist($data, $this->conf);
314 $this->assertEquals($html, $data['links'][0]['description']);
315 }
316}
diff --git a/tests/updater/DummyUpdater.php b/tests/updater/DummyUpdater.php
index 9e866f1f..07c7f5c4 100644
--- a/tests/updater/DummyUpdater.php
+++ b/tests/updater/DummyUpdater.php
@@ -4,6 +4,7 @@ namespace Shaarli\Updater;
4use Exception; 4use Exception;
5use ReflectionClass; 5use ReflectionClass;
6use ReflectionMethod; 6use ReflectionMethod;
7use Shaarli\Bookmark\BookmarkFileService;
7use Shaarli\Bookmark\LinkDB; 8use Shaarli\Bookmark\LinkDB;
8use Shaarli\Config\ConfigManager; 9use Shaarli\Config\ConfigManager;
9 10
@@ -16,14 +17,14 @@ class DummyUpdater extends Updater
16 /** 17 /**
17 * Object constructor. 18 * Object constructor.
18 * 19 *
19 * @param array $doneUpdates Updates which are already done. 20 * @param array $doneUpdates Updates which are already done.
20 * @param LinkDB $linkDB LinkDB instance. 21 * @param BookmarkFileService $bookmarkService LinkDB instance.
21 * @param ConfigManager $conf Configuration Manager instance. 22 * @param ConfigManager $conf Configuration Manager instance.
22 * @param boolean $isLoggedIn True if the user is logged in. 23 * @param boolean $isLoggedIn True if the user is logged in.
23 */ 24 */
24 public function __construct($doneUpdates, $linkDB, $conf, $isLoggedIn) 25 public function __construct($doneUpdates, $bookmarkService, $conf, $isLoggedIn)
25 { 26 {
26 parent::__construct($doneUpdates, $linkDB, $conf, $isLoggedIn); 27 parent::__construct($doneUpdates, $bookmarkService, $conf, $isLoggedIn);
27 28
28 // Retrieve all update methods. 29 // Retrieve all update methods.
29 // For unit test, only retrieve final methods, 30 // For unit test, only retrieve final methods,
diff --git a/tests/updater/UpdaterTest.php b/tests/updater/UpdaterTest.php
index ac87e33c..c689982b 100644
--- a/tests/updater/UpdaterTest.php
+++ b/tests/updater/UpdaterTest.php
@@ -1,15 +1,9 @@
1<?php 1<?php
2namespace Shaarli\Updater; 2namespace Shaarli\Updater;
3 3
4use DateTime;
5use Exception; 4use Exception;
6use Shaarli\Bookmark\LinkDB;
7use Shaarli\Config\ConfigJson;
8use Shaarli\Config\ConfigManager; 5use Shaarli\Config\ConfigManager;
9use Shaarli\Config\ConfigPhp;
10use Shaarli\Thumbnailer;
11 6
12require_once 'application/updater/UpdaterUtils.php';
13require_once 'tests/updater/DummyUpdater.php'; 7require_once 'tests/updater/DummyUpdater.php';
14require_once 'tests/utils/ReferenceLinkDB.php'; 8require_once 'tests/utils/ReferenceLinkDB.php';
15require_once 'inc/rain.tpl.class.php'; 9require_once 'inc/rain.tpl.class.php';
@@ -45,14 +39,14 @@ class UpdaterTest extends \PHPUnit\Framework\TestCase
45 } 39 }
46 40
47 /** 41 /**
48 * Test read_updates_file with an empty/missing file. 42 * Test UpdaterUtils::read_updates_file with an empty/missing file.
49 */ 43 */
50 public function testReadEmptyUpdatesFile() 44 public function testReadEmptyUpdatesFile()
51 { 45 {
52 $this->assertEquals(array(), read_updates_file('')); 46 $this->assertEquals(array(), UpdaterUtils::read_updates_file(''));
53 $updatesFile = $this->conf->get('resource.data_dir') . '/updates.txt'; 47 $updatesFile = $this->conf->get('resource.data_dir') . '/updates.txt';
54 touch($updatesFile); 48 touch($updatesFile);
55 $this->assertEquals(array(), read_updates_file($updatesFile)); 49 $this->assertEquals(array(), UpdaterUtils::read_updates_file($updatesFile));
56 unlink($updatesFile); 50 unlink($updatesFile);
57 } 51 }
58 52
@@ -64,31 +58,31 @@ class UpdaterTest extends \PHPUnit\Framework\TestCase
64 $updatesFile = $this->conf->get('resource.data_dir') . '/updates.txt'; 58 $updatesFile = $this->conf->get('resource.data_dir') . '/updates.txt';
65 $updatesMethods = array('m1', 'm2', 'm3'); 59 $updatesMethods = array('m1', 'm2', 'm3');
66 60
67 write_updates_file($updatesFile, $updatesMethods); 61 UpdaterUtils::write_updates_file($updatesFile, $updatesMethods);
68 $readMethods = read_updates_file($updatesFile); 62 $readMethods = UpdaterUtils::read_updates_file($updatesFile);
69 $this->assertEquals($readMethods, $updatesMethods); 63 $this->assertEquals($readMethods, $updatesMethods);
70 64
71 // Update 65 // Update
72 $updatesMethods[] = 'm4'; 66 $updatesMethods[] = 'm4';
73 write_updates_file($updatesFile, $updatesMethods); 67 UpdaterUtils::write_updates_file($updatesFile, $updatesMethods);
74 $readMethods = read_updates_file($updatesFile); 68 $readMethods = UpdaterUtils::read_updates_file($updatesFile);
75 $this->assertEquals($readMethods, $updatesMethods); 69 $this->assertEquals($readMethods, $updatesMethods);
76 unlink($updatesFile); 70 unlink($updatesFile);
77 } 71 }
78 72
79 /** 73 /**
80 * Test errors in write_updates_file(): empty updates file. 74 * Test errors in UpdaterUtils::write_updates_file(): empty updates file.
81 * 75 *
82 * @expectedException Exception 76 * @expectedException Exception
83 * @expectedExceptionMessageRegExp /Updates file path is not set(.*)/ 77 * @expectedExceptionMessageRegExp /Updates file path is not set(.*)/
84 */ 78 */
85 public function testWriteEmptyUpdatesFile() 79 public function testWriteEmptyUpdatesFile()
86 { 80 {
87 write_updates_file('', array('test')); 81 UpdaterUtils::write_updates_file('', array('test'));
88 } 82 }
89 83
90 /** 84 /**
91 * Test errors in write_updates_file(): not writable updates file. 85 * Test errors in UpdaterUtils::write_updates_file(): not writable updates file.
92 * 86 *
93 * @expectedException Exception 87 * @expectedException Exception
94 * @expectedExceptionMessageRegExp /Unable to write(.*)/ 88 * @expectedExceptionMessageRegExp /Unable to write(.*)/
@@ -99,7 +93,7 @@ class UpdaterTest extends \PHPUnit\Framework\TestCase
99 touch($updatesFile); 93 touch($updatesFile);
100 chmod($updatesFile, 0444); 94 chmod($updatesFile, 0444);
101 try { 95 try {
102 @write_updates_file($updatesFile, array('test')); 96 @UpdaterUtils::write_updates_file($updatesFile, array('test'));
103 } catch (Exception $e) { 97 } catch (Exception $e) {
104 unlink($updatesFile); 98 unlink($updatesFile);
105 throw $e; 99 throw $e;
@@ -173,660 +167,4 @@ class UpdaterTest extends \PHPUnit\Framework\TestCase
173 $updater = new DummyUpdater($updates, array(), $this->conf, true); 167 $updater = new DummyUpdater($updates, array(), $this->conf, true);
174 $updater->update(); 168 $updater->update();
175 } 169 }
176
177 /**
178 * Test update mergeDeprecatedConfig:
179 * 1. init a config file.
180 * 2. init a options.php file with update value.
181 * 3. merge.
182 * 4. check updated value in config file.
183 */
184 public function testUpdateMergeDeprecatedConfig()
185 {
186 $this->conf->setConfigFile('tests/utils/config/configPhp');
187 $this->conf->reset();
188
189 $optionsFile = 'tests/updater/options.php';
190 $options = '<?php
191$GLOBALS[\'privateLinkByDefault\'] = true;';
192 file_put_contents($optionsFile, $options);
193
194 // tmp config file.
195 $this->conf->setConfigFile('tests/updater/config');
196
197 // merge configs
198 $updater = new Updater(array(), array(), $this->conf, true);
199 // This writes a new config file in tests/updater/config.php
200 $updater->updateMethodMergeDeprecatedConfigFile();
201
202 // make sure updated field is changed
203 $this->conf->reload();
204 $this->assertTrue($this->conf->get('privacy.default_private_links'));
205 $this->assertFalse(is_file($optionsFile));
206 // Delete the generated file.
207 unlink($this->conf->getConfigFileExt());
208 }
209
210 /**
211 * Test mergeDeprecatedConfig in without options file.
212 */
213 public function testMergeDeprecatedConfigNoFile()
214 {
215 $updater = new Updater(array(), array(), $this->conf, true);
216 $updater->updateMethodMergeDeprecatedConfigFile();
217
218 $this->assertEquals('root', $this->conf->get('credentials.login'));
219 }
220
221 /**
222 * Test renameDashTags update method.
223 */
224 public function testRenameDashTags()
225 {
226 $refDB = new \ReferenceLinkDB();
227 $refDB->write(self::$testDatastore);
228 $linkDB = new LinkDB(self::$testDatastore, true, false);
229
230 $this->assertEmpty($linkDB->filterSearch(array('searchtags' => 'exclude')));
231 $updater = new Updater(array(), $linkDB, $this->conf, true);
232 $updater->updateMethodRenameDashTags();
233 $this->assertNotEmpty($linkDB->filterSearch(array('searchtags' => 'exclude')));
234 }
235
236 /**
237 * Convert old PHP config file to JSON config.
238 */
239 public function testConfigToJson()
240 {
241 $configFile = 'tests/utils/config/configPhp';
242 $this->conf->setConfigFile($configFile);
243 $this->conf->reset();
244
245 // The ConfigIO is initialized with ConfigPhp.
246 $this->assertTrue($this->conf->getConfigIO() instanceof ConfigPhp);
247
248 $updater = new Updater(array(), array(), $this->conf, false);
249 $done = $updater->updateMethodConfigToJson();
250 $this->assertTrue($done);
251
252 // The ConfigIO has been updated to ConfigJson.
253 $this->assertTrue($this->conf->getConfigIO() instanceof ConfigJson);
254 $this->assertTrue(file_exists($this->conf->getConfigFileExt()));
255
256 // Check JSON config data.
257 $this->conf->reload();
258 $this->assertEquals('root', $this->conf->get('credentials.login'));
259 $this->assertEquals('lala', $this->conf->get('redirector.url'));
260 $this->assertEquals('data/datastore.php', $this->conf->get('resource.datastore'));
261 $this->assertEquals('1', $this->conf->get('plugins.WALLABAG_VERSION'));
262
263 rename($configFile . '.save.php', $configFile . '.php');
264 unlink($this->conf->getConfigFileExt());
265 }
266
267 /**
268 * Launch config conversion update with an existing JSON file => nothing to do.
269 */
270 public function testConfigToJsonNothingToDo()
271 {
272 $filetime = filemtime($this->conf->getConfigFileExt());
273 $updater = new Updater(array(), array(), $this->conf, false);
274 $done = $updater->updateMethodConfigToJson();
275 $this->assertTrue($done);
276 $expected = filemtime($this->conf->getConfigFileExt());
277 $this->assertEquals($expected, $filetime);
278 }
279
280 /**
281 * Test escapeUnescapedConfig with valid data.
282 */
283 public function testEscapeConfig()
284 {
285 $sandbox = 'sandbox/config';
286 copy(self::$configFile . '.json.php', $sandbox . '.json.php');
287 $this->conf = new ConfigManager($sandbox);
288 $title = '<script>alert("title");</script>';
289 $headerLink = '<script>alert("header_link");</script>';
290 $this->conf->set('general.title', $title);
291 $this->conf->set('general.header_link', $headerLink);
292 $updater = new Updater(array(), array(), $this->conf, true);
293 $done = $updater->updateMethodEscapeUnescapedConfig();
294 $this->assertTrue($done);
295 $this->conf->reload();
296 $this->assertEquals(escape($title), $this->conf->get('general.title'));
297 $this->assertEquals(escape($headerLink), $this->conf->get('general.header_link'));
298 unlink($sandbox . '.json.php');
299 }
300
301 /**
302 * Test updateMethodApiSettings(): create default settings for the API (enabled + secret).
303 */
304 public function testUpdateApiSettings()
305 {
306 $confFile = 'sandbox/config';
307 copy(self::$configFile .'.json.php', $confFile .'.json.php');
308 $conf = new ConfigManager($confFile);
309 $updater = new Updater(array(), array(), $conf, true);
310
311 $this->assertFalse($conf->exists('api.enabled'));
312 $this->assertFalse($conf->exists('api.secret'));
313 $updater->updateMethodApiSettings();
314 $conf->reload();
315 $this->assertTrue($conf->get('api.enabled'));
316 $this->assertTrue($conf->exists('api.secret'));
317 unlink($confFile .'.json.php');
318 }
319
320 /**
321 * Test updateMethodApiSettings(): already set, do nothing.
322 */
323 public function testUpdateApiSettingsNothingToDo()
324 {
325 $confFile = 'sandbox/config';
326 copy(self::$configFile .'.json.php', $confFile .'.json.php');
327 $conf = new ConfigManager($confFile);
328 $conf->set('api.enabled', false);
329 $conf->set('api.secret', '');
330 $updater = new Updater(array(), array(), $conf, true);
331 $updater->updateMethodApiSettings();
332 $this->assertFalse($conf->get('api.enabled'));
333 $this->assertEmpty($conf->get('api.secret'));
334 unlink($confFile .'.json.php');
335 }
336
337 /**
338 * Test updateMethodDatastoreIds().
339 */
340 public function testDatastoreIds()
341 {
342 $links = array(
343 '20121206_182539' => array(
344 'linkdate' => '20121206_182539',
345 'title' => 'Geek and Poke',
346 'url' => 'http://geek-and-poke.com/',
347 'description' => 'desc',
348 'tags' => 'dev cartoon tag1 tag2 tag3 tag4 ',
349 'updated' => '20121206_190301',
350 'private' => false,
351 ),
352 '20121206_172539' => array(
353 'linkdate' => '20121206_172539',
354 'title' => 'UserFriendly - Samba',
355 'url' => 'http://ars.userfriendly.org/cartoons/?id=20010306',
356 'description' => '',
357 'tags' => 'samba cartoon web',
358 'private' => false,
359 ),
360 '20121206_142300' => array(
361 'linkdate' => '20121206_142300',
362 'title' => 'UserFriendly - Web Designer',
363 'url' => 'http://ars.userfriendly.org/cartoons/?id=20121206',
364 'description' => 'Naming conventions... #private',
365 'tags' => 'samba cartoon web',
366 'private' => true,
367 ),
368 );
369 $refDB = new \ReferenceLinkDB();
370 $refDB->setLinks($links);
371 $refDB->write(self::$testDatastore);
372 $linkDB = new LinkDB(self::$testDatastore, true, false);
373
374 $checksum = hash_file('sha1', self::$testDatastore);
375
376 $this->conf->set('resource.data_dir', 'sandbox');
377 $this->conf->set('resource.datastore', self::$testDatastore);
378
379 $updater = new Updater(array(), $linkDB, $this->conf, true);
380 $this->assertTrue($updater->updateMethodDatastoreIds());
381
382 $linkDB = new LinkDB(self::$testDatastore, true, false);
383
384 $backup = glob($this->conf->get('resource.data_dir') . '/datastore.'. date('YmdH') .'*.php');
385 $backup = $backup[0];
386
387 $this->assertFileExists($backup);
388 $this->assertEquals($checksum, hash_file('sha1', $backup));
389 unlink($backup);
390
391 $this->assertEquals(3, count($linkDB));
392 $this->assertTrue(isset($linkDB[0]));
393 $this->assertFalse(isset($linkDB[0]['linkdate']));
394 $this->assertEquals(0, $linkDB[0]['id']);
395 $this->assertEquals('UserFriendly - Web Designer', $linkDB[0]['title']);
396 $this->assertEquals('http://ars.userfriendly.org/cartoons/?id=20121206', $linkDB[0]['url']);
397 $this->assertEquals('Naming conventions... #private', $linkDB[0]['description']);
398 $this->assertEquals('samba cartoon web', $linkDB[0]['tags']);
399 $this->assertTrue($linkDB[0]['private']);
400 $this->assertEquals(
401 DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20121206_142300'),
402 $linkDB[0]['created']
403 );
404
405 $this->assertTrue(isset($linkDB[1]));
406 $this->assertFalse(isset($linkDB[1]['linkdate']));
407 $this->assertEquals(1, $linkDB[1]['id']);
408 $this->assertEquals('UserFriendly - Samba', $linkDB[1]['title']);
409 $this->assertEquals(
410 DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20121206_172539'),
411 $linkDB[1]['created']
412 );
413
414 $this->assertTrue(isset($linkDB[2]));
415 $this->assertFalse(isset($linkDB[2]['linkdate']));
416 $this->assertEquals(2, $linkDB[2]['id']);
417 $this->assertEquals('Geek and Poke', $linkDB[2]['title']);
418 $this->assertEquals(
419 DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20121206_182539'),
420 $linkDB[2]['created']
421 );
422 $this->assertEquals(
423 DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20121206_190301'),
424 $linkDB[2]['updated']
425 );
426 }
427
428 /**
429 * Test updateMethodDatastoreIds() with the update already applied: nothing to do.
430 */
431 public function testDatastoreIdsNothingToDo()
432 {
433 $refDB = new \ReferenceLinkDB();
434 $refDB->write(self::$testDatastore);
435 $linkDB = new LinkDB(self::$testDatastore, true, false);
436
437 $this->conf->set('resource.data_dir', 'sandbox');
438 $this->conf->set('resource.datastore', self::$testDatastore);
439
440 $checksum = hash_file('sha1', self::$testDatastore);
441 $updater = new Updater(array(), $linkDB, $this->conf, true);
442 $this->assertTrue($updater->updateMethodDatastoreIds());
443 $this->assertEquals($checksum, hash_file('sha1', self::$testDatastore));
444 }
445
446 /**
447 * Test defaultTheme update with default settings: nothing to do.
448 */
449 public function testDefaultThemeWithDefaultSettings()
450 {
451 $sandbox = 'sandbox/config';
452 copy(self::$configFile . '.json.php', $sandbox . '.json.php');
453 $this->conf = new ConfigManager($sandbox);
454 $updater = new Updater([], [], $this->conf, true);
455 $this->assertTrue($updater->updateMethodDefaultTheme());
456
457 $this->assertEquals('tpl/', $this->conf->get('resource.raintpl_tpl'));
458 $this->assertEquals('default', $this->conf->get('resource.theme'));
459 $this->conf = new ConfigManager($sandbox);
460 $this->assertEquals('tpl/', $this->conf->get('resource.raintpl_tpl'));
461 $this->assertEquals('default', $this->conf->get('resource.theme'));
462 unlink($sandbox . '.json.php');
463 }
464
465 /**
466 * Test defaultTheme update with a custom theme in a subfolder
467 */
468 public function testDefaultThemeWithCustomTheme()
469 {
470 $theme = 'iamanartist';
471 $sandbox = 'sandbox/config';
472 copy(self::$configFile . '.json.php', $sandbox . '.json.php');
473 $this->conf = new ConfigManager($sandbox);
474 mkdir('sandbox/'. $theme);
475 touch('sandbox/'. $theme .'/linklist.html');
476 $this->conf->set('resource.raintpl_tpl', 'sandbox/'. $theme .'/');
477 $updater = new Updater([], [], $this->conf, true);
478 $this->assertTrue($updater->updateMethodDefaultTheme());
479
480 $this->assertEquals('sandbox', $this->conf->get('resource.raintpl_tpl'));
481 $this->assertEquals($theme, $this->conf->get('resource.theme'));
482 $this->conf = new ConfigManager($sandbox);
483 $this->assertEquals('sandbox', $this->conf->get('resource.raintpl_tpl'));
484 $this->assertEquals($theme, $this->conf->get('resource.theme'));
485 unlink($sandbox . '.json.php');
486 unlink('sandbox/'. $theme .'/linklist.html');
487 rmdir('sandbox/'. $theme);
488 }
489
490 /**
491 * Test updateMethodEscapeMarkdown with markdown plugin enabled
492 * => setting markdown_escape set to false.
493 */
494 public function testEscapeMarkdownSettingToFalse()
495 {
496 $sandboxConf = 'sandbox/config';
497 copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
498 $this->conf = new ConfigManager($sandboxConf);
499
500 $this->conf->set('general.enabled_plugins', ['markdown']);
501 $updater = new Updater([], [], $this->conf, true);
502 $this->assertTrue($updater->updateMethodEscapeMarkdown());
503 $this->assertFalse($this->conf->get('security.markdown_escape'));
504
505 // reload from file
506 $this->conf = new ConfigManager($sandboxConf);
507 $this->assertFalse($this->conf->get('security.markdown_escape'));
508 }
509
510
511 /**
512 * Test updateMethodEscapeMarkdown with markdown plugin disabled
513 * => setting markdown_escape set to true.
514 */
515 public function testEscapeMarkdownSettingToTrue()
516 {
517 $sandboxConf = 'sandbox/config';
518 copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
519 $this->conf = new ConfigManager($sandboxConf);
520
521 $this->conf->set('general.enabled_plugins', []);
522 $updater = new Updater([], [], $this->conf, true);
523 $this->assertTrue($updater->updateMethodEscapeMarkdown());
524 $this->assertTrue($this->conf->get('security.markdown_escape'));
525
526 // reload from file
527 $this->conf = new ConfigManager($sandboxConf);
528 $this->assertTrue($this->conf->get('security.markdown_escape'));
529 }
530
531 /**
532 * Test updateMethodEscapeMarkdown with nothing to do (setting already enabled)
533 */
534 public function testEscapeMarkdownSettingNothingToDoEnabled()
535 {
536 $sandboxConf = 'sandbox/config';
537 copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
538 $this->conf = new ConfigManager($sandboxConf);
539 $this->conf->set('security.markdown_escape', true);
540 $updater = new Updater([], [], $this->conf, true);
541 $this->assertTrue($updater->updateMethodEscapeMarkdown());
542 $this->assertTrue($this->conf->get('security.markdown_escape'));
543 }
544
545 /**
546 * Test updateMethodEscapeMarkdown with nothing to do (setting already disabled)
547 */
548 public function testEscapeMarkdownSettingNothingToDoDisabled()
549 {
550 $this->conf->set('security.markdown_escape', false);
551 $updater = new Updater([], [], $this->conf, true);
552 $this->assertTrue($updater->updateMethodEscapeMarkdown());
553 $this->assertFalse($this->conf->get('security.markdown_escape'));
554 }
555
556 /**
557 * Test updateMethodPiwikUrl with valid data
558 */
559 public function testUpdatePiwikUrlValid()
560 {
561 $sandboxConf = 'sandbox/config';
562 copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
563 $this->conf = new ConfigManager($sandboxConf);
564 $url = 'mypiwik.tld';
565 $this->conf->set('plugins.PIWIK_URL', $url);
566 $updater = new Updater([], [], $this->conf, true);
567 $this->assertTrue($updater->updateMethodPiwikUrl());
568 $this->assertEquals('http://'. $url, $this->conf->get('plugins.PIWIK_URL'));
569
570 // reload from file
571 $this->conf = new ConfigManager($sandboxConf);
572 $this->assertEquals('http://'. $url, $this->conf->get('plugins.PIWIK_URL'));
573 }
574
575 /**
576 * Test updateMethodPiwikUrl without setting
577 */
578 public function testUpdatePiwikUrlEmpty()
579 {
580 $updater = new Updater([], [], $this->conf, true);
581 $this->assertTrue($updater->updateMethodPiwikUrl());
582 $this->assertEmpty($this->conf->get('plugins.PIWIK_URL'));
583 }
584
585 /**
586 * Test updateMethodPiwikUrl: valid URL, nothing to do
587 */
588 public function testUpdatePiwikUrlNothingToDo()
589 {
590 $url = 'https://mypiwik.tld';
591 $this->conf->set('plugins.PIWIK_URL', $url);
592 $updater = new Updater([], [], $this->conf, true);
593 $this->assertTrue($updater->updateMethodPiwikUrl());
594 $this->assertEquals($url, $this->conf->get('plugins.PIWIK_URL'));
595 }
596
597 /**
598 * Test updateMethodAtomDefault with show_atom set to false
599 * => update to true.
600 */
601 public function testUpdateMethodAtomDefault()
602 {
603 $sandboxConf = 'sandbox/config';
604 copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
605 $this->conf = new ConfigManager($sandboxConf);
606 $this->conf->set('feed.show_atom', false);
607 $updater = new Updater([], [], $this->conf, true);
608 $this->assertTrue($updater->updateMethodAtomDefault());
609 $this->assertTrue($this->conf->get('feed.show_atom'));
610 // reload from file
611 $this->conf = new ConfigManager($sandboxConf);
612 $this->assertTrue($this->conf->get('feed.show_atom'));
613 }
614 /**
615 * Test updateMethodAtomDefault with show_atom not set.
616 * => nothing to do
617 */
618 public function testUpdateMethodAtomDefaultNoExist()
619 {
620 $sandboxConf = 'sandbox/config';
621 copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
622 $this->conf = new ConfigManager($sandboxConf);
623 $updater = new Updater([], [], $this->conf, true);
624 $this->assertTrue($updater->updateMethodAtomDefault());
625 $this->assertTrue($this->conf->get('feed.show_atom'));
626 }
627 /**
628 * Test updateMethodAtomDefault with show_atom set to true.
629 * => nothing to do
630 */
631 public function testUpdateMethodAtomDefaultAlreadyTrue()
632 {
633 $sandboxConf = 'sandbox/config';
634 copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
635 $this->conf = new ConfigManager($sandboxConf);
636 $this->conf->set('feed.show_atom', true);
637 $updater = new Updater([], [], $this->conf, true);
638 $this->assertTrue($updater->updateMethodAtomDefault());
639 $this->assertTrue($this->conf->get('feed.show_atom'));
640 }
641
642 /**
643 * Test updateMethodDownloadSizeAndTimeoutConf, it should be set if none is already defined.
644 */
645 public function testUpdateMethodDownloadSizeAndTimeoutConf()
646 {
647 $sandboxConf = 'sandbox/config';
648 copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
649 $this->conf = new ConfigManager($sandboxConf);
650 $updater = new Updater([], [], $this->conf, true);
651 $this->assertTrue($updater->updateMethodDownloadSizeAndTimeoutConf());
652 $this->assertEquals(4194304, $this->conf->get('general.download_max_size'));
653 $this->assertEquals(30, $this->conf->get('general.download_timeout'));
654
655 $this->conf = new ConfigManager($sandboxConf);
656 $this->assertEquals(4194304, $this->conf->get('general.download_max_size'));
657 $this->assertEquals(30, $this->conf->get('general.download_timeout'));
658 }
659
660 /**
661 * Test updateMethodDownloadSizeAndTimeoutConf, it shouldn't be set if it is already defined.
662 */
663 public function testUpdateMethodDownloadSizeAndTimeoutConfIgnore()
664 {
665 $sandboxConf = 'sandbox/config';
666 copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
667 $this->conf = new ConfigManager($sandboxConf);
668 $this->conf->set('general.download_max_size', 38);
669 $this->conf->set('general.download_timeout', 70);
670 $updater = new Updater([], [], $this->conf, true);
671 $this->assertTrue($updater->updateMethodDownloadSizeAndTimeoutConf());
672 $this->assertEquals(38, $this->conf->get('general.download_max_size'));
673 $this->assertEquals(70, $this->conf->get('general.download_timeout'));
674 }
675
676 /**
677 * Test updateMethodDownloadSizeAndTimeoutConf, only the maz size should be set here.
678 */
679 public function testUpdateMethodDownloadSizeAndTimeoutConfOnlySize()
680 {
681 $sandboxConf = 'sandbox/config';
682 copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
683 $this->conf = new ConfigManager($sandboxConf);
684 $this->conf->set('general.download_max_size', 38);
685 $updater = new Updater([], [], $this->conf, true);
686 $this->assertTrue($updater->updateMethodDownloadSizeAndTimeoutConf());
687 $this->assertEquals(38, $this->conf->get('general.download_max_size'));
688 $this->assertEquals(30, $this->conf->get('general.download_timeout'));
689 }
690
691 /**
692 * Test updateMethodDownloadSizeAndTimeoutConf, only the time out should be set here.
693 */
694 public function testUpdateMethodDownloadSizeAndTimeoutConfOnlyTimeout()
695 {
696 $sandboxConf = 'sandbox/config';
697 copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
698 $this->conf = new ConfigManager($sandboxConf);
699 $this->conf->set('general.download_timeout', 3);
700 $updater = new Updater([], [], $this->conf, true);
701 $this->assertTrue($updater->updateMethodDownloadSizeAndTimeoutConf());
702 $this->assertEquals(4194304, $this->conf->get('general.download_max_size'));
703 $this->assertEquals(3, $this->conf->get('general.download_timeout'));
704 }
705
706 /**
707 * Test updateMethodWebThumbnailer with thumbnails enabled.
708 */
709 public function testUpdateMethodWebThumbnailerEnabled()
710 {
711 $this->conf->remove('thumbnails');
712 $this->conf->set('thumbnail.enable_thumbnails', true);
713 $updater = new Updater([], [], $this->conf, true, $_SESSION);
714 $this->assertTrue($updater->updateMethodWebThumbnailer());
715 $this->assertFalse($this->conf->exists('thumbnail'));
716 $this->assertEquals(\Shaarli\Thumbnailer::MODE_ALL, $this->conf->get('thumbnails.mode'));
717 $this->assertEquals(125, $this->conf->get('thumbnails.width'));
718 $this->assertEquals(90, $this->conf->get('thumbnails.height'));
719 $this->assertContains('You have enabled or changed thumbnails', $_SESSION['warnings'][0]);
720 }
721
722 /**
723 * Test updateMethodWebThumbnailer with thumbnails disabled.
724 */
725 public function testUpdateMethodWebThumbnailerDisabled()
726 {
727 if (isset($_SESSION['warnings'])) {
728 unset($_SESSION['warnings']);
729 }
730 $this->conf->remove('thumbnails');
731 $this->conf->set('thumbnail.enable_thumbnails', false);
732 $updater = new Updater([], [], $this->conf, true, $_SESSION);
733 $this->assertTrue($updater->updateMethodWebThumbnailer());
734 $this->assertFalse($this->conf->exists('thumbnail'));
735 $this->assertEquals(Thumbnailer::MODE_NONE, $this->conf->get('thumbnails.mode'));
736 $this->assertEquals(125, $this->conf->get('thumbnails.width'));
737 $this->assertEquals(90, $this->conf->get('thumbnails.height'));
738 $this->assertTrue(empty($_SESSION['warnings']));
739 }
740
741 /**
742 * Test updateMethodWebThumbnailer with thumbnails disabled.
743 */
744 public function testUpdateMethodWebThumbnailerNothingToDo()
745 {
746 if (isset($_SESSION['warnings'])) {
747 unset($_SESSION['warnings']);
748 }
749 $updater = new Updater([], [], $this->conf, true, $_SESSION);
750 $this->assertTrue($updater->updateMethodWebThumbnailer());
751 $this->assertFalse($this->conf->exists('thumbnail'));
752 $this->assertEquals(Thumbnailer::MODE_COMMON, $this->conf->get('thumbnails.mode'));
753 $this->assertEquals(90, $this->conf->get('thumbnails.width'));
754 $this->assertEquals(53, $this->conf->get('thumbnails.height'));
755 $this->assertTrue(empty($_SESSION['warnings']));
756 }
757
758 /**
759 * Test updateMethodSetSticky().
760 */
761 public function testUpdateStickyValid()
762 {
763 $blank = [
764 'id' => 1,
765 'url' => 'z',
766 'title' => '',
767 'description' => '',
768 'tags' => '',
769 'created' => new DateTime(),
770 ];
771 $links = [
772 1 => ['id' => 1] + $blank,
773 2 => ['id' => 2] + $blank,
774 ];
775 $refDB = new \ReferenceLinkDB();
776 $refDB->setLinks($links);
777 $refDB->write(self::$testDatastore);
778 $linkDB = new LinkDB(self::$testDatastore, true, false);
779
780 $updater = new Updater(array(), $linkDB, $this->conf, true);
781 $this->assertTrue($updater->updateMethodSetSticky());
782
783 $linkDB = new LinkDB(self::$testDatastore, true, false);
784 foreach ($linkDB as $link) {
785 $this->assertFalse($link['sticky']);
786 }
787 }
788
789 /**
790 * Test updateMethodSetSticky().
791 */
792 public function testUpdateStickyNothingToDo()
793 {
794 $blank = [
795 'id' => 1,
796 'url' => 'z',
797 'title' => '',
798 'description' => '',
799 'tags' => '',
800 'created' => new DateTime(),
801 ];
802 $links = [
803 1 => ['id' => 1, 'sticky' => true] + $blank,
804 2 => ['id' => 2] + $blank,
805 ];
806 $refDB = new \ReferenceLinkDB();
807 $refDB->setLinks($links);
808 $refDB->write(self::$testDatastore);
809 $linkDB = new LinkDB(self::$testDatastore, true, false);
810
811 $updater = new Updater(array(), $linkDB, $this->conf, true);
812 $this->assertTrue($updater->updateMethodSetSticky());
813
814 $linkDB = new LinkDB(self::$testDatastore, true, false);
815 $this->assertTrue($linkDB[1]['sticky']);
816 }
817
818 /**
819 * Test updateMethodRemoveRedirector().
820 */
821 public function testUpdateRemoveRedirector()
822 {
823 $sandboxConf = 'sandbox/config';
824 copy(self::$configFile . '.json.php', $sandboxConf . '.json.php');
825 $this->conf = new ConfigManager($sandboxConf);
826 $updater = new Updater([], null, $this->conf, true);
827 $this->assertTrue($updater->updateMethodRemoveRedirector());
828 $this->assertFalse($this->conf->exists('redirector'));
829 $this->conf = new ConfigManager($sandboxConf);
830 $this->assertFalse($this->conf->exists('redirector'));
831 }
832} 170}
diff --git a/tests/utils/FakeBookmarkService.php b/tests/utils/FakeBookmarkService.php
new file mode 100644
index 00000000..1ec5bc3d
--- /dev/null
+++ b/tests/utils/FakeBookmarkService.php
@@ -0,0 +1,18 @@
1<?php
2
3
4use Shaarli\Bookmark\BookmarkArray;
5use Shaarli\Bookmark\BookmarkFilter;
6use Shaarli\Bookmark\BookmarkIO;
7use Shaarli\Bookmark\BookmarkFileService;
8use Shaarli\Bookmark\Exception\EmptyDataStoreException;
9use Shaarli\Config\ConfigManager;
10use Shaarli\History;
11
12class FakeBookmarkService extends BookmarkFileService
13{
14 public function getBookmarks()
15 {
16 return $this->bookmarks;
17 }
18}
diff --git a/tests/utils/ReferenceHistory.php b/tests/utils/ReferenceHistory.php
index e411c417..516c9f51 100644
--- a/tests/utils/ReferenceHistory.php
+++ b/tests/utils/ReferenceHistory.php
@@ -76,7 +76,7 @@ class ReferenceHistory
76 } 76 }
77 77
78 /** 78 /**
79 * Returns the number of links in the reference data 79 * Returns the number of bookmarks in the reference data
80 */ 80 */
81 public function count() 81 public function count()
82 { 82 {
diff --git a/tests/utils/ReferenceLinkDB.php b/tests/utils/ReferenceLinkDB.php
index c12bcb67..0095f5a1 100644
--- a/tests/utils/ReferenceLinkDB.php
+++ b/tests/utils/ReferenceLinkDB.php
@@ -1,30 +1,39 @@
1<?php 1<?php
2 2
3use Shaarli\Bookmark\LinkDB; 3use Shaarli\Bookmark\Bookmark;
4use Shaarli\Bookmark\BookmarkArray;
4 5
5/** 6/**
6 * Populates a reference datastore to test LinkDB 7 * Populates a reference datastore to test Bookmark
7 */ 8 */
8class ReferenceLinkDB 9class ReferenceLinkDB
9{ 10{
10 public static $NB_LINKS_TOTAL = 11; 11 public static $NB_LINKS_TOTAL = 11;
11 12
12 private $_links = array(); 13 private $bookmarks = array();
13 private $_publicCount = 0; 14 private $_publicCount = 0;
14 private $_privateCount = 0; 15 private $_privateCount = 0;
15 16
17 private $isLegacy;
18
16 /** 19 /**
17 * Populates the test DB with reference data 20 * Populates the test DB with reference data
21 *
22 * @param bool $isLegacy Use links as array instead of Bookmark object
18 */ 23 */
19 public function __construct() 24 public function __construct($isLegacy = false)
20 { 25 {
26 $this->isLegacy = $isLegacy;
27 if (! $this->isLegacy) {
28 $this->bookmarks = new BookmarkArray();
29 }
21 $this->addLink( 30 $this->addLink(
22 11, 31 11,
23 'Pined older', 32 'Pined older',
24 '?PCRizQ', 33 '?PCRizQ',
25 'This is an older pinned link', 34 'This is an older pinned link',
26 0, 35 0,
27 DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20100309_101010'), 36 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20100309_101010'),
28 '', 37 '',
29 null, 38 null,
30 'PCRizQ', 39 'PCRizQ',
@@ -37,7 +46,7 @@ class ReferenceLinkDB
37 '?0gCTjQ', 46 '?0gCTjQ',
38 'This is a pinned link', 47 'This is a pinned link',
39 0, 48 0,
40 DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20121207_152312'), 49 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20121207_152312'),
41 '', 50 '',
42 null, 51 null,
43 '0gCTjQ', 52 '0gCTjQ',
@@ -50,7 +59,7 @@ class ReferenceLinkDB
50 '?WDWyig', 59 '?WDWyig',
51 'Stallman has a beard and is part of the Free Software Foundation (or not). Seriously, read this. #hashtag', 60 'Stallman has a beard and is part of the Free Software Foundation (or not). Seriously, read this. #hashtag',
52 0, 61 0,
53 DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20150310_114651'), 62 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20150310_114651'),
54 'sTuff', 63 'sTuff',
55 null, 64 null,
56 'WDWyig' 65 'WDWyig'
@@ -60,9 +69,9 @@ class ReferenceLinkDB
60 42, 69 42,
61 'Note: I have a big ID but an old date', 70 'Note: I have a big ID but an old date',
62 '?WDWyig', 71 '?WDWyig',
63 'Used to test links reordering.', 72 'Used to test bookmarks reordering.',
64 0, 73 0,
65 DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20100310_101010'), 74 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20100310_101010'),
66 'ut' 75 'ut'
67 ); 76 );
68 77
@@ -72,7 +81,7 @@ class ReferenceLinkDB
72 'http://www.php-fig.org/psr/psr-2/', 81 'http://www.php-fig.org/psr/psr-2/',
73 'This guide extends and expands on PSR-1, the basic coding standard.', 82 'This guide extends and expands on PSR-1, the basic coding standard.',
74 0, 83 0,
75 DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20121206_152312'), 84 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20121206_152312'),
76 '' 85 ''
77 ); 86 );
78 87
@@ -82,9 +91,9 @@ class ReferenceLinkDB
82 'https://static.fsf.org/nosvn/faif-2.0.pdf', 91 'https://static.fsf.org/nosvn/faif-2.0.pdf',
83 'Richard Stallman and the Free Software Revolution. Read this. #hashtag', 92 'Richard Stallman and the Free Software Revolution. Read this. #hashtag',
84 0, 93 0,
85 DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20150310_114633'), 94 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20150310_114633'),
86 'free gnu software stallman -exclude stuff hashtag', 95 'free gnu software stallman -exclude stuff hashtag',
87 DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160803_093033') 96 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20160803_093033')
88 ); 97 );
89 98
90 $this->addLink( 99 $this->addLink(
@@ -93,9 +102,9 @@ class ReferenceLinkDB
93 'http://mediagoblin.org/', 102 'http://mediagoblin.org/',
94 'A free software media publishing platform #hashtagOther', 103 'A free software media publishing platform #hashtagOther',
95 0, 104 0,
96 DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20130614_184135'), 105 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20130614_184135'),
97 'gnu media web .hidden hashtag', 106 'gnu media web .hidden hashtag',
98 DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20130615_184230'), 107 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20130615_184230'),
99 'IuWvgA' 108 'IuWvgA'
100 ); 109 );
101 110
@@ -105,7 +114,7 @@ class ReferenceLinkDB
105 'https://dvcs.w3.org/hg/markup-validator/summary', 114 'https://dvcs.w3.org/hg/markup-validator/summary',
106 'Mercurial repository for the W3C Validator #private', 115 'Mercurial repository for the W3C Validator #private',
107 1, 116 1,
108 DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20141125_084734'), 117 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20141125_084734'),
109 'css html w3c web Mercurial' 118 'css html w3c web Mercurial'
110 ); 119 );
111 120
@@ -115,7 +124,7 @@ class ReferenceLinkDB
115 'http://ars.userfriendly.org/cartoons/?id=20121206', 124 'http://ars.userfriendly.org/cartoons/?id=20121206',
116 'Naming conventions... #private', 125 'Naming conventions... #private',
117 0, 126 0,
118 DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20121206_142300'), 127 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20121206_142300'),
119 'dev cartoon web' 128 'dev cartoon web'
120 ); 129 );
121 130
@@ -125,7 +134,7 @@ class ReferenceLinkDB
125 'http://ars.userfriendly.org/cartoons/?id=20010306', 134 'http://ars.userfriendly.org/cartoons/?id=20010306',
126 'Tropical printing', 135 'Tropical printing',
127 0, 136 0,
128 DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20121206_172539'), 137 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20121206_172539'),
129 'samba cartoon web' 138 'samba cartoon web'
130 ); 139 );
131 140
@@ -135,7 +144,7 @@ class ReferenceLinkDB
135 'http://geek-and-poke.com/', 144 'http://geek-and-poke.com/',
136 '', 145 '',
137 1, 146 1,
138 DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20121206_182539'), 147 DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, '20121206_182539'),
139 'dev cartoon tag1 tag2 tag3 tag4 ' 148 'dev cartoon tag1 tag2 tag3 tag4 '
140 ); 149 );
141 } 150 }
@@ -164,10 +173,15 @@ class ReferenceLinkDB
164 'tags' => $tags, 173 'tags' => $tags,
165 'created' => $date, 174 'created' => $date,
166 'updated' => $updated, 175 'updated' => $updated,
167 'shorturl' => $shorturl ? $shorturl : smallHash($date->format(LinkDB::LINK_DATE_FORMAT) . $id), 176 'shorturl' => $shorturl ? $shorturl : smallHash($date->format(Bookmark::LINK_DATE_FORMAT) . $id),
168 'sticky' => $pinned 177 'sticky' => $pinned
169 ); 178 );
170 $this->_links[$id] = $link; 179 if (! $this->isLegacy) {
180 $bookmark = new Bookmark();
181 $this->bookmarks[$id] = $bookmark->fromArray($link);
182 } else {
183 $this->bookmarks[$id] = $link;
184 }
171 185
172 if ($private) { 186 if ($private) {
173 $this->_privateCount++; 187 $this->_privateCount++;
@@ -184,37 +198,38 @@ class ReferenceLinkDB
184 $this->reorder(); 198 $this->reorder();
185 file_put_contents( 199 file_put_contents(
186 $filename, 200 $filename,
187 '<?php /* '.base64_encode(gzdeflate(serialize($this->_links))).' */ ?>' 201 '<?php /* '.base64_encode(gzdeflate(serialize($this->bookmarks))).' */ ?>'
188 ); 202 );
189 } 203 }
190 204
191 /** 205 /**
192 * Reorder links by creation date (newest first). 206 * Reorder links by creation date (newest first).
193 * 207 *
194 * Also update the urls and ids mapping arrays.
195 *
196 * @param string $order ASC|DESC 208 * @param string $order ASC|DESC
197 */ 209 */
198 public function reorder($order = 'DESC') 210 public function reorder($order = 'DESC')
199 { 211 {
200 // backward compatibility: ignore reorder if the the `created` field doesn't exist 212 if (! $this->isLegacy) {
201 if (! isset(array_values($this->_links)[0]['created'])) { 213 $this->bookmarks->reorder($order);
202 return; 214 } else {
203 } 215 $order = $order === 'ASC' ? -1 : 1;
204 216 // backward compatibility: ignore reorder if the the `created` field doesn't exist
205 $order = $order === 'ASC' ? -1 : 1; 217 if (! isset(array_values($this->bookmarks)[0]['created'])) {
206 // Reorder array by dates. 218 return;
207 usort($this->_links, function ($a, $b) use ($order) {
208 if (isset($a['sticky']) && isset($b['sticky']) && $a['sticky'] !== $b['sticky']) {
209 return $a['sticky'] ? -1 : 1;
210 } 219 }
211 220
212 return $a['created'] < $b['created'] ? 1 * $order : -1 * $order; 221 usort($this->bookmarks, function ($a, $b) use ($order) {
213 }); 222 if (isset($a['sticky']) && isset($b['sticky']) && $a['sticky'] !== $b['sticky']) {
223 return $a['sticky'] ? -1 : 1;
224 }
225
226 return $a['created'] < $b['created'] ? 1 * $order : -1 * $order;
227 });
228 }
214 } 229 }
215 230
216 /** 231 /**
217 * Returns the number of links in the reference data 232 * Returns the number of bookmarks in the reference data
218 */ 233 */
219 public function countLinks() 234 public function countLinks()
220 { 235 {
@@ -222,7 +237,7 @@ class ReferenceLinkDB
222 } 237 }
223 238
224 /** 239 /**
225 * Returns the number of public links in the reference data 240 * Returns the number of public bookmarks in the reference data
226 */ 241 */
227 public function countPublicLinks() 242 public function countPublicLinks()
228 { 243 {
@@ -230,7 +245,7 @@ class ReferenceLinkDB
230 } 245 }
231 246
232 /** 247 /**
233 * Returns the number of private links in the reference data 248 * Returns the number of private bookmarks in the reference data
234 */ 249 */
235 public function countPrivateLinks() 250 public function countPrivateLinks()
236 { 251 {
@@ -238,14 +253,20 @@ class ReferenceLinkDB
238 } 253 }
239 254
240 /** 255 /**
241 * Returns the number of links without tag 256 * Returns the number of bookmarks without tag
242 */ 257 */
243 public function countUntaggedLinks() 258 public function countUntaggedLinks()
244 { 259 {
245 $cpt = 0; 260 $cpt = 0;
246 foreach ($this->_links as $link) { 261 foreach ($this->bookmarks as $link) {
247 if (empty($link['tags'])) { 262 if (! $this->isLegacy) {
248 ++$cpt; 263 if (empty($link->getTags())) {
264 ++$cpt;
265 }
266 } else {
267 if (empty($link['tags'])) {
268 ++$cpt;
269 }
249 } 270 }
250 } 271 }
251 return $cpt; 272 return $cpt;
@@ -254,16 +275,16 @@ class ReferenceLinkDB
254 public function getLinks() 275 public function getLinks()
255 { 276 {
256 $this->reorder(); 277 $this->reorder();
257 return $this->_links; 278 return $this->bookmarks;
258 } 279 }
259 280
260 /** 281 /**
261 * Setter to override link creation. 282 * Setter to override link creation.
262 * 283 *
263 * @param array $links List of links. 284 * @param array $links List of bookmarks.
264 */ 285 */
265 public function setLinks($links) 286 public function setLinks($links)
266 { 287 {
267 $this->_links = $links; 288 $this->bookmarks = $links;
268 } 289 }
269} 290}
diff --git a/tests/utils/config/configJson.json.php b/tests/utils/config/configJson.json.php
index 1549ddfc..b04dc303 100644
--- a/tests/utils/config/configJson.json.php
+++ b/tests/utils/config/configJson.json.php
@@ -41,12 +41,12 @@
41 "foo": "bar" 41 "foo": "bar"
42 }, 42 },
43 "resource": { 43 "resource": {
44 "datastore": "tests\/utils\/config\/datastore.php", 44 "datastore": "sandbox/datastore.php",
45 "data_dir": "sandbox\/", 45 "data_dir": "sandbox\/",
46 "raintpl_tpl": "tpl\/", 46 "raintpl_tpl": "tpl\/",
47 "config": "data\/config.php", 47 "config": "data\/config.php",
48 "ban_file": "data\/ipbans.php", 48 "ban_file": "data\/ipbans.php",
49 "updates": "data\/updates.txt", 49 "updates": "sandbox/updates.txt",
50 "log": "data\/log.txt", 50 "log": "data\/log.txt",
51 "update_check": "data\/lastupdatecheck.txt", 51 "update_check": "data\/lastupdatecheck.txt",
52 "history": "data\/history.php", 52 "history": "data\/history.php",
@@ -59,7 +59,7 @@
59 "WALLABAG_VERSION": 1 59 "WALLABAG_VERSION": 1
60 }, 60 },
61 "dev": { 61 "dev": {
62 "debug": true 62 "debug": false
63 }, 63 },
64 "updates": { 64 "updates": {
65 "check_updates": false, 65 "check_updates": false,
diff --git a/tpl/default/configure.html b/tpl/default/configure.html
index c1a6a6bc..8b75900d 100644
--- a/tpl/default/configure.html
+++ b/tpl/default/configure.html
@@ -68,6 +68,28 @@
68 </select> 68 </select>
69 </div> 69 </div>
70 </div> 70 </div>
71 <div class="pure-u-lg-{$ratioLabel} pure-u-1">
72 <div class="form-label">
73 <label for="formatter">
74 <span class="label-name">{'Description formatter'|t}</span>
75 </label>
76 </div>
77 </div>
78 <div class="pure-u-lg-{$ratioInput} pure-u-1">
79 <div class="form-input">
80 <select name="formatter" id="formatter" class="align">
81 {loop="$formatter_available"}
82 <option value="{$value}"
83 {if="$value===$formatter"}
84 selected="selected"
85 {/if}
86 >
87 {$value|ucfirst}
88 </option>
89 {/loop}
90 </select>
91 </div>
92 </div>
71 </div> 93 </div>
72 <div class="pure-g"> 94 <div class="pure-g">
73 <div class="pure-u-lg-{$ratioLabel} pure-u-1"> 95 <div class="pure-u-lg-{$ratioLabel} pure-u-1">
diff --git a/tpl/default/editlink.html b/tpl/default/editlink.html
index df14535d..d16059a3 100644
--- a/tpl/default/editlink.html
+++ b/tpl/default/editlink.html
@@ -11,7 +11,6 @@
11 <h2 class="window-title"> 11 <h2 class="window-title">
12 {if="!$link_is_new"}{'Edit Shaare'|t}{else}{'New Shaare'|t}{/if} 12 {if="!$link_is_new"}{'Edit Shaare'|t}{else}{'New Shaare'|t}{/if}
13 </h2> 13 </h2>
14 <input type="hidden" name="lf_linkdate" value="{$link.linkdate}">
15 {if="isset($link.id)"} 14 {if="isset($link.id)"}
16 <input type="hidden" name="lf_id" value="{$link.id}"> 15 <input type="hidden" name="lf_id" value="{$link.id}">
17 {/if} 16 {/if}
@@ -20,7 +19,7 @@
20 <label for="lf_url">{'URL'|t}</label> 19 <label for="lf_url">{'URL'|t}</label>
21 </div> 20 </div>
22 <div> 21 <div>
23 <input type="text" name="lf_url" id="lf_url" value="{$link.url}" class="lf_input autofocus"> 22 <input type="text" name="lf_url" id="lf_url" value="{$link.url}" class="lf_input">
24 </div> 23 </div>
25 <div> 24 <div>
26 <label for="lf_title">{'Title'|t}</label> 25 <label for="lf_title">{'Title'|t}</label>
@@ -50,6 +49,15 @@
50 &nbsp;<label for="lf_private">{'Private'|t}</label> 49 &nbsp;<label for="lf_private">{'Private'|t}</label>
51 </div> 50 </div>
52 51
52 {if="$formatter==='markdown'"}
53 <div class="md_help">
54 {'Description will be rendered with'|t}
55 <a href="http://daringfireball.net/projects/markdown/syntax" title="{'Markdown syntax documentation'|t}">
56 {'Markdown syntax'|t}
57 </a>.
58 </div>
59 {/if}
60
53 <div id="editlink-plugins"> 61 <div id="editlink-plugins">
54 {loop="$edit_link_plugin"} 62 {loop="$edit_link_plugin"}
55 {$value} 63 {$value}
diff --git a/tpl/default/includes.html b/tpl/default/includes.html
index 428b8ee2..3820a4f7 100644
--- a/tpl/default/includes.html
+++ b/tpl/default/includes.html
@@ -8,6 +8,9 @@
8<link href="img/favicon.png" rel="shortcut icon" type="image/png" /> 8<link href="img/favicon.png" rel="shortcut icon" type="image/png" />
9<link href="img/apple-touch-icon.png" rel="apple-touch-icon" sizes="180x180" /> 9<link href="img/apple-touch-icon.png" rel="apple-touch-icon" sizes="180x180" />
10<link type="text/css" rel="stylesheet" href="css/shaarli.min.css?v={$version_hash}" /> 10<link type="text/css" rel="stylesheet" href="css/shaarli.min.css?v={$version_hash}" />
11{if="$formatter==='markdown'"}
12 <link type="text/css" rel="stylesheet" href="css/markdown.min.css?v={$version_hash}" />
13{/if}
11{loop="$plugins_includes.css_files"} 14{loop="$plugins_includes.css_files"}
12 <link type="text/css" rel="stylesheet" href="{$value}?v={$version_hash}#"/> 15 <link type="text/css" rel="stylesheet" href="{$value}?v={$version_hash}#"/>
13{/loop} 16{/loop}
diff --git a/tpl/vintage/configure.html b/tpl/vintage/configure.html
index 160286a5..53b0cad2 100644
--- a/tpl/vintage/configure.html
+++ b/tpl/vintage/configure.html
@@ -33,6 +33,19 @@
33 </tr> 33 </tr>
34 34
35 <tr> 35 <tr>
36 <td><b>Description formatter:</b></td>
37 <td>
38 <select name="formatter" id="formatter">
39 {loop="$formatter_available"}
40 <option value="{$value}" {if="$value===$formatter"}selected{/if}>
41 {$value|ucfirst}
42 </option>
43 {/loop}
44 </select>
45 </td>
46 </tr>
47
48 <tr>
36 <td><b>Timezone:</b></td> 49 <td><b>Timezone:</b></td>
37 <td> 50 <td>
38 <select id="continent" name="continent"> 51 <select id="continent" name="continent">
diff --git a/tpl/vintage/editlink.html b/tpl/vintage/editlink.html
index 5fa7d194..6f7a330f 100644
--- a/tpl/vintage/editlink.html
+++ b/tpl/vintage/editlink.html
@@ -26,7 +26,16 @@
26 <input type="text" name="lf_tags" id="lf_tags" value="{$link.tags}" class="lf_input" 26 <input type="text" name="lf_tags" id="lf_tags" value="{$link.tags}" class="lf_input"
27 data-list="{loop="$tags"}{$key}, {/loop}" data-multiple autocomplete="off" ><br> 27 data-list="{loop="$tags"}{$key}, {/loop}" data-multiple autocomplete="off" ><br>
28 28
29 {loop="$edit_link_plugin"} 29 {if="$formatter==='markdown'"}
30 <div class="md_help">
31 {'Description will be rendered with'|t}
32 <a href="http://daringfireball.net/projects/markdown/syntax" title="{'Markdown syntax documentation'|t}">
33 {'Markdown syntax'|t}
34 </a>.
35 </div>
36 {/if}
37
38 {loop="$edit_link_plugin"}
30 {$value} 39 {$value}
31 {/loop} 40 {/loop}
32 41
@@ -38,7 +47,6 @@
38 &nbsp;<label for="lf_private"><i>Private</i></label><br><br> 47 &nbsp;<label for="lf_private"><i>Private</i></label><br><br>
39 {/if} 48 {/if}
40 <input type="submit" value="Save" name="save_edit" class="bigbutton"> 49 <input type="submit" value="Save" name="save_edit" class="bigbutton">
41 <input type="submit" value="Cancel" name="cancel_edit" class="bigbutton">
42 {if="!$link_is_new && isset($link.id)"} 50 {if="!$link_is_new && isset($link.id)"}
43 <a href="?delete_link&amp;lf_linkdate={$link.id}&amp;token={$token}" 51 <a href="?delete_link&amp;lf_linkdate={$link.id}&amp;token={$token}"
44 name="delete_link" class="bigbutton" 52 name="delete_link" class="bigbutton"
diff --git a/tpl/vintage/includes.html b/tpl/vintage/includes.html
index 1c4ff79c..8d273c44 100644
--- a/tpl/vintage/includes.html
+++ b/tpl/vintage/includes.html
@@ -7,6 +7,9 @@
7<link rel="alternate" type="application/atom+xml" href="{$feedurl}?do=atom{$searchcrits}#" title="ATOM Feed" /> 7<link rel="alternate" type="application/atom+xml" href="{$feedurl}?do=atom{$searchcrits}#" title="ATOM Feed" />
8<link href="img/favicon.ico" rel="shortcut icon" type="image/x-icon" /> 8<link href="img/favicon.ico" rel="shortcut icon" type="image/x-icon" />
9<link type="text/css" rel="stylesheet" href="css/shaarli.min.css" /> 9<link type="text/css" rel="stylesheet" href="css/shaarli.min.css" />
10{if="$formatter==='markdown'"}
11 <link type="text/css" rel="stylesheet" href="css/markdown.min.css?v={$version_hash}" />
12{/if}
10{loop="$plugins_includes.css_files"} 13{loop="$plugins_includes.css_files"}
11<link type="text/css" rel="stylesheet" href="{$value}#"/> 14<link type="text/css" rel="stylesheet" href="{$value}#"/>
12{/loop} 15{/loop}
diff --git a/webpack.config.js b/webpack.config.js
index ed548c73..602147e5 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -30,6 +30,7 @@ module.exports = [
30 './assets/default/js/base.js', 30 './assets/default/js/base.js',
31 './assets/default/scss/shaarli.scss', 31 './assets/default/scss/shaarli.scss',
32 ].concat(glob.sync('./assets/default/img/*')), 32 ].concat(glob.sync('./assets/default/img/*')),
33 markdown: './assets/common/css/markdown.css',
33 }, 34 },
34 output: { 35 output: {
35 filename: '[name].min.js', 36 filename: '[name].min.js',
@@ -50,7 +51,7 @@ module.exports = [
50 } 51 }
51 }, 52 },
52 { 53 {
53 test: /\.scss/, 54 test: /\.s?css/,
54 use: extractCssDefault.extract({ 55 use: extractCssDefault.extract({
55 use: [{ 56 use: [{
56 loader: "css-loader", 57 loader: "css-loader",
@@ -97,6 +98,7 @@ module.exports = [
97 './assets/vintage/css/reset.css', 98 './assets/vintage/css/reset.css',
98 './assets/vintage/css/shaarli.css', 99 './assets/vintage/css/shaarli.css',
99 ].concat(glob.sync('./assets/vintage/img/*')), 100 ].concat(glob.sync('./assets/vintage/img/*')),
101 markdown: './assets/common/css/markdown.css',
100 thumbnails: './assets/common/js/thumbnails.js', 102 thumbnails: './assets/common/js/thumbnails.js',
101 thumbnails_update: './assets/common/js/thumbnails-update.js', 103 thumbnails_update: './assets/common/js/thumbnails-update.js',
102 }, 104 },