diff options
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 | ||
4 | use DateTime; | 4 | use DateTime; |
5 | use Exception; | 5 | use Exception; |
6 | use 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 | ||
4 | use Shaarli\Api\Exceptions\ApiAuthorizationException; | 4 | use Shaarli\Api\Exceptions\ApiAuthorizationException; |
5 | use Shaarli\Api\Exceptions\ApiException; | 5 | use Shaarli\Api\Exceptions\ApiException; |
6 | use Shaarli\Bookmark\BookmarkFileService; | ||
6 | use Shaarli\Config\ConfigManager; | 7 | use Shaarli\Config\ConfigManager; |
7 | use Slim\Container; | 8 | use Slim\Container; |
8 | use Slim\Http\Request; | 9 | use 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 @@ | |||
2 | namespace Shaarli\Api; | 2 | namespace Shaarli\Api; |
3 | 3 | ||
4 | use Shaarli\Api\Exceptions\ApiAuthorizationException; | 4 | use Shaarli\Api\Exceptions\ApiAuthorizationException; |
5 | use Shaarli\Bookmark\Bookmark; | ||
5 | use Shaarli\Http\Base64Url; | 6 | use 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 | ||
3 | namespace Shaarli\Api\Controllers; | 3 | namespace Shaarli\Api\Controllers; |
4 | 4 | ||
5 | use Shaarli\Bookmark\LinkDB; | 5 | use Shaarli\Bookmark\BookmarkServiceInterface; |
6 | use Shaarli\Config\ConfigManager; | 6 | use Shaarli\Config\ConfigManager; |
7 | use Slim\Container; | 7 | use 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 | ||
3 | namespace Shaarli\Api\Controllers; | 3 | namespace Shaarli\Api\Controllers; |
4 | 4 | ||
5 | use Shaarli\Bookmark\BookmarkFilter; | ||
5 | use Slim\Http\Request; | 6 | use Slim\Http\Request; |
6 | use Slim\Http\Response; | 7 | use 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; | |||
19 | class Links extends ApiController | 19 | class 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; | |||
5 | use Shaarli\Api\ApiUtils; | 5 | use Shaarli\Api\ApiUtils; |
6 | use Shaarli\Api\Exceptions\ApiBadParametersException; | 6 | use Shaarli\Api\Exceptions\ApiBadParametersException; |
7 | use Shaarli\Api\Exceptions\ApiTagNotFoundException; | 7 | use Shaarli\Api\Exceptions\ApiTagNotFoundException; |
8 | use Shaarli\Bookmark\BookmarkFilter; | ||
8 | use Slim\Http\Request; | 9 | use Slim\Http\Request; |
9 | use Slim\Http\Response; | 10 | use Slim\Http\Response; |
10 | 11 | ||
@@ -18,7 +19,7 @@ use Slim\Http\Response; | |||
18 | class Tags extends ApiController | 19 | class 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 @@ | |||
2 | namespace Shaarli\Feed; | 2 | namespace Shaarli\Feed; |
3 | 3 | ||
4 | use DateTime; | 4 | use DateTime; |
5 | use Shaarli\Bookmark\Bookmark; | ||
6 | use Shaarli\Bookmark\BookmarkServiceInterface; | ||
7 | use 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>— ' . $permalink; |
160 | $link['description'] .= PHP_EOL . PHP_EOL . '<br>— ' . $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; | |||
7 | use Exception; | 7 | use Exception; |
8 | use Katzgrau\KLogger\Logger; | 8 | use Katzgrau\KLogger\Logger; |
9 | use Psr\Log\LogLevel; | 9 | use Psr\Log\LogLevel; |
10 | use Shaarli\Bookmark\LinkDB; | 10 | use Shaarli\Bookmark\Bookmark; |
11 | use Shaarli\Bookmark\BookmarkServiceInterface; | ||
11 | use Shaarli\Config\ConfigManager; | 12 | use Shaarli\Config\ConfigManager; |
13 | use Shaarli\Formatter\BookmarkFormatter; | ||
12 | use Shaarli\History; | 14 | use Shaarli\History; |
13 | use Shaarli\NetscapeBookmarkParser\NetscapeBookmarkParser; | 15 | use 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; | |||
5 | use Exception; | 5 | use Exception; |
6 | use RainTPL; | 6 | use RainTPL; |
7 | use Shaarli\ApplicationUtils; | 7 | use Shaarli\ApplicationUtils; |
8 | use Shaarli\Bookmark\LinkDB; | 8 | use Shaarli\Bookmark\BookmarkServiceInterface; |
9 | use Shaarli\Config\ConfigManager; | 9 | use Shaarli\Config\ConfigManager; |
10 | use Shaarli\Thumbnailer; | 10 | use 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 | ||
3 | namespace Shaarli\Updater; | 3 | namespace Shaarli\Updater; |
4 | 4 | ||
5 | use Exception; | ||
6 | use RainTPL; | ||
7 | use ReflectionClass; | ||
8 | use ReflectionException; | ||
9 | use ReflectionMethod; | ||
10 | use Shaarli\ApplicationUtils; | ||
11 | use Shaarli\Bookmark\LinkDB; | ||
12 | use Shaarli\Bookmark\LinkFilter; | ||
13 | use Shaarli\Config\ConfigJson; | ||
14 | use Shaarli\Config\ConfigManager; | 5 | use Shaarli\Config\ConfigManager; |
15 | use Shaarli\Config\ConfigPhp; | 6 | use Shaarli\Bookmark\BookmarkServiceInterface; |
16 | use Shaarli\Exceptions\IOException; | ||
17 | use Shaarli\Thumbnailer; | ||
18 | use Shaarli\Updater\Exception\UpdaterException; | 7 | use 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 | */ |
25 | class Updater | 14 | class 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 | /** | 3 | namespace Shaarli\Updater; |
4 | * Read the updates file, and return already done updates. | 4 | |
5 | * | 5 | class UpdaterUtils |
6 | * @param string $updatesFilepath Updates file path. | ||
7 | * | ||
8 | * @return array Already done update methods. | ||
9 | */ | ||
10 | function 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 | */ |
29 | function 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 | |||
17 | php -r 'print(json_encode(unserialize(gzinflate(base64_decode(preg_replace("!.*/\* (.+) \*/.*!", "$1", file_get_contents("data/datastore.php")))))));' | jq . | 17 | php -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) |
@@ -35,9 +35,6 @@ ini_set('upload_max_filesize', '16M'); | |||
35 | 35 | ||
36 | // See all error except warnings | 36 | // See all error except warnings |
37 | error_reporting(E_ALL^E_WARNING); | 37 | error_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 |
43 | if (! file_exists(__DIR__ . '/vendor/autoload.php')) { | 40 | if (! file_exists(__DIR__ . '/vendor/autoload.php')) { |
@@ -65,11 +62,15 @@ require_once 'application/TimeZone.php'; | |||
65 | require_once 'application/Utils.php'; | 62 | require_once 'application/Utils.php'; |
66 | 63 | ||
67 | use \Shaarli\ApplicationUtils; | 64 | use \Shaarli\ApplicationUtils; |
68 | use \Shaarli\Bookmark\Exception\LinkNotFoundException; | 65 | use Shaarli\Bookmark\BookmarkServiceInterface; |
69 | use \Shaarli\Bookmark\LinkDB; | 66 | use \Shaarli\Bookmark\Exception\BookmarkNotFoundException; |
67 | use Shaarli\Bookmark\Bookmark; | ||
68 | use Shaarli\Bookmark\BookmarkFilter; | ||
69 | use Shaarli\Bookmark\BookmarkFileService; | ||
70 | use \Shaarli\Config\ConfigManager; | 70 | use \Shaarli\Config\ConfigManager; |
71 | use \Shaarli\Feed\CachedPage; | 71 | use \Shaarli\Feed\CachedPage; |
72 | use \Shaarli\Feed\FeedBuilder; | 72 | use \Shaarli\Feed\FeedBuilder; |
73 | use Shaarli\Formatter\FormatterFactory; | ||
73 | use \Shaarli\History; | 74 | use \Shaarli\History; |
74 | use \Shaarli\Languages; | 75 | use \Shaarli\Languages; |
75 | use \Shaarli\Netscape\NetscapeBookmarkUtils; | 76 | use \Shaarli\Netscape\NetscapeBookmarkUtils; |
@@ -81,6 +82,7 @@ use \Shaarli\Security\LoginManager; | |||
81 | use \Shaarli\Security\SessionManager; | 82 | use \Shaarli\Security\SessionManager; |
82 | use \Shaarli\Thumbnailer; | 83 | use \Shaarli\Thumbnailer; |
83 | use \Shaarli\Updater\Updater; | 84 | use \Shaarli\Updater\Updater; |
85 | use \Shaarli\Updater\UpdaterUtils; | ||
84 | 86 | ||
85 | // Ensure the PHP version is supported | 87 | // Ensure the PHP version is supported |
86 | try { | 88 | try { |
@@ -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 | ||
129 | if ($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'])) { | |||
140 | new Languages(setlocale(LC_MESSAGES, 0), $conf); | 153 | new 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))); |
144 | RainTPL::$tpl_dir = $conf->get('resource.raintpl_tpl').'/'.$conf->get('resource.theme').'/'; // template directory | 157 | RainTPL::$tpl_dir = $conf->get('resource.raintpl_tpl').'/'.$conf->get('resource.theme').'/'; // template directory |
145 | RainTPL::$cache_dir = $conf->get('resource.raintpl_tmp'); // cache directory | 158 | RainTPL::$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 | */ |
293 | function showDailyRSS($conf, $loginManager) | 307 | function 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 | */ |
391 | function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager, $loginManager) | 405 | function 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 | */ |
495 | function showLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager) | 510 | function 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 | */ |
511 | function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager, $loginManager) | 526 | function 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 | */ |
1651 | function buildLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager) | 1656 | function 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 | ||
1900 | if (isset($_SERVER['QUERY_STRING']) && startsWith($_SERVER['QUERY_STRING'], 'do=dailyrss')) { | ||
1901 | showDailyRSS($conf, $loginManager); | ||
1902 | exit; | ||
1903 | } | ||
1904 | |||
1905 | if (!isset($_SESSION['LINKS_PER_PAGE'])) { | 1905 | if (!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(), | 1917 | if (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 | |||
3 | Convert all your shaares description to HTML formatted Markdown. | ||
4 | |||
5 | [Read more about Markdown syntax](http://daringfireball.net/projects/markdown/syntax). | ||
6 | |||
7 | Markdown processing is done with [Parsedown library](https://github.com/erusev/parsedown). | ||
8 | |||
9 | ### Installation | ||
10 | |||
11 | As a default plugin, it should already be in `tpl/plugins/` directory. | ||
12 | If not, download and unpack it there. | ||
13 | |||
14 | The 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 | |||
26 | To enable the plugin, just check it in the plugin administration page. | ||
27 | |||
28 | You can also add `markdown` to your list of enabled plugins in `data/config.json.php` | ||
29 | (`general.enabled_plugins` list). | ||
30 | |||
31 | This should look like: | ||
32 | |||
33 | ``` | ||
34 | "general": { | ||
35 | "enabled_plugins": [ | ||
36 | "markdown", | ||
37 | [...] | ||
38 | ], | ||
39 | } | ||
40 | ``` | ||
41 | |||
42 | Parsedown parsing library is imported using Composer. If you installed Shaarli using `git`, | ||
43 | or the `master` branch, run | ||
44 | |||
45 | composer update --no-dev --prefer-dist | ||
46 | |||
47 | ### No Markdown tag | ||
48 | |||
49 | If 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 | |||
55 | By default, HTML tags are escaped. You can enable HTML tags rendering | ||
56 | by 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 | |||
66 | With this setting, Markdown support HTML tags. For example: | ||
67 | |||
68 | > <strong>strong</strong><strike>strike</strike> | ||
69 | |||
70 | Will 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, | ||
81 | enabling it might break your page. | ||
82 | |||
83 | ### Known issue | ||
84 | |||
85 | #### Redirector | ||
86 | |||
87 | If you're using a redirector, you *need* to add a space after a link, | ||
88 | otherwise the rest of the line will be `urlencode`. | ||
89 | |||
90 | ``` | ||
91 | [link](http://domain.tld)-->test | ||
92 | ``` | ||
93 | |||
94 | Will consider `http://domain.tld)-->test` as URL. | ||
95 | |||
96 | Instead, 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 @@ | |||
1 | description="Render shaare description with Markdown syntax.<br><strong>Warning</strong>: | ||
2 | If your shaared descriptions contained HTML tags before enabling the markdown plugin, | ||
3 | enabling it might break your page. | ||
4 | See 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 | |||
9 | use Shaarli\Config\ConfigManager; | ||
10 | use Shaarli\Plugin\PluginManager; | ||
11 | use Shaarli\Router; | ||
12 | |||
13 | /* | ||
14 | * If this tag is used on a shaare, the description won't be processed by Parsedown. | ||
15 | */ | ||
16 | define('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 | */ | ||
26 | function 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 | */ | ||
51 | function 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 | */ | ||
77 | function 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 | */ | ||
103 | function 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 | */ | ||
115 | function 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 | */ | ||
138 | function 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 | */ | ||
158 | function 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 | */ | ||
185 | function 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 | */ | ||
243 | function reverse_nl2br($description) | ||
244 | { | ||
245 | return preg_replace('!<br */?>!im', '', $description); | ||
246 | } | ||
247 | |||
248 | /** | ||
249 | * Remove HTML spaces ' ' auto generated by Shaarli core system. | ||
250 | * | ||
251 | * @param string $description input description text. | ||
252 | * | ||
253 | * @return string $description without HTML links. | ||
254 | */ | ||
255 | function reverse_space2nbsp($description) | ||
256 | { | ||
257 | return preg_replace('/(^| ) /m', '$1 ', $description); | ||
258 | } | ||
259 | |||
260 | function reverse_feed_permalink($description) | ||
261 | { | ||
262 | return preg_replace('@— <a href="([^"]+)" title="[^"]+">([^<]+)</a>$@im', '— [$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 | */ | ||
273 | function 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 | */ | ||
292 | function 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 | */ | ||
332 | function 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 | */ | ||
358 | function markdown_dummy_translation() | ||
359 | { | ||
360 | // meta | ||
361 | t('Render shaare description with Markdown syntax.<br><strong>Warning</strong>: | ||
362 | If your shaared descriptions contained HTML tags before enabling the markdown plugin, | ||
363 | enabling it might break your page. | ||
364 | See 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 | <label for="lf_private">{'Private'|t}</label> | 49 | <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 | <label for="lf_private"><i>Private</i></label><br><br> | 47 | <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&lf_linkdate={$link.id}&token={$token}" | 51 | <a href="?delete_link&lf_linkdate={$link.id}&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 | }, |