aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-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/BookmarkArray.php2
-rw-r--r--application/config/ConfigManager.php2
-rw-r--r--application/feed/FeedBuilder.php78
-rw-r--r--application/formatter/BookmarkMarkdownFormatter.php6
-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--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--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
31 files changed, 631 insertions, 1502 deletions
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/BookmarkArray.php b/application/bookmark/BookmarkArray.php
index b427c91a..d87d43b4 100644
--- a/application/bookmark/BookmarkArray.php
+++ b/application/bookmark/BookmarkArray.php
@@ -118,7 +118,7 @@ class BookmarkArray implements \Iterator, \Countable, \ArrayAccess
118 $realOffset = $this->getBookmarkOffset($offset); 118 $realOffset = $this->getBookmarkOffset($offset);
119 $url = $this->bookmarks[$realOffset]->getUrl(); 119 $url = $this->bookmarks[$realOffset]->getUrl();
120 unset($this->urls[$url]); 120 unset($this->urls[$url]);
121 unset($this->ids[$realOffset]); 121 unset($this->ids[$offset]);
122 unset($this->bookmarks[$realOffset]); 122 unset($this->bookmarks[$realOffset]);
123 } 123 }
124 124
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/BookmarkMarkdownFormatter.php b/application/formatter/BookmarkMarkdownFormatter.php
index f60c61f4..7797bfbf 100644
--- a/application/formatter/BookmarkMarkdownFormatter.php
+++ b/application/formatter/BookmarkMarkdownFormatter.php
@@ -57,6 +57,7 @@ class BookmarkMarkdownFormatter extends BookmarkDefaultFormatter
57 $processedDescription = $bookmark->getDescription(); 57 $processedDescription = $bookmark->getDescription();
58 $processedDescription = $this->filterProtocols($processedDescription); 58 $processedDescription = $this->filterProtocols($processedDescription);
59 $processedDescription = $this->formatHashTags($processedDescription); 59 $processedDescription = $this->formatHashTags($processedDescription);
60 $processedDescription = $this->reverseEscapedHtml($processedDescription);
60 $processedDescription = $this->parsedown 61 $processedDescription = $this->parsedown
61 ->setMarkupEscaped($this->escape) 62 ->setMarkupEscaped($this->escape)
62 ->setBreaksEnabled(true) 63 ->setBreaksEnabled(true)
@@ -195,4 +196,9 @@ class BookmarkMarkdownFormatter extends BookmarkDefaultFormatter
195 ); 196 );
196 return $description; 197 return $description;
197 } 198 }
199
200 protected function reverseEscapedHtml($description)
201 {
202 return unescape($description);
203 }
198} 204}
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/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..ae74bc7e 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('raw'),
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/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 },