diff options
Diffstat (limited to 'application')
35 files changed, 3512 insertions, 799 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/Bookmark.php b/application/bookmark/Bookmark.php new file mode 100644 index 00000000..f9b21d3d --- /dev/null +++ b/application/bookmark/Bookmark.php | |||
@@ -0,0 +1,461 @@ | |||
1 | <?php | ||
2 | |||
3 | namespace Shaarli\Bookmark; | ||
4 | |||
5 | use DateTime; | ||
6 | use Shaarli\Bookmark\Exception\InvalidBookmarkException; | ||
7 | |||
8 | /** | ||
9 | * Class Bookmark | ||
10 | * | ||
11 | * This class represent a single Bookmark with all its attributes. | ||
12 | * Every bookmark should manipulated using this, before being formatted. | ||
13 | * | ||
14 | * @package Shaarli\Bookmark | ||
15 | */ | ||
16 | class Bookmark | ||
17 | { | ||
18 | /** @var string Date format used in string (former ID format) */ | ||
19 | const LINK_DATE_FORMAT = 'Ymd_His'; | ||
20 | |||
21 | /** @var int Bookmark ID */ | ||
22 | protected $id; | ||
23 | |||
24 | /** @var string Permalink identifier */ | ||
25 | protected $shortUrl; | ||
26 | |||
27 | /** @var string Bookmark's URL - $shortUrl prefixed with `?` for notes */ | ||
28 | protected $url; | ||
29 | |||
30 | /** @var string Bookmark's title */ | ||
31 | protected $title; | ||
32 | |||
33 | /** @var string Raw bookmark's description */ | ||
34 | protected $description; | ||
35 | |||
36 | /** @var array List of bookmark's tags */ | ||
37 | protected $tags; | ||
38 | |||
39 | /** @var string Thumbnail's URL - false if no thumbnail could be found */ | ||
40 | protected $thumbnail; | ||
41 | |||
42 | /** @var bool Set to true if the bookmark is set as sticky */ | ||
43 | protected $sticky; | ||
44 | |||
45 | /** @var DateTime Creation datetime */ | ||
46 | protected $created; | ||
47 | |||
48 | /** @var DateTime Update datetime */ | ||
49 | protected $updated; | ||
50 | |||
51 | /** @var bool True if the bookmark can only be seen while logged in */ | ||
52 | protected $private; | ||
53 | |||
54 | /** | ||
55 | * Initialize a link from array data. Especially useful to create a Bookmark from former link storage format. | ||
56 | * | ||
57 | * @param array $data | ||
58 | * | ||
59 | * @return $this | ||
60 | */ | ||
61 | public function fromArray($data) | ||
62 | { | ||
63 | $this->id = $data['id']; | ||
64 | $this->shortUrl = $data['shorturl']; | ||
65 | $this->url = $data['url']; | ||
66 | $this->title = $data['title']; | ||
67 | $this->description = $data['description']; | ||
68 | $this->thumbnail = isset($data['thumbnail']) ? $data['thumbnail'] : null; | ||
69 | $this->sticky = isset($data['sticky']) ? $data['sticky'] : false; | ||
70 | $this->created = $data['created']; | ||
71 | if (is_array($data['tags'])) { | ||
72 | $this->tags = $data['tags']; | ||
73 | } else { | ||
74 | $this->tags = preg_split('/\s+/', $data['tags'], -1, PREG_SPLIT_NO_EMPTY); | ||
75 | } | ||
76 | if (! empty($data['updated'])) { | ||
77 | $this->updated = $data['updated']; | ||
78 | } | ||
79 | $this->private = $data['private'] ? true : false; | ||
80 | |||
81 | return $this; | ||
82 | } | ||
83 | |||
84 | /** | ||
85 | * Make sure that the current instance of Bookmark is valid and can be saved into the data store. | ||
86 | * A valid link requires: | ||
87 | * - an integer ID | ||
88 | * - a short URL (for permalinks) | ||
89 | * - a creation date | ||
90 | * | ||
91 | * This function also initialize optional empty fields: | ||
92 | * - the URL with the permalink | ||
93 | * - the title with the URL | ||
94 | * | ||
95 | * @throws InvalidBookmarkException | ||
96 | */ | ||
97 | public function validate() | ||
98 | { | ||
99 | if ($this->id === null | ||
100 | || ! is_int($this->id) | ||
101 | || empty($this->shortUrl) | ||
102 | || empty($this->created) | ||
103 | || ! $this->created instanceof DateTime | ||
104 | ) { | ||
105 | throw new InvalidBookmarkException($this); | ||
106 | } | ||
107 | if (empty($this->url)) { | ||
108 | $this->url = '?'. $this->shortUrl; | ||
109 | } | ||
110 | if (empty($this->title)) { | ||
111 | $this->title = $this->url; | ||
112 | } | ||
113 | } | ||
114 | |||
115 | /** | ||
116 | * Set the Id. | ||
117 | * If they're not already initialized, this function also set: | ||
118 | * - created: with the current datetime | ||
119 | * - shortUrl: with a generated small hash from the date and the given ID | ||
120 | * | ||
121 | * @param int $id | ||
122 | * | ||
123 | * @return Bookmark | ||
124 | */ | ||
125 | public function setId($id) | ||
126 | { | ||
127 | $this->id = $id; | ||
128 | if (empty($this->created)) { | ||
129 | $this->created = new DateTime(); | ||
130 | } | ||
131 | if (empty($this->shortUrl)) { | ||
132 | $this->shortUrl = link_small_hash($this->created, $this->id); | ||
133 | } | ||
134 | |||
135 | return $this; | ||
136 | } | ||
137 | |||
138 | /** | ||
139 | * Get the Id. | ||
140 | * | ||
141 | * @return int | ||
142 | */ | ||
143 | public function getId() | ||
144 | { | ||
145 | return $this->id; | ||
146 | } | ||
147 | |||
148 | /** | ||
149 | * Get the ShortUrl. | ||
150 | * | ||
151 | * @return string | ||
152 | */ | ||
153 | public function getShortUrl() | ||
154 | { | ||
155 | return $this->shortUrl; | ||
156 | } | ||
157 | |||
158 | /** | ||
159 | * Get the Url. | ||
160 | * | ||
161 | * @return string | ||
162 | */ | ||
163 | public function getUrl() | ||
164 | { | ||
165 | return $this->url; | ||
166 | } | ||
167 | |||
168 | /** | ||
169 | * Get the Title. | ||
170 | * | ||
171 | * @return string | ||
172 | */ | ||
173 | public function getTitle() | ||
174 | { | ||
175 | return $this->title; | ||
176 | } | ||
177 | |||
178 | /** | ||
179 | * Get the Description. | ||
180 | * | ||
181 | * @return string | ||
182 | */ | ||
183 | public function getDescription() | ||
184 | { | ||
185 | return ! empty($this->description) ? $this->description : ''; | ||
186 | } | ||
187 | |||
188 | /** | ||
189 | * Get the Created. | ||
190 | * | ||
191 | * @return DateTime | ||
192 | */ | ||
193 | public function getCreated() | ||
194 | { | ||
195 | return $this->created; | ||
196 | } | ||
197 | |||
198 | /** | ||
199 | * Get the Updated. | ||
200 | * | ||
201 | * @return DateTime | ||
202 | */ | ||
203 | public function getUpdated() | ||
204 | { | ||
205 | return $this->updated; | ||
206 | } | ||
207 | |||
208 | /** | ||
209 | * Set the ShortUrl. | ||
210 | * | ||
211 | * @param string $shortUrl | ||
212 | * | ||
213 | * @return Bookmark | ||
214 | */ | ||
215 | public function setShortUrl($shortUrl) | ||
216 | { | ||
217 | $this->shortUrl = $shortUrl; | ||
218 | |||
219 | return $this; | ||
220 | } | ||
221 | |||
222 | /** | ||
223 | * Set the Url. | ||
224 | * | ||
225 | * @param string $url | ||
226 | * @param array $allowedProtocols | ||
227 | * | ||
228 | * @return Bookmark | ||
229 | */ | ||
230 | public function setUrl($url, $allowedProtocols = []) | ||
231 | { | ||
232 | $url = trim($url); | ||
233 | if (! empty($url)) { | ||
234 | $url = whitelist_protocols($url, $allowedProtocols); | ||
235 | } | ||
236 | $this->url = $url; | ||
237 | |||
238 | return $this; | ||
239 | } | ||
240 | |||
241 | /** | ||
242 | * Set the Title. | ||
243 | * | ||
244 | * @param string $title | ||
245 | * | ||
246 | * @return Bookmark | ||
247 | */ | ||
248 | public function setTitle($title) | ||
249 | { | ||
250 | $this->title = trim($title); | ||
251 | |||
252 | return $this; | ||
253 | } | ||
254 | |||
255 | /** | ||
256 | * Set the Description. | ||
257 | * | ||
258 | * @param string $description | ||
259 | * | ||
260 | * @return Bookmark | ||
261 | */ | ||
262 | public function setDescription($description) | ||
263 | { | ||
264 | $this->description = $description; | ||
265 | |||
266 | return $this; | ||
267 | } | ||
268 | |||
269 | /** | ||
270 | * Set the Created. | ||
271 | * Note: you shouldn't set this manually except for special cases (like bookmark import) | ||
272 | * | ||
273 | * @param DateTime $created | ||
274 | * | ||
275 | * @return Bookmark | ||
276 | */ | ||
277 | public function setCreated($created) | ||
278 | { | ||
279 | $this->created = $created; | ||
280 | |||
281 | return $this; | ||
282 | } | ||
283 | |||
284 | /** | ||
285 | * Set the Updated. | ||
286 | * | ||
287 | * @param DateTime $updated | ||
288 | * | ||
289 | * @return Bookmark | ||
290 | */ | ||
291 | public function setUpdated($updated) | ||
292 | { | ||
293 | $this->updated = $updated; | ||
294 | |||
295 | return $this; | ||
296 | } | ||
297 | |||
298 | /** | ||
299 | * Get the Private. | ||
300 | * | ||
301 | * @return bool | ||
302 | */ | ||
303 | public function isPrivate() | ||
304 | { | ||
305 | return $this->private ? true : false; | ||
306 | } | ||
307 | |||
308 | /** | ||
309 | * Set the Private. | ||
310 | * | ||
311 | * @param bool $private | ||
312 | * | ||
313 | * @return Bookmark | ||
314 | */ | ||
315 | public function setPrivate($private) | ||
316 | { | ||
317 | $this->private = $private ? true : false; | ||
318 | |||
319 | return $this; | ||
320 | } | ||
321 | |||
322 | /** | ||
323 | * Get the Tags. | ||
324 | * | ||
325 | * @return array | ||
326 | */ | ||
327 | public function getTags() | ||
328 | { | ||
329 | return is_array($this->tags) ? $this->tags : []; | ||
330 | } | ||
331 | |||
332 | /** | ||
333 | * Set the Tags. | ||
334 | * | ||
335 | * @param array $tags | ||
336 | * | ||
337 | * @return Bookmark | ||
338 | */ | ||
339 | public function setTags($tags) | ||
340 | { | ||
341 | $this->setTagsString(implode(' ', $tags)); | ||
342 | |||
343 | return $this; | ||
344 | } | ||
345 | |||
346 | /** | ||
347 | * Get the Thumbnail. | ||
348 | * | ||
349 | * @return string|bool | ||
350 | */ | ||
351 | public function getThumbnail() | ||
352 | { | ||
353 | return !$this->isNote() ? $this->thumbnail : false; | ||
354 | } | ||
355 | |||
356 | /** | ||
357 | * Set the Thumbnail. | ||
358 | * | ||
359 | * @param string|bool $thumbnail | ||
360 | * | ||
361 | * @return Bookmark | ||
362 | */ | ||
363 | public function setThumbnail($thumbnail) | ||
364 | { | ||
365 | $this->thumbnail = $thumbnail; | ||
366 | |||
367 | return $this; | ||
368 | } | ||
369 | |||
370 | /** | ||
371 | * Get the Sticky. | ||
372 | * | ||
373 | * @return bool | ||
374 | */ | ||
375 | public function isSticky() | ||
376 | { | ||
377 | return $this->sticky ? true : false; | ||
378 | } | ||
379 | |||
380 | /** | ||
381 | * Set the Sticky. | ||
382 | * | ||
383 | * @param bool $sticky | ||
384 | * | ||
385 | * @return Bookmark | ||
386 | */ | ||
387 | public function setSticky($sticky) | ||
388 | { | ||
389 | $this->sticky = $sticky ? true : false; | ||
390 | |||
391 | return $this; | ||
392 | } | ||
393 | |||
394 | /** | ||
395 | * @return string Bookmark's tags as a string, separated by a space | ||
396 | */ | ||
397 | public function getTagsString() | ||
398 | { | ||
399 | return implode(' ', $this->getTags()); | ||
400 | } | ||
401 | |||
402 | /** | ||
403 | * @return bool | ||
404 | */ | ||
405 | public function isNote() | ||
406 | { | ||
407 | // We check empty value to get a valid result if the link has not been saved yet | ||
408 | return empty($this->url) || $this->url[0] === '?'; | ||
409 | } | ||
410 | |||
411 | /** | ||
412 | * Set tags from a string. | ||
413 | * Note: | ||
414 | * - tags must be separated whether by a space or a comma | ||
415 | * - multiple spaces will be removed | ||
416 | * - trailing dash in tags will be removed | ||
417 | * | ||
418 | * @param string $tags | ||
419 | * | ||
420 | * @return $this | ||
421 | */ | ||
422 | public function setTagsString($tags) | ||
423 | { | ||
424 | // Remove first '-' char in tags. | ||
425 | $tags = preg_replace('/(^| )\-/', '$1', $tags); | ||
426 | // Explode all tags separted by spaces or commas | ||
427 | $tags = preg_split('/[\s,]+/', $tags); | ||
428 | // Remove eventual empty values | ||
429 | $tags = array_values(array_filter($tags)); | ||
430 | |||
431 | $this->tags = $tags; | ||
432 | |||
433 | return $this; | ||
434 | } | ||
435 | |||
436 | /** | ||
437 | * Rename a tag in tags list. | ||
438 | * | ||
439 | * @param string $fromTag | ||
440 | * @param string $toTag | ||
441 | */ | ||
442 | public function renameTag($fromTag, $toTag) | ||
443 | { | ||
444 | if (($pos = array_search($fromTag, $this->tags)) !== false) { | ||
445 | $this->tags[$pos] = trim($toTag); | ||
446 | } | ||
447 | } | ||
448 | |||
449 | /** | ||
450 | * Delete a tag from tags list. | ||
451 | * | ||
452 | * @param string $tag | ||
453 | */ | ||
454 | public function deleteTag($tag) | ||
455 | { | ||
456 | if (($pos = array_search($tag, $this->tags)) !== false) { | ||
457 | unset($this->tags[$pos]); | ||
458 | $this->tags = array_values($this->tags); | ||
459 | } | ||
460 | } | ||
461 | } | ||
diff --git a/application/bookmark/BookmarkArray.php b/application/bookmark/BookmarkArray.php new file mode 100644 index 00000000..d87d43b4 --- /dev/null +++ b/application/bookmark/BookmarkArray.php | |||
@@ -0,0 +1,259 @@ | |||
1 | <?php | ||
2 | |||
3 | namespace Shaarli\Bookmark; | ||
4 | |||
5 | use Shaarli\Bookmark\Exception\InvalidBookmarkException; | ||
6 | |||
7 | /** | ||
8 | * Class BookmarkArray | ||
9 | * | ||
10 | * Implementing ArrayAccess, this allows us to use the bookmark list | ||
11 | * as an array and iterate over it. | ||
12 | * | ||
13 | * @package Shaarli\Bookmark | ||
14 | */ | ||
15 | class BookmarkArray implements \Iterator, \Countable, \ArrayAccess | ||
16 | { | ||
17 | /** | ||
18 | * @var Bookmark[] | ||
19 | */ | ||
20 | protected $bookmarks; | ||
21 | |||
22 | /** | ||
23 | * @var array List of all bookmarks IDS mapped with their array offset. | ||
24 | * Map: id->offset. | ||
25 | */ | ||
26 | protected $ids; | ||
27 | |||
28 | /** | ||
29 | * @var int Position in the $this->keys array (for the Iterator interface) | ||
30 | */ | ||
31 | protected $position; | ||
32 | |||
33 | /** | ||
34 | * @var array List of offset keys (for the Iterator interface implementation) | ||
35 | */ | ||
36 | protected $keys; | ||
37 | |||
38 | /** | ||
39 | * @var array List of all recorded URLs (key=url, value=bookmark offset) | ||
40 | * for fast reserve search (url-->bookmark offset) | ||
41 | */ | ||
42 | protected $urls; | ||
43 | |||
44 | public function __construct() | ||
45 | { | ||
46 | $this->ids = []; | ||
47 | $this->bookmarks = []; | ||
48 | $this->keys = []; | ||
49 | $this->urls = []; | ||
50 | $this->position = 0; | ||
51 | } | ||
52 | |||
53 | /** | ||
54 | * Countable - Counts elements of an object | ||
55 | * | ||
56 | * @return int Number of bookmarks | ||
57 | */ | ||
58 | public function count() | ||
59 | { | ||
60 | return count($this->bookmarks); | ||
61 | } | ||
62 | |||
63 | /** | ||
64 | * ArrayAccess - Assigns a value to the specified offset | ||
65 | * | ||
66 | * @param int $offset Bookmark ID | ||
67 | * @param Bookmark $value instance | ||
68 | * | ||
69 | * @throws InvalidBookmarkException | ||
70 | */ | ||
71 | public function offsetSet($offset, $value) | ||
72 | { | ||
73 | if (! $value instanceof Bookmark | ||
74 | || $value->getId() === null || empty($value->getUrl()) | ||
75 | || ($offset !== null && ! is_int($offset)) || ! is_int($value->getId()) | ||
76 | || $offset !== null && $offset !== $value->getId() | ||
77 | ) { | ||
78 | throw new InvalidBookmarkException($value); | ||
79 | } | ||
80 | |||
81 | // If the bookmark exists, we reuse the real offset, otherwise new entry | ||
82 | if ($offset !== null) { | ||
83 | $existing = $this->getBookmarkOffset($offset); | ||
84 | } else { | ||
85 | $existing = $this->getBookmarkOffset($value->getId()); | ||
86 | } | ||
87 | |||
88 | if ($existing !== null) { | ||
89 | $offset = $existing; | ||
90 | } else { | ||
91 | $offset = count($this->bookmarks); | ||
92 | } | ||
93 | |||
94 | $this->bookmarks[$offset] = $value; | ||
95 | $this->urls[$value->getUrl()] = $offset; | ||
96 | $this->ids[$value->getId()] = $offset; | ||
97 | } | ||
98 | |||
99 | /** | ||
100 | * ArrayAccess - Whether or not an offset exists | ||
101 | * | ||
102 | * @param int $offset Bookmark ID | ||
103 | * | ||
104 | * @return bool true if it exists, false otherwise | ||
105 | */ | ||
106 | public function offsetExists($offset) | ||
107 | { | ||
108 | return array_key_exists($this->getBookmarkOffset($offset), $this->bookmarks); | ||
109 | } | ||
110 | |||
111 | /** | ||
112 | * ArrayAccess - Unsets an offset | ||
113 | * | ||
114 | * @param int $offset Bookmark ID | ||
115 | */ | ||
116 | public function offsetUnset($offset) | ||
117 | { | ||
118 | $realOffset = $this->getBookmarkOffset($offset); | ||
119 | $url = $this->bookmarks[$realOffset]->getUrl(); | ||
120 | unset($this->urls[$url]); | ||
121 | unset($this->ids[$offset]); | ||
122 | unset($this->bookmarks[$realOffset]); | ||
123 | } | ||
124 | |||
125 | /** | ||
126 | * ArrayAccess - Returns the value at specified offset | ||
127 | * | ||
128 | * @param int $offset Bookmark ID | ||
129 | * | ||
130 | * @return Bookmark|null The Bookmark if found, null otherwise | ||
131 | */ | ||
132 | public function offsetGet($offset) | ||
133 | { | ||
134 | $realOffset = $this->getBookmarkOffset($offset); | ||
135 | return isset($this->bookmarks[$realOffset]) ? $this->bookmarks[$realOffset] : null; | ||
136 | } | ||
137 | |||
138 | /** | ||
139 | * Iterator - Returns the current element | ||
140 | * | ||
141 | * @return Bookmark corresponding to the current position | ||
142 | */ | ||
143 | public function current() | ||
144 | { | ||
145 | return $this[$this->keys[$this->position]]; | ||
146 | } | ||
147 | |||
148 | /** | ||
149 | * Iterator - Returns the key of the current element | ||
150 | * | ||
151 | * @return int Bookmark ID corresponding to the current position | ||
152 | */ | ||
153 | public function key() | ||
154 | { | ||
155 | return $this->keys[$this->position]; | ||
156 | } | ||
157 | |||
158 | /** | ||
159 | * Iterator - Moves forward to next element | ||
160 | */ | ||
161 | public function next() | ||
162 | { | ||
163 | ++$this->position; | ||
164 | } | ||
165 | |||
166 | /** | ||
167 | * Iterator - Rewinds the Iterator to the first element | ||
168 | * | ||
169 | * Entries are sorted by date (latest first) | ||
170 | */ | ||
171 | public function rewind() | ||
172 | { | ||
173 | $this->keys = array_keys($this->ids); | ||
174 | $this->position = 0; | ||
175 | } | ||
176 | |||
177 | /** | ||
178 | * Iterator - Checks if current position is valid | ||
179 | * | ||
180 | * @return bool true if the current Bookmark ID exists, false otherwise | ||
181 | */ | ||
182 | public function valid() | ||
183 | { | ||
184 | return isset($this->keys[$this->position]); | ||
185 | } | ||
186 | |||
187 | /** | ||
188 | * Returns a bookmark offset in bookmarks array from its unique ID. | ||
189 | * | ||
190 | * @param int $id Persistent ID of a bookmark. | ||
191 | * | ||
192 | * @return int Real offset in local array, or null if doesn't exist. | ||
193 | */ | ||
194 | protected function getBookmarkOffset($id) | ||
195 | { | ||
196 | if (isset($this->ids[$id])) { | ||
197 | return $this->ids[$id]; | ||
198 | } | ||
199 | return null; | ||
200 | } | ||
201 | |||
202 | /** | ||
203 | * Return the next key for bookmark creation. | ||
204 | * E.g. If the last ID is 597, the next will be 598. | ||
205 | * | ||
206 | * @return int next ID. | ||
207 | */ | ||
208 | public function getNextId() | ||
209 | { | ||
210 | if (!empty($this->ids)) { | ||
211 | return max(array_keys($this->ids)) + 1; | ||
212 | } | ||
213 | return 0; | ||
214 | } | ||
215 | |||
216 | /** | ||
217 | * @param $url | ||
218 | * | ||
219 | * @return Bookmark|null | ||
220 | */ | ||
221 | public function getByUrl($url) | ||
222 | { | ||
223 | if (! empty($url) | ||
224 | && isset($this->urls[$url]) | ||
225 | && isset($this->bookmarks[$this->urls[$url]]) | ||
226 | ) { | ||
227 | return $this->bookmarks[$this->urls[$url]]; | ||
228 | } | ||
229 | return null; | ||
230 | } | ||
231 | |||
232 | /** | ||
233 | * Reorder links by creation date (newest first). | ||
234 | * | ||
235 | * Also update the urls and ids mapping arrays. | ||
236 | * | ||
237 | * @param string $order ASC|DESC | ||
238 | */ | ||
239 | public function reorder($order = 'DESC') | ||
240 | { | ||
241 | $order = $order === 'ASC' ? -1 : 1; | ||
242 | // Reorder array by dates. | ||
243 | usort($this->bookmarks, function ($a, $b) use ($order) { | ||
244 | /** @var $a Bookmark */ | ||
245 | /** @var $b Bookmark */ | ||
246 | if ($a->isSticky() !== $b->isSticky()) { | ||
247 | return $a->isSticky() ? -1 : 1; | ||
248 | } | ||
249 | return $a->getCreated() < $b->getCreated() ? 1 * $order : -1 * $order; | ||
250 | }); | ||
251 | |||
252 | $this->urls = []; | ||
253 | $this->ids = []; | ||
254 | foreach ($this->bookmarks as $key => $bookmark) { | ||
255 | $this->urls[$bookmark->getUrl()] = $key; | ||
256 | $this->ids[$bookmark->getId()] = $key; | ||
257 | } | ||
258 | } | ||
259 | } | ||
diff --git a/application/bookmark/BookmarkFileService.php b/application/bookmark/BookmarkFileService.php new file mode 100644 index 00000000..a56cc92b --- /dev/null +++ b/application/bookmark/BookmarkFileService.php | |||
@@ -0,0 +1,373 @@ | |||
1 | <?php | ||
2 | |||
3 | |||
4 | namespace Shaarli\Bookmark; | ||
5 | |||
6 | |||
7 | use Exception; | ||
8 | use Shaarli\Bookmark\Exception\BookmarkNotFoundException; | ||
9 | use Shaarli\Bookmark\Exception\EmptyDataStoreException; | ||
10 | use Shaarli\Config\ConfigManager; | ||
11 | use Shaarli\History; | ||
12 | use Shaarli\Legacy\LegacyLinkDB; | ||
13 | use Shaarli\Legacy\LegacyUpdater; | ||
14 | use Shaarli\Updater\UpdaterUtils; | ||
15 | |||
16 | /** | ||
17 | * Class BookmarksService | ||
18 | * | ||
19 | * This is the entry point to manipulate the bookmark DB. | ||
20 | * It manipulates loads links from a file data store containing all bookmarks. | ||
21 | * | ||
22 | * It also triggers the legacy format (bookmarks as arrays) migration. | ||
23 | */ | ||
24 | class BookmarkFileService implements BookmarkServiceInterface | ||
25 | { | ||
26 | /** @var Bookmark[] instance */ | ||
27 | protected $bookmarks; | ||
28 | |||
29 | /** @var BookmarkIO instance */ | ||
30 | protected $bookmarksIO; | ||
31 | |||
32 | /** @var BookmarkFilter */ | ||
33 | protected $bookmarkFilter; | ||
34 | |||
35 | /** @var ConfigManager instance */ | ||
36 | protected $conf; | ||
37 | |||
38 | /** @var History instance */ | ||
39 | protected $history; | ||
40 | |||
41 | /** @var bool true for logged in users. Default value to retrieve private bookmarks. */ | ||
42 | protected $isLoggedIn; | ||
43 | |||
44 | /** | ||
45 | * @inheritDoc | ||
46 | */ | ||
47 | public function __construct(ConfigManager $conf, History $history, $isLoggedIn) | ||
48 | { | ||
49 | $this->conf = $conf; | ||
50 | $this->history = $history; | ||
51 | $this->bookmarksIO = new BookmarkIO($this->conf); | ||
52 | $this->isLoggedIn = $isLoggedIn; | ||
53 | |||
54 | if (!$this->isLoggedIn && $this->conf->get('privacy.hide_public_links', false)) { | ||
55 | $this->bookmarks = []; | ||
56 | } else { | ||
57 | try { | ||
58 | $this->bookmarks = $this->bookmarksIO->read(); | ||
59 | } catch (EmptyDataStoreException $e) { | ||
60 | $this->bookmarks = new BookmarkArray(); | ||
61 | if ($isLoggedIn) { | ||
62 | $this->save(); | ||
63 | } | ||
64 | } | ||
65 | |||
66 | if (! $this->bookmarks instanceof BookmarkArray) { | ||
67 | $this->migrate(); | ||
68 | exit( | ||
69 | 'Your data store has been migrated, please reload the page.'. PHP_EOL . | ||
70 | 'If this message keeps showing up, please delete data/updates.txt file.' | ||
71 | ); | ||
72 | } | ||
73 | } | ||
74 | |||
75 | $this->bookmarkFilter = new BookmarkFilter($this->bookmarks); | ||
76 | } | ||
77 | |||
78 | /** | ||
79 | * @inheritDoc | ||
80 | */ | ||
81 | public function findByHash($hash) | ||
82 | { | ||
83 | $bookmark = $this->bookmarkFilter->filter(BookmarkFilter::$FILTER_HASH, $hash); | ||
84 | // PHP 7.3 introduced array_key_first() to avoid this hack | ||
85 | $first = reset($bookmark); | ||
86 | if (! $this->isLoggedIn && $first->isPrivate()) { | ||
87 | throw new Exception('Not authorized'); | ||
88 | } | ||
89 | |||
90 | return $bookmark; | ||
91 | } | ||
92 | |||
93 | /** | ||
94 | * @inheritDoc | ||
95 | */ | ||
96 | public function findByUrl($url) | ||
97 | { | ||
98 | return $this->bookmarks->getByUrl($url); | ||
99 | } | ||
100 | |||
101 | /** | ||
102 | * @inheritDoc | ||
103 | */ | ||
104 | public function search($request = [], $visibility = null, $caseSensitive = false, $untaggedOnly = false) | ||
105 | { | ||
106 | if ($visibility === null) { | ||
107 | $visibility = $this->isLoggedIn ? BookmarkFilter::$ALL : BookmarkFilter::$PUBLIC; | ||
108 | } | ||
109 | |||
110 | // Filter bookmark database according to parameters. | ||
111 | $searchtags = isset($request['searchtags']) ? $request['searchtags'] : ''; | ||
112 | $searchterm = isset($request['searchterm']) ? $request['searchterm'] : ''; | ||
113 | |||
114 | return $this->bookmarkFilter->filter( | ||
115 | BookmarkFilter::$FILTER_TAG | BookmarkFilter::$FILTER_TEXT, | ||
116 | [$searchtags, $searchterm], | ||
117 | $caseSensitive, | ||
118 | $visibility, | ||
119 | $untaggedOnly | ||
120 | ); | ||
121 | } | ||
122 | |||
123 | /** | ||
124 | * @inheritDoc | ||
125 | */ | ||
126 | public function get($id, $visibility = null) | ||
127 | { | ||
128 | if (! isset($this->bookmarks[$id])) { | ||
129 | throw new BookmarkNotFoundException(); | ||
130 | } | ||
131 | |||
132 | if ($visibility === null) { | ||
133 | $visibility = $this->isLoggedIn ? 'all' : 'public'; | ||
134 | } | ||
135 | |||
136 | $bookmark = $this->bookmarks[$id]; | ||
137 | if (($bookmark->isPrivate() && $visibility != 'all' && $visibility != 'private') | ||
138 | || (! $bookmark->isPrivate() && $visibility != 'all' && $visibility != 'public') | ||
139 | ) { | ||
140 | throw new Exception('Unauthorized'); | ||
141 | } | ||
142 | |||
143 | return $bookmark; | ||
144 | } | ||
145 | |||
146 | /** | ||
147 | * @inheritDoc | ||
148 | */ | ||
149 | public function set($bookmark, $save = true) | ||
150 | { | ||
151 | if ($this->isLoggedIn !== true) { | ||
152 | throw new Exception(t('You\'re not authorized to alter the datastore')); | ||
153 | } | ||
154 | if (! $bookmark instanceof Bookmark) { | ||
155 | throw new Exception(t('Provided data is invalid')); | ||
156 | } | ||
157 | if (! isset($this->bookmarks[$bookmark->getId()])) { | ||
158 | throw new BookmarkNotFoundException(); | ||
159 | } | ||
160 | $bookmark->validate(); | ||
161 | |||
162 | $bookmark->setUpdated(new \DateTime()); | ||
163 | $this->bookmarks[$bookmark->getId()] = $bookmark; | ||
164 | if ($save === true) { | ||
165 | $this->save(); | ||
166 | $this->history->updateLink($bookmark); | ||
167 | } | ||
168 | return $this->bookmarks[$bookmark->getId()]; | ||
169 | } | ||
170 | |||
171 | /** | ||
172 | * @inheritDoc | ||
173 | */ | ||
174 | public function add($bookmark, $save = true) | ||
175 | { | ||
176 | if ($this->isLoggedIn !== true) { | ||
177 | throw new Exception(t('You\'re not authorized to alter the datastore')); | ||
178 | } | ||
179 | if (! $bookmark instanceof Bookmark) { | ||
180 | throw new Exception(t('Provided data is invalid')); | ||
181 | } | ||
182 | if (! empty($bookmark->getId())) { | ||
183 | throw new Exception(t('This bookmarks already exists')); | ||
184 | } | ||
185 | $bookmark->setId($this->bookmarks->getNextId()); | ||
186 | $bookmark->validate(); | ||
187 | |||
188 | $this->bookmarks[$bookmark->getId()] = $bookmark; | ||
189 | if ($save === true) { | ||
190 | $this->save(); | ||
191 | $this->history->addLink($bookmark); | ||
192 | } | ||
193 | return $this->bookmarks[$bookmark->getId()]; | ||
194 | } | ||
195 | |||
196 | /** | ||
197 | * @inheritDoc | ||
198 | */ | ||
199 | public function addOrSet($bookmark, $save = true) | ||
200 | { | ||
201 | if ($this->isLoggedIn !== true) { | ||
202 | throw new Exception(t('You\'re not authorized to alter the datastore')); | ||
203 | } | ||
204 | if (! $bookmark instanceof Bookmark) { | ||
205 | throw new Exception('Provided data is invalid'); | ||
206 | } | ||
207 | if ($bookmark->getId() === null) { | ||
208 | return $this->add($bookmark, $save); | ||
209 | } | ||
210 | return $this->set($bookmark, $save); | ||
211 | } | ||
212 | |||
213 | /** | ||
214 | * @inheritDoc | ||
215 | */ | ||
216 | public function remove($bookmark, $save = true) | ||
217 | { | ||
218 | if ($this->isLoggedIn !== true) { | ||
219 | throw new Exception(t('You\'re not authorized to alter the datastore')); | ||
220 | } | ||
221 | if (! $bookmark instanceof Bookmark) { | ||
222 | throw new Exception(t('Provided data is invalid')); | ||
223 | } | ||
224 | if (! isset($this->bookmarks[$bookmark->getId()])) { | ||
225 | throw new BookmarkNotFoundException(); | ||
226 | } | ||
227 | |||
228 | unset($this->bookmarks[$bookmark->getId()]); | ||
229 | if ($save === true) { | ||
230 | $this->save(); | ||
231 | $this->history->deleteLink($bookmark); | ||
232 | } | ||
233 | } | ||
234 | |||
235 | /** | ||
236 | * @inheritDoc | ||
237 | */ | ||
238 | public function exists($id, $visibility = null) | ||
239 | { | ||
240 | if (! isset($this->bookmarks[$id])) { | ||
241 | return false; | ||
242 | } | ||
243 | |||
244 | if ($visibility === null) { | ||
245 | $visibility = $this->isLoggedIn ? 'all' : 'public'; | ||
246 | } | ||
247 | |||
248 | $bookmark = $this->bookmarks[$id]; | ||
249 | if (($bookmark->isPrivate() && $visibility != 'all' && $visibility != 'private') | ||
250 | || (! $bookmark->isPrivate() && $visibility != 'all' && $visibility != 'public') | ||
251 | ) { | ||
252 | return false; | ||
253 | } | ||
254 | |||
255 | return true; | ||
256 | } | ||
257 | |||
258 | /** | ||
259 | * @inheritDoc | ||
260 | */ | ||
261 | public function count($visibility = null) | ||
262 | { | ||
263 | return count($this->search([], $visibility)); | ||
264 | } | ||
265 | |||
266 | /** | ||
267 | * @inheritDoc | ||
268 | */ | ||
269 | public function save() | ||
270 | { | ||
271 | if (!$this->isLoggedIn) { | ||
272 | // TODO: raise an Exception instead | ||
273 | die('You are not authorized to change the database.'); | ||
274 | } | ||
275 | $this->bookmarks->reorder(); | ||
276 | $this->bookmarksIO->write($this->bookmarks); | ||
277 | invalidateCaches($this->conf->get('resource.page_cache')); | ||
278 | } | ||
279 | |||
280 | /** | ||
281 | * @inheritDoc | ||
282 | */ | ||
283 | public function bookmarksCountPerTag($filteringTags = [], $visibility = null) | ||
284 | { | ||
285 | $bookmarks = $this->search(['searchtags' => $filteringTags], $visibility); | ||
286 | $tags = []; | ||
287 | $caseMapping = []; | ||
288 | foreach ($bookmarks as $bookmark) { | ||
289 | foreach ($bookmark->getTags() as $tag) { | ||
290 | if (empty($tag) || (! $this->isLoggedIn && startsWith($tag, '.'))) { | ||
291 | continue; | ||
292 | } | ||
293 | // The first case found will be displayed. | ||
294 | if (!isset($caseMapping[strtolower($tag)])) { | ||
295 | $caseMapping[strtolower($tag)] = $tag; | ||
296 | $tags[$caseMapping[strtolower($tag)]] = 0; | ||
297 | } | ||
298 | $tags[$caseMapping[strtolower($tag)]]++; | ||
299 | } | ||
300 | } | ||
301 | |||
302 | /* | ||
303 | * Formerly used arsort(), which doesn't define the sort behaviour for equal values. | ||
304 | * Also, this function doesn't produce the same result between PHP 5.6 and 7. | ||
305 | * | ||
306 | * So we now use array_multisort() to sort tags by DESC occurrences, | ||
307 | * then ASC alphabetically for equal values. | ||
308 | * | ||
309 | * @see https://github.com/shaarli/Shaarli/issues/1142 | ||
310 | */ | ||
311 | $keys = array_keys($tags); | ||
312 | $tmpTags = array_combine($keys, $keys); | ||
313 | array_multisort($tags, SORT_DESC, $tmpTags, SORT_ASC, $tags); | ||
314 | return $tags; | ||
315 | } | ||
316 | |||
317 | /** | ||
318 | * @inheritDoc | ||
319 | */ | ||
320 | public function days() | ||
321 | { | ||
322 | $bookmarkDays = []; | ||
323 | foreach ($this->search() as $bookmark) { | ||
324 | $bookmarkDays[$bookmark->getCreated()->format('Ymd')] = 0; | ||
325 | } | ||
326 | $bookmarkDays = array_keys($bookmarkDays); | ||
327 | sort($bookmarkDays); | ||
328 | |||
329 | return $bookmarkDays; | ||
330 | } | ||
331 | |||
332 | /** | ||
333 | * @inheritDoc | ||
334 | */ | ||
335 | public function filterDay($request) | ||
336 | { | ||
337 | return $this->bookmarkFilter->filter(BookmarkFilter::$FILTER_DAY, $request); | ||
338 | } | ||
339 | |||
340 | /** | ||
341 | * @inheritDoc | ||
342 | */ | ||
343 | public function initialize() | ||
344 | { | ||
345 | $initializer = new BookmarkInitializer($this); | ||
346 | $initializer->initialize(); | ||
347 | } | ||
348 | |||
349 | /** | ||
350 | * Handles migration to the new database format (BookmarksArray). | ||
351 | */ | ||
352 | protected function migrate() | ||
353 | { | ||
354 | $bookmarkDb = new LegacyLinkDB( | ||
355 | $this->conf->get('resource.datastore'), | ||
356 | true, | ||
357 | false | ||
358 | ); | ||
359 | $updater = new LegacyUpdater( | ||
360 | UpdaterUtils::read_updates_file($this->conf->get('resource.updates')), | ||
361 | $bookmarkDb, | ||
362 | $this->conf, | ||
363 | true | ||
364 | ); | ||
365 | $newUpdates = $updater->update(); | ||
366 | if (! empty($newUpdates)) { | ||
367 | UpdaterUtils::write_updates_file( | ||
368 | $this->conf->get('resource.updates'), | ||
369 | $updater->getDoneUpdates() | ||
370 | ); | ||
371 | } | ||
372 | } | ||
373 | } | ||
diff --git a/application/bookmark/BookmarkFilter.php b/application/bookmark/BookmarkFilter.php new file mode 100644 index 00000000..fd556679 --- /dev/null +++ b/application/bookmark/BookmarkFilter.php | |||
@@ -0,0 +1,468 @@ | |||
1 | <?php | ||
2 | |||
3 | namespace Shaarli\Bookmark; | ||
4 | |||
5 | use Exception; | ||
6 | use Shaarli\Bookmark\Exception\BookmarkNotFoundException; | ||
7 | |||
8 | /** | ||
9 | * Class LinkFilter. | ||
10 | * | ||
11 | * Perform search and filter operation on link data list. | ||
12 | */ | ||
13 | class BookmarkFilter | ||
14 | { | ||
15 | /** | ||
16 | * @var string permalinks. | ||
17 | */ | ||
18 | public static $FILTER_HASH = 'permalink'; | ||
19 | |||
20 | /** | ||
21 | * @var string text search. | ||
22 | */ | ||
23 | public static $FILTER_TEXT = 'fulltext'; | ||
24 | |||
25 | /** | ||
26 | * @var string tag filter. | ||
27 | */ | ||
28 | public static $FILTER_TAG = 'tags'; | ||
29 | |||
30 | /** | ||
31 | * @var string filter by day. | ||
32 | */ | ||
33 | public static $FILTER_DAY = 'FILTER_DAY'; | ||
34 | |||
35 | /** | ||
36 | * @var string filter by day. | ||
37 | */ | ||
38 | public static $DEFAULT = 'NO_FILTER'; | ||
39 | |||
40 | /** @var string Visibility: all */ | ||
41 | public static $ALL = 'all'; | ||
42 | |||
43 | /** @var string Visibility: public */ | ||
44 | public static $PUBLIC = 'public'; | ||
45 | |||
46 | /** @var string Visibility: private */ | ||
47 | public static $PRIVATE = 'private'; | ||
48 | |||
49 | /** | ||
50 | * @var string Allowed characters for hashtags (regex syntax). | ||
51 | */ | ||
52 | public static $HASHTAG_CHARS = '\p{Pc}\p{N}\p{L}\p{Mn}'; | ||
53 | |||
54 | /** | ||
55 | * @var Bookmark[] all available bookmarks. | ||
56 | */ | ||
57 | private $bookmarks; | ||
58 | |||
59 | /** | ||
60 | * @param Bookmark[] $bookmarks initialization. | ||
61 | */ | ||
62 | public function __construct($bookmarks) | ||
63 | { | ||
64 | $this->bookmarks = $bookmarks; | ||
65 | } | ||
66 | |||
67 | /** | ||
68 | * Filter bookmarks according to parameters. | ||
69 | * | ||
70 | * @param string $type Type of filter (eg. tags, permalink, etc.). | ||
71 | * @param mixed $request Filter content. | ||
72 | * @param bool $casesensitive Optional: Perform case sensitive filter if true. | ||
73 | * @param string $visibility Optional: return only all/private/public bookmarks | ||
74 | * @param bool $untaggedonly Optional: return only untagged bookmarks. Applies only if $type includes FILTER_TAG | ||
75 | * | ||
76 | * @return Bookmark[] filtered bookmark list. | ||
77 | * | ||
78 | * @throws BookmarkNotFoundException | ||
79 | */ | ||
80 | public function filter($type, $request, $casesensitive = false, $visibility = 'all', $untaggedonly = false) | ||
81 | { | ||
82 | if (!in_array($visibility, ['all', 'public', 'private'])) { | ||
83 | $visibility = 'all'; | ||
84 | } | ||
85 | |||
86 | switch ($type) { | ||
87 | case self::$FILTER_HASH: | ||
88 | return $this->filterSmallHash($request); | ||
89 | case self::$FILTER_TAG | self::$FILTER_TEXT: // == "vuotext" | ||
90 | $noRequest = empty($request) || (empty($request[0]) && empty($request[1])); | ||
91 | if ($noRequest) { | ||
92 | if ($untaggedonly) { | ||
93 | return $this->filterUntagged($visibility); | ||
94 | } | ||
95 | return $this->noFilter($visibility); | ||
96 | } | ||
97 | if ($untaggedonly) { | ||
98 | $filtered = $this->filterUntagged($visibility); | ||
99 | } else { | ||
100 | $filtered = $this->bookmarks; | ||
101 | } | ||
102 | if (!empty($request[0])) { | ||
103 | $filtered = (new BookmarkFilter($filtered))->filterTags($request[0], $casesensitive, $visibility); | ||
104 | } | ||
105 | if (!empty($request[1])) { | ||
106 | $filtered = (new BookmarkFilter($filtered))->filterFulltext($request[1], $visibility); | ||
107 | } | ||
108 | return $filtered; | ||
109 | case self::$FILTER_TEXT: | ||
110 | return $this->filterFulltext($request, $visibility); | ||
111 | case self::$FILTER_TAG: | ||
112 | if ($untaggedonly) { | ||
113 | return $this->filterUntagged($visibility); | ||
114 | } else { | ||
115 | return $this->filterTags($request, $casesensitive, $visibility); | ||
116 | } | ||
117 | case self::$FILTER_DAY: | ||
118 | return $this->filterDay($request); | ||
119 | default: | ||
120 | return $this->noFilter($visibility); | ||
121 | } | ||
122 | } | ||
123 | |||
124 | /** | ||
125 | * Unknown filter, but handle private only. | ||
126 | * | ||
127 | * @param string $visibility Optional: return only all/private/public bookmarks | ||
128 | * | ||
129 | * @return Bookmark[] filtered bookmarks. | ||
130 | */ | ||
131 | private function noFilter($visibility = 'all') | ||
132 | { | ||
133 | if ($visibility === 'all') { | ||
134 | return $this->bookmarks; | ||
135 | } | ||
136 | |||
137 | $out = array(); | ||
138 | foreach ($this->bookmarks as $key => $value) { | ||
139 | if ($value->isPrivate() && $visibility === 'private') { | ||
140 | $out[$key] = $value; | ||
141 | } elseif (!$value->isPrivate() && $visibility === 'public') { | ||
142 | $out[$key] = $value; | ||
143 | } | ||
144 | } | ||
145 | |||
146 | return $out; | ||
147 | } | ||
148 | |||
149 | /** | ||
150 | * Returns the shaare corresponding to a smallHash. | ||
151 | * | ||
152 | * @param string $smallHash permalink hash. | ||
153 | * | ||
154 | * @return array $filtered array containing permalink data. | ||
155 | * | ||
156 | * @throws \Shaarli\Bookmark\Exception\BookmarkNotFoundException if the smallhash doesn't match any link. | ||
157 | */ | ||
158 | private function filterSmallHash($smallHash) | ||
159 | { | ||
160 | foreach ($this->bookmarks as $key => $l) { | ||
161 | if ($smallHash == $l->getShortUrl()) { | ||
162 | // Yes, this is ugly and slow | ||
163 | return [$key => $l]; | ||
164 | } | ||
165 | } | ||
166 | |||
167 | throw new BookmarkNotFoundException(); | ||
168 | } | ||
169 | |||
170 | /** | ||
171 | * Returns the list of bookmarks corresponding to a full-text search | ||
172 | * | ||
173 | * Searches: | ||
174 | * - in the URLs, title and description; | ||
175 | * - are case-insensitive; | ||
176 | * - terms surrounded by quotes " are exact terms search. | ||
177 | * - terms starting with a dash - are excluded (except exact terms). | ||
178 | * | ||
179 | * Example: | ||
180 | * print_r($mydb->filterFulltext('hollandais')); | ||
181 | * | ||
182 | * mb_convert_case($val, MB_CASE_LOWER, 'UTF-8') | ||
183 | * - allows to perform searches on Unicode text | ||
184 | * - see https://github.com/shaarli/Shaarli/issues/75 for examples | ||
185 | * | ||
186 | * @param string $searchterms search query. | ||
187 | * @param string $visibility Optional: return only all/private/public bookmarks. | ||
188 | * | ||
189 | * @return array search results. | ||
190 | */ | ||
191 | private function filterFulltext($searchterms, $visibility = 'all') | ||
192 | { | ||
193 | if (empty($searchterms)) { | ||
194 | return $this->noFilter($visibility); | ||
195 | } | ||
196 | |||
197 | $filtered = array(); | ||
198 | $search = mb_convert_case(html_entity_decode($searchterms), MB_CASE_LOWER, 'UTF-8'); | ||
199 | $exactRegex = '/"([^"]+)"/'; | ||
200 | // Retrieve exact search terms. | ||
201 | preg_match_all($exactRegex, $search, $exactSearch); | ||
202 | $exactSearch = array_values(array_filter($exactSearch[1])); | ||
203 | |||
204 | // Remove exact search terms to get AND terms search. | ||
205 | $explodedSearchAnd = explode(' ', trim(preg_replace($exactRegex, '', $search))); | ||
206 | $explodedSearchAnd = array_values(array_filter($explodedSearchAnd)); | ||
207 | |||
208 | // Filter excluding terms and update andSearch. | ||
209 | $excludeSearch = array(); | ||
210 | $andSearch = array(); | ||
211 | foreach ($explodedSearchAnd as $needle) { | ||
212 | if ($needle[0] == '-' && strlen($needle) > 1) { | ||
213 | $excludeSearch[] = substr($needle, 1); | ||
214 | } else { | ||
215 | $andSearch[] = $needle; | ||
216 | } | ||
217 | } | ||
218 | |||
219 | // Iterate over every stored link. | ||
220 | foreach ($this->bookmarks as $id => $link) { | ||
221 | // ignore non private bookmarks when 'privatonly' is on. | ||
222 | if ($visibility !== 'all') { | ||
223 | if (!$link->isPrivate() && $visibility === 'private') { | ||
224 | continue; | ||
225 | } elseif ($link->isPrivate() && $visibility === 'public') { | ||
226 | continue; | ||
227 | } | ||
228 | } | ||
229 | |||
230 | // Concatenate link fields to search across fields. | ||
231 | // Adds a '\' separator for exact search terms. | ||
232 | $content = mb_convert_case($link->getTitle(), MB_CASE_LOWER, 'UTF-8') .'\\'; | ||
233 | $content .= mb_convert_case($link->getDescription(), MB_CASE_LOWER, 'UTF-8') .'\\'; | ||
234 | $content .= mb_convert_case($link->getUrl(), MB_CASE_LOWER, 'UTF-8') .'\\'; | ||
235 | $content .= mb_convert_case($link->getTagsString(), MB_CASE_LOWER, 'UTF-8') .'\\'; | ||
236 | |||
237 | // Be optimistic | ||
238 | $found = true; | ||
239 | |||
240 | // First, we look for exact term search | ||
241 | for ($i = 0; $i < count($exactSearch) && $found; $i++) { | ||
242 | $found = strpos($content, $exactSearch[$i]) !== false; | ||
243 | } | ||
244 | |||
245 | // Iterate over keywords, if keyword is not found, | ||
246 | // no need to check for the others. We want all or nothing. | ||
247 | for ($i = 0; $i < count($andSearch) && $found; $i++) { | ||
248 | $found = strpos($content, $andSearch[$i]) !== false; | ||
249 | } | ||
250 | |||
251 | // Exclude terms. | ||
252 | for ($i = 0; $i < count($excludeSearch) && $found; $i++) { | ||
253 | $found = strpos($content, $excludeSearch[$i]) === false; | ||
254 | } | ||
255 | |||
256 | if ($found) { | ||
257 | $filtered[$id] = $link; | ||
258 | } | ||
259 | } | ||
260 | |||
261 | return $filtered; | ||
262 | } | ||
263 | |||
264 | /** | ||
265 | * generate a regex fragment out of a tag | ||
266 | * | ||
267 | * @param string $tag to to generate regexs from. may start with '-' to negate, contain '*' as wildcard | ||
268 | * | ||
269 | * @return string generated regex fragment | ||
270 | */ | ||
271 | private static function tag2regex($tag) | ||
272 | { | ||
273 | $len = strlen($tag); | ||
274 | if (!$len || $tag === "-" || $tag === "*") { | ||
275 | // nothing to search, return empty regex | ||
276 | return ''; | ||
277 | } | ||
278 | if ($tag[0] === "-") { | ||
279 | // query is negated | ||
280 | $i = 1; // use offset to start after '-' character | ||
281 | $regex = '(?!'; // create negative lookahead | ||
282 | } else { | ||
283 | $i = 0; // start at first character | ||
284 | $regex = '(?='; // use positive lookahead | ||
285 | } | ||
286 | $regex .= '.*(?:^| )'; // before tag may only be a space or the beginning | ||
287 | // iterate over string, separating it into placeholder and content | ||
288 | for (; $i < $len; $i++) { | ||
289 | if ($tag[$i] === '*') { | ||
290 | // placeholder found | ||
291 | $regex .= '[^ ]*?'; | ||
292 | } else { | ||
293 | // regular characters | ||
294 | $offset = strpos($tag, '*', $i); | ||
295 | if ($offset === false) { | ||
296 | // no placeholder found, set offset to end of string | ||
297 | $offset = $len; | ||
298 | } | ||
299 | // subtract one, as we want to get before the placeholder or end of string | ||
300 | $offset -= 1; | ||
301 | // we got a tag name that we want to search for. escape any regex characters to prevent conflicts. | ||
302 | $regex .= preg_quote(substr($tag, $i, $offset - $i + 1), '/'); | ||
303 | // move $i on | ||
304 | $i = $offset; | ||
305 | } | ||
306 | } | ||
307 | $regex .= '(?:$| ))'; // after the tag may only be a space or the end | ||
308 | return $regex; | ||
309 | } | ||
310 | |||
311 | /** | ||
312 | * Returns the list of bookmarks associated with a given list of tags | ||
313 | * | ||
314 | * You can specify one or more tags, separated by space or a comma, e.g. | ||
315 | * print_r($mydb->filterTags('linux programming')); | ||
316 | * | ||
317 | * @param string $tags list of tags separated by commas or blank spaces. | ||
318 | * @param bool $casesensitive ignore case if false. | ||
319 | * @param string $visibility Optional: return only all/private/public bookmarks. | ||
320 | * | ||
321 | * @return array filtered bookmarks. | ||
322 | */ | ||
323 | public function filterTags($tags, $casesensitive = false, $visibility = 'all') | ||
324 | { | ||
325 | // get single tags (we may get passed an array, even though the docs say different) | ||
326 | $inputTags = $tags; | ||
327 | if (!is_array($tags)) { | ||
328 | // we got an input string, split tags | ||
329 | $inputTags = preg_split('/(?:\s+)|,/', $inputTags, -1, PREG_SPLIT_NO_EMPTY); | ||
330 | } | ||
331 | |||
332 | if (!count($inputTags)) { | ||
333 | // no input tags | ||
334 | return $this->noFilter($visibility); | ||
335 | } | ||
336 | |||
337 | // If we only have public visibility, we can't look for hidden tags | ||
338 | if ($visibility === self::$PUBLIC) { | ||
339 | $inputTags = array_values(array_filter($inputTags, function ($tag) { | ||
340 | return ! startsWith($tag, '.'); | ||
341 | })); | ||
342 | |||
343 | if (empty($inputTags)) { | ||
344 | return []; | ||
345 | } | ||
346 | } | ||
347 | |||
348 | // build regex from all tags | ||
349 | $re = '/^' . implode(array_map("self::tag2regex", $inputTags)) . '.*$/'; | ||
350 | if (!$casesensitive) { | ||
351 | // make regex case insensitive | ||
352 | $re .= 'i'; | ||
353 | } | ||
354 | |||
355 | // create resulting array | ||
356 | $filtered = []; | ||
357 | |||
358 | // iterate over each link | ||
359 | foreach ($this->bookmarks as $key => $link) { | ||
360 | // check level of visibility | ||
361 | // ignore non private bookmarks when 'privateonly' is on. | ||
362 | if ($visibility !== 'all') { | ||
363 | if (!$link->isPrivate() && $visibility === 'private') { | ||
364 | continue; | ||
365 | } elseif ($link->isPrivate() && $visibility === 'public') { | ||
366 | continue; | ||
367 | } | ||
368 | } | ||
369 | $search = $link->getTagsString(); // build search string, start with tags of current link | ||
370 | if (strlen(trim($link->getDescription())) && strpos($link->getDescription(), '#') !== false) { | ||
371 | // description given and at least one possible tag found | ||
372 | $descTags = array(); | ||
373 | // find all tags in the form of #tag in the description | ||
374 | preg_match_all( | ||
375 | '/(?<![' . self::$HASHTAG_CHARS . '])#([' . self::$HASHTAG_CHARS . ']+?)\b/sm', | ||
376 | $link->getDescription(), | ||
377 | $descTags | ||
378 | ); | ||
379 | if (count($descTags[1])) { | ||
380 | // there were some tags in the description, add them to the search string | ||
381 | $search .= ' ' . implode(' ', $descTags[1]); | ||
382 | } | ||
383 | }; | ||
384 | // match regular expression with search string | ||
385 | if (!preg_match($re, $search)) { | ||
386 | // this entry does _not_ match our regex | ||
387 | continue; | ||
388 | } | ||
389 | $filtered[$key] = $link; | ||
390 | } | ||
391 | return $filtered; | ||
392 | } | ||
393 | |||
394 | /** | ||
395 | * Return only bookmarks without any tag. | ||
396 | * | ||
397 | * @param string $visibility return only all/private/public bookmarks. | ||
398 | * | ||
399 | * @return array filtered bookmarks. | ||
400 | */ | ||
401 | public function filterUntagged($visibility) | ||
402 | { | ||
403 | $filtered = []; | ||
404 | foreach ($this->bookmarks as $key => $link) { | ||
405 | if ($visibility !== 'all') { | ||
406 | if (!$link->isPrivate() && $visibility === 'private') { | ||
407 | continue; | ||
408 | } elseif ($link->isPrivate() && $visibility === 'public') { | ||
409 | continue; | ||
410 | } | ||
411 | } | ||
412 | |||
413 | if (empty(trim($link->getTagsString()))) { | ||
414 | $filtered[$key] = $link; | ||
415 | } | ||
416 | } | ||
417 | |||
418 | return $filtered; | ||
419 | } | ||
420 | |||
421 | /** | ||
422 | * Returns the list of articles for a given day, chronologically sorted | ||
423 | * | ||
424 | * Day must be in the form 'YYYYMMDD' (e.g. '20120125'), e.g. | ||
425 | * print_r($mydb->filterDay('20120125')); | ||
426 | * | ||
427 | * @param string $day day to filter. | ||
428 | * | ||
429 | * @return array all link matching given day. | ||
430 | * | ||
431 | * @throws Exception if date format is invalid. | ||
432 | */ | ||
433 | public function filterDay($day) | ||
434 | { | ||
435 | if (!checkDateFormat('Ymd', $day)) { | ||
436 | throw new Exception('Invalid date format'); | ||
437 | } | ||
438 | |||
439 | $filtered = array(); | ||
440 | foreach ($this->bookmarks as $key => $l) { | ||
441 | if ($l->getCreated()->format('Ymd') == $day) { | ||
442 | $filtered[$key] = $l; | ||
443 | } | ||
444 | } | ||
445 | |||
446 | // sort by date ASC | ||
447 | return array_reverse($filtered, true); | ||
448 | } | ||
449 | |||
450 | /** | ||
451 | * Convert a list of tags (str) to an array. Also | ||
452 | * - handle case sensitivity. | ||
453 | * - accepts spaces commas as separator. | ||
454 | * | ||
455 | * @param string $tags string containing a list of tags. | ||
456 | * @param bool $casesensitive will convert everything to lowercase if false. | ||
457 | * | ||
458 | * @return array filtered tags string. | ||
459 | */ | ||
460 | public static function tagsStrToArray($tags, $casesensitive) | ||
461 | { | ||
462 | // We use UTF-8 conversion to handle various graphemes (i.e. cyrillic, or greek) | ||
463 | $tagsOut = $casesensitive ? $tags : mb_convert_case($tags, MB_CASE_LOWER, 'UTF-8'); | ||
464 | $tagsOut = str_replace(',', ' ', $tagsOut); | ||
465 | |||
466 | return preg_split('/\s+/', $tagsOut, -1, PREG_SPLIT_NO_EMPTY); | ||
467 | } | ||
468 | } | ||
diff --git a/application/bookmark/BookmarkIO.php b/application/bookmark/BookmarkIO.php new file mode 100644 index 00000000..ae9ffcb4 --- /dev/null +++ b/application/bookmark/BookmarkIO.php | |||
@@ -0,0 +1,108 @@ | |||
1 | <?php | ||
2 | |||
3 | namespace Shaarli\Bookmark; | ||
4 | |||
5 | use Shaarli\Bookmark\Exception\EmptyDataStoreException; | ||
6 | use Shaarli\Bookmark\Exception\NotWritableDataStoreException; | ||
7 | use Shaarli\Config\ConfigManager; | ||
8 | |||
9 | /** | ||
10 | * Class BookmarkIO | ||
11 | * | ||
12 | * This class performs read/write operation to the file data store. | ||
13 | * Used by BookmarkFileService. | ||
14 | * | ||
15 | * @package Shaarli\Bookmark | ||
16 | */ | ||
17 | class BookmarkIO | ||
18 | { | ||
19 | /** | ||
20 | * @var string Datastore file path | ||
21 | */ | ||
22 | protected $datastore; | ||
23 | |||
24 | /** | ||
25 | * @var ConfigManager instance | ||
26 | */ | ||
27 | protected $conf; | ||
28 | |||
29 | /** | ||
30 | * string Datastore PHP prefix | ||
31 | */ | ||
32 | protected static $phpPrefix = '<?php /* '; | ||
33 | |||
34 | /** | ||
35 | * string Datastore PHP suffix | ||
36 | */ | ||
37 | protected static $phpSuffix = ' */ ?>'; | ||
38 | |||
39 | /** | ||
40 | * LinksIO constructor. | ||
41 | * | ||
42 | * @param ConfigManager $conf instance | ||
43 | */ | ||
44 | public function __construct($conf) | ||
45 | { | ||
46 | $this->conf = $conf; | ||
47 | $this->datastore = $conf->get('resource.datastore'); | ||
48 | } | ||
49 | |||
50 | /** | ||
51 | * Reads database from disk to memory | ||
52 | * | ||
53 | * @return BookmarkArray instance | ||
54 | * | ||
55 | * @throws NotWritableDataStoreException Data couldn't be loaded | ||
56 | * @throws EmptyDataStoreException Datastore doesn't exist | ||
57 | */ | ||
58 | public function read() | ||
59 | { | ||
60 | if (! file_exists($this->datastore)) { | ||
61 | throw new EmptyDataStoreException(); | ||
62 | } | ||
63 | |||
64 | if (!is_writable($this->datastore)) { | ||
65 | throw new NotWritableDataStoreException($this->datastore); | ||
66 | } | ||
67 | |||
68 | // Note that gzinflate is faster than gzuncompress. | ||
69 | // See: http://www.php.net/manual/en/function.gzdeflate.php#96439 | ||
70 | $links = unserialize(gzinflate(base64_decode( | ||
71 | substr(file_get_contents($this->datastore), | ||
72 | strlen(self::$phpPrefix), -strlen(self::$phpSuffix))))); | ||
73 | |||
74 | if (empty($links)) { | ||
75 | if (filesize($this->datastore) > 100) { | ||
76 | throw new NotWritableDataStoreException($this->datastore); | ||
77 | } | ||
78 | throw new EmptyDataStoreException(); | ||
79 | } | ||
80 | |||
81 | return $links; | ||
82 | } | ||
83 | |||
84 | /** | ||
85 | * Saves the database from memory to disk | ||
86 | * | ||
87 | * @param BookmarkArray $links instance. | ||
88 | * | ||
89 | * @throws NotWritableDataStoreException the datastore is not writable | ||
90 | */ | ||
91 | public function write($links) | ||
92 | { | ||
93 | if (is_file($this->datastore) && !is_writeable($this->datastore)) { | ||
94 | // The datastore exists but is not writeable | ||
95 | throw new NotWritableDataStoreException($this->datastore); | ||
96 | } else if (!is_file($this->datastore) && !is_writeable(dirname($this->datastore))) { | ||
97 | // The datastore does not exist and its parent directory is not writeable | ||
98 | throw new NotWritableDataStoreException(dirname($this->datastore)); | ||
99 | } | ||
100 | |||
101 | file_put_contents( | ||
102 | $this->datastore, | ||
103 | self::$phpPrefix.base64_encode(gzdeflate(serialize($links))).self::$phpSuffix | ||
104 | ); | ||
105 | |||
106 | invalidateCaches($this->conf->get('resource.page_cache')); | ||
107 | } | ||
108 | } | ||
diff --git a/application/bookmark/BookmarkInitializer.php b/application/bookmark/BookmarkInitializer.php new file mode 100644 index 00000000..9eee9a35 --- /dev/null +++ b/application/bookmark/BookmarkInitializer.php | |||
@@ -0,0 +1,59 @@ | |||
1 | <?php | ||
2 | |||
3 | namespace Shaarli\Bookmark; | ||
4 | |||
5 | /** | ||
6 | * Class BookmarkInitializer | ||
7 | * | ||
8 | * This class is used to initialized default bookmarks after a fresh install of Shaarli. | ||
9 | * It is no longer call when the data store is empty, | ||
10 | * because user might want to delete default bookmarks after the install. | ||
11 | * | ||
12 | * To prevent data corruption, it does not overwrite existing bookmarks, | ||
13 | * even though there should not be any. | ||
14 | * | ||
15 | * @package Shaarli\Bookmark | ||
16 | */ | ||
17 | class BookmarkInitializer | ||
18 | { | ||
19 | /** @var BookmarkServiceInterface */ | ||
20 | protected $bookmarkService; | ||
21 | |||
22 | /** | ||
23 | * BookmarkInitializer constructor. | ||
24 | * | ||
25 | * @param BookmarkServiceInterface $bookmarkService | ||
26 | */ | ||
27 | public function __construct($bookmarkService) | ||
28 | { | ||
29 | $this->bookmarkService = $bookmarkService; | ||
30 | } | ||
31 | |||
32 | /** | ||
33 | * Initialize the data store with default bookmarks | ||
34 | */ | ||
35 | public function initialize() | ||
36 | { | ||
37 | $bookmark = new Bookmark(); | ||
38 | $bookmark->setTitle(t('My secret stuff... - Pastebin.com')); | ||
39 | $bookmark->setUrl('http://sebsauvage.net/paste/?8434b27936c09649#bR7XsXhoTiLcqCpQbmOpBi3rq2zzQUC5hBI7ZT1O3x8=', []); | ||
40 | $bookmark->setDescription(t('Shhhh! I\'m a private link only YOU can see. You can delete me too.')); | ||
41 | $bookmark->setTagsString('secretstuff'); | ||
42 | $bookmark->setPrivate(true); | ||
43 | $this->bookmarkService->add($bookmark); | ||
44 | |||
45 | $bookmark = new Bookmark(); | ||
46 | $bookmark->setTitle(t('The personal, minimalist, super-fast, database free, bookmarking service')); | ||
47 | $bookmark->setUrl('https://shaarli.readthedocs.io', []); | ||
48 | $bookmark->setDescription(t( | ||
49 | 'Welcome to Shaarli! This is your first public bookmark. ' | ||
50 | . 'To edit or delete me, you must first login. | ||
51 | |||
52 | To learn how to use Shaarli, consult the link "Documentation" at the bottom of this page. | ||
53 | |||
54 | You use the community supported version of the original Shaarli project, by Sebastien Sauvage.' | ||
55 | )); | ||
56 | $bookmark->setTagsString('opensource software'); | ||
57 | $this->bookmarkService->add($bookmark); | ||
58 | } | ||
59 | } | ||
diff --git a/application/bookmark/BookmarkServiceInterface.php b/application/bookmark/BookmarkServiceInterface.php new file mode 100644 index 00000000..7b7a4f09 --- /dev/null +++ b/application/bookmark/BookmarkServiceInterface.php | |||
@@ -0,0 +1,180 @@ | |||
1 | <?php | ||
2 | |||
3 | namespace Shaarli\Bookmark; | ||
4 | |||
5 | |||
6 | use Shaarli\Bookmark\Exception\BookmarkNotFoundException; | ||
7 | use Shaarli\Bookmark\Exception\NotWritableDataStoreException; | ||
8 | use Shaarli\Config\ConfigManager; | ||
9 | use Shaarli\Exceptions\IOException; | ||
10 | use Shaarli\History; | ||
11 | |||
12 | /** | ||
13 | * Class BookmarksService | ||
14 | * | ||
15 | * This is the entry point to manipulate the bookmark DB. | ||
16 | */ | ||
17 | interface BookmarkServiceInterface | ||
18 | { | ||
19 | /** | ||
20 | * BookmarksService constructor. | ||
21 | * | ||
22 | * @param ConfigManager $conf instance | ||
23 | * @param History $history instance | ||
24 | * @param bool $isLoggedIn true if the current user is logged in | ||
25 | */ | ||
26 | public function __construct(ConfigManager $conf, History $history, $isLoggedIn); | ||
27 | |||
28 | /** | ||
29 | * Find a bookmark by hash | ||
30 | * | ||
31 | * @param string $hash | ||
32 | * | ||
33 | * @return mixed | ||
34 | * | ||
35 | * @throws \Exception | ||
36 | */ | ||
37 | public function findByHash($hash); | ||
38 | |||
39 | /** | ||
40 | * @param $url | ||
41 | * | ||
42 | * @return Bookmark|null | ||
43 | */ | ||
44 | public function findByUrl($url); | ||
45 | |||
46 | /** | ||
47 | * Search bookmarks | ||
48 | * | ||
49 | * @param mixed $request | ||
50 | * @param string $visibility | ||
51 | * @param bool $caseSensitive | ||
52 | * @param bool $untaggedOnly | ||
53 | * | ||
54 | * @return Bookmark[] | ||
55 | */ | ||
56 | public function search($request = [], $visibility = null, $caseSensitive = false, $untaggedOnly = false); | ||
57 | |||
58 | /** | ||
59 | * Get a single bookmark by its ID. | ||
60 | * | ||
61 | * @param int $id Bookmark ID | ||
62 | * @param string $visibility all|public|private e.g. with public, accessing a private bookmark will throw an | ||
63 | * exception | ||
64 | * | ||
65 | * @return Bookmark | ||
66 | * | ||
67 | * @throws BookmarkNotFoundException | ||
68 | * @throws \Exception | ||
69 | */ | ||
70 | public function get($id, $visibility = null); | ||
71 | |||
72 | /** | ||
73 | * Updates an existing bookmark (depending on its ID). | ||
74 | * | ||
75 | * @param Bookmark $bookmark | ||
76 | * @param bool $save Writes to the datastore if set to true | ||
77 | * | ||
78 | * @return Bookmark Updated bookmark | ||
79 | * | ||
80 | * @throws BookmarkNotFoundException | ||
81 | * @throws \Exception | ||
82 | */ | ||
83 | public function set($bookmark, $save = true); | ||
84 | |||
85 | /** | ||
86 | * Adds a new bookmark (the ID must be empty). | ||
87 | * | ||
88 | * @param Bookmark $bookmark | ||
89 | * @param bool $save Writes to the datastore if set to true | ||
90 | * | ||
91 | * @return Bookmark new bookmark | ||
92 | * | ||
93 | * @throws \Exception | ||
94 | */ | ||
95 | public function add($bookmark, $save = true); | ||
96 | |||
97 | /** | ||
98 | * Adds or updates a bookmark depending on its ID: | ||
99 | * - a Bookmark without ID will be added | ||
100 | * - a Bookmark with an existing ID will be updated | ||
101 | * | ||
102 | * @param Bookmark $bookmark | ||
103 | * @param bool $save | ||
104 | * | ||
105 | * @return Bookmark | ||
106 | * | ||
107 | * @throws \Exception | ||
108 | */ | ||
109 | public function addOrSet($bookmark, $save = true); | ||
110 | |||
111 | /** | ||
112 | * Deletes a bookmark. | ||
113 | * | ||
114 | * @param Bookmark $bookmark | ||
115 | * @param bool $save | ||
116 | * | ||
117 | * @throws \Exception | ||
118 | */ | ||
119 | public function remove($bookmark, $save = true); | ||
120 | |||
121 | /** | ||
122 | * Get a single bookmark by its ID. | ||
123 | * | ||
124 | * @param int $id Bookmark ID | ||
125 | * @param string $visibility all|public|private e.g. with public, accessing a private bookmark will throw an | ||
126 | * exception | ||
127 | * | ||
128 | * @return bool | ||
129 | */ | ||
130 | public function exists($id, $visibility = null); | ||
131 | |||
132 | /** | ||
133 | * Return the number of available bookmarks for given visibility. | ||
134 | * | ||
135 | * @param string $visibility public|private|all | ||
136 | * | ||
137 | * @return int Number of bookmarks | ||
138 | */ | ||
139 | public function count($visibility = null); | ||
140 | |||
141 | /** | ||
142 | * Write the datastore. | ||
143 | * | ||
144 | * @throws NotWritableDataStoreException | ||
145 | */ | ||
146 | public function save(); | ||
147 | |||
148 | /** | ||
149 | * Returns the list tags appearing in the bookmarks with the given tags | ||
150 | * | ||
151 | * @param array $filteringTags tags selecting the bookmarks to consider | ||
152 | * @param string $visibility process only all/private/public bookmarks | ||
153 | * | ||
154 | * @return array tag => bookmarksCount | ||
155 | */ | ||
156 | public function bookmarksCountPerTag($filteringTags = [], $visibility = 'all'); | ||
157 | |||
158 | /** | ||
159 | * Returns the list of days containing articles (oldest first) | ||
160 | * | ||
161 | * @return array containing days (in format YYYYMMDD). | ||
162 | */ | ||
163 | public function days(); | ||
164 | |||
165 | /** | ||
166 | * Returns the list of articles for a given day. | ||
167 | * | ||
168 | * @param string $request day to filter. Format: YYYYMMDD. | ||
169 | * | ||
170 | * @return Bookmark[] list of shaare found. | ||
171 | * | ||
172 | * @throws BookmarkNotFoundException | ||
173 | */ | ||
174 | public function filterDay($request); | ||
175 | |||
176 | /** | ||
177 | * Creates the default database after a fresh install. | ||
178 | */ | ||
179 | public function initialize(); | ||
180 | } | ||
diff --git a/application/bookmark/LinkUtils.php b/application/bookmark/LinkUtils.php index 77eb2d95..88379430 100644 --- a/application/bookmark/LinkUtils.php +++ b/application/bookmark/LinkUtils.php | |||
@@ -1,6 +1,6 @@ | |||
1 | <?php | 1 | <?php |
2 | 2 | ||
3 | use Shaarli\Bookmark\LinkDB; | 3 | use Shaarli\Bookmark\Bookmark; |
4 | 4 | ||
5 | /** | 5 | /** |
6 | * Get cURL callback function for CURLOPT_WRITEFUNCTION | 6 | * Get cURL callback function for CURLOPT_WRITEFUNCTION |
@@ -188,30 +188,11 @@ function html_extract_tag($tag, $html) | |||
188 | } | 188 | } |
189 | 189 | ||
190 | /** | 190 | /** |
191 | * Count private links in given linklist. | 191 | * In a string, converts URLs to clickable bookmarks. |
192 | * | ||
193 | * @param array|Countable $links Linklist. | ||
194 | * | ||
195 | * @return int Number of private links. | ||
196 | */ | ||
197 | function count_private($links) | ||
198 | { | ||
199 | $cpt = 0; | ||
200 | foreach ($links as $link) { | ||
201 | if ($link['private']) { | ||
202 | $cpt += 1; | ||
203 | } | ||
204 | } | ||
205 | |||
206 | return $cpt; | ||
207 | } | ||
208 | |||
209 | /** | ||
210 | * In a string, converts URLs to clickable links. | ||
211 | * | 192 | * |
212 | * @param string $text input string. | 193 | * @param string $text input string. |
213 | * | 194 | * |
214 | * @return string returns $text with all links converted to HTML links. | 195 | * @return string returns $text with all bookmarks converted to HTML bookmarks. |
215 | * | 196 | * |
216 | * @see Function inspired from http://www.php.net/manual/en/function.preg-replace.php#85722 | 197 | * @see Function inspired from http://www.php.net/manual/en/function.preg-replace.php#85722 |
217 | */ | 198 | */ |
@@ -279,7 +260,7 @@ function format_description($description, $indexUrl = '') | |||
279 | */ | 260 | */ |
280 | function link_small_hash($date, $id) | 261 | function link_small_hash($date, $id) |
281 | { | 262 | { |
282 | return smallHash($date->format(LinkDB::LINK_DATE_FORMAT) . $id); | 263 | return smallHash($date->format(Bookmark::LINK_DATE_FORMAT) . $id); |
283 | } | 264 | } |
284 | 265 | ||
285 | /** | 266 | /** |
diff --git a/application/bookmark/exception/LinkNotFoundException.php b/application/bookmark/exception/BookmarkNotFoundException.php index f9414428..827a3d35 100644 --- a/application/bookmark/exception/LinkNotFoundException.php +++ b/application/bookmark/exception/BookmarkNotFoundException.php | |||
@@ -3,7 +3,7 @@ namespace Shaarli\Bookmark\Exception; | |||
3 | 3 | ||
4 | use Exception; | 4 | use Exception; |
5 | 5 | ||
6 | class LinkNotFoundException extends Exception | 6 | class BookmarkNotFoundException extends Exception |
7 | { | 7 | { |
8 | /** | 8 | /** |
9 | * LinkNotFoundException constructor. | 9 | * LinkNotFoundException constructor. |
diff --git a/application/bookmark/exception/EmptyDataStoreException.php b/application/bookmark/exception/EmptyDataStoreException.php new file mode 100644 index 00000000..cd48c1e6 --- /dev/null +++ b/application/bookmark/exception/EmptyDataStoreException.php | |||
@@ -0,0 +1,7 @@ | |||
1 | <?php | ||
2 | |||
3 | |||
4 | namespace Shaarli\Bookmark\Exception; | ||
5 | |||
6 | |||
7 | class EmptyDataStoreException extends \Exception {} | ||
diff --git a/application/bookmark/exception/InvalidBookmarkException.php b/application/bookmark/exception/InvalidBookmarkException.php new file mode 100644 index 00000000..10c84a6d --- /dev/null +++ b/application/bookmark/exception/InvalidBookmarkException.php | |||
@@ -0,0 +1,30 @@ | |||
1 | <?php | ||
2 | |||
3 | namespace Shaarli\Bookmark\Exception; | ||
4 | |||
5 | use Shaarli\Bookmark\Bookmark; | ||
6 | |||
7 | class InvalidBookmarkException extends \Exception | ||
8 | { | ||
9 | public function __construct($bookmark) | ||
10 | { | ||
11 | if ($bookmark instanceof Bookmark) { | ||
12 | if ($bookmark->getCreated() instanceof \DateTime) { | ||
13 | $created = $bookmark->getCreated()->format(\DateTime::ATOM); | ||
14 | } elseif (empty($bookmark->getCreated())) { | ||
15 | $created = ''; | ||
16 | } else { | ||
17 | $created = 'Not a DateTime object'; | ||
18 | } | ||
19 | $this->message = 'This bookmark is not valid'. PHP_EOL; | ||
20 | $this->message .= ' - ID: '. $bookmark->getId() . PHP_EOL; | ||
21 | $this->message .= ' - Title: '. $bookmark->getTitle() . PHP_EOL; | ||
22 | $this->message .= ' - Url: '. $bookmark->getUrl() . PHP_EOL; | ||
23 | $this->message .= ' - ShortUrl: '. $bookmark->getShortUrl() . PHP_EOL; | ||
24 | $this->message .= ' - Created: '. $created . PHP_EOL; | ||
25 | } else { | ||
26 | $this->message = 'The provided data is not a bookmark'. PHP_EOL; | ||
27 | $this->message .= var_export($bookmark, true); | ||
28 | } | ||
29 | } | ||
30 | } | ||
diff --git a/application/bookmark/exception/NotWritableDataStoreException.php b/application/bookmark/exception/NotWritableDataStoreException.php new file mode 100644 index 00000000..95f34b50 --- /dev/null +++ b/application/bookmark/exception/NotWritableDataStoreException.php | |||
@@ -0,0 +1,19 @@ | |||
1 | <?php | ||
2 | |||
3 | |||
4 | namespace Shaarli\Bookmark\Exception; | ||
5 | |||
6 | |||
7 | class NotWritableDataStoreException extends \Exception | ||
8 | { | ||
9 | /** | ||
10 | * NotReadableDataStore constructor. | ||
11 | * | ||
12 | * @param string $dataStore file path | ||
13 | */ | ||
14 | public function __construct($dataStore) | ||
15 | { | ||
16 | $this->message = 'Couldn\'t load data from the data store file "'. $dataStore .'". '. | ||
17 | 'Your data might be corrupted, or your file isn\'t readable.'; | ||
18 | } | ||
19 | } | ||
diff --git a/application/config/ConfigManager.php b/application/config/ConfigManager.php index c95e6800..e45bb4c3 100644 --- a/application/config/ConfigManager.php +++ b/application/config/ConfigManager.php | |||
@@ -389,6 +389,8 @@ class ConfigManager | |||
389 | $this->setEmpty('translation.extensions', []); | 389 | $this->setEmpty('translation.extensions', []); |
390 | 390 | ||
391 | $this->setEmpty('plugins', array()); | 391 | $this->setEmpty('plugins', array()); |
392 | |||
393 | $this->setEmpty('formatter', 'markdown'); | ||
392 | } | 394 | } |
393 | 395 | ||
394 | /** | 396 | /** |
diff --git a/application/feed/FeedBuilder.php b/application/feed/FeedBuilder.php index 957c8273..40bd4f15 100644 --- a/application/feed/FeedBuilder.php +++ b/application/feed/FeedBuilder.php | |||
@@ -2,6 +2,9 @@ | |||
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/BookmarkDefaultFormatter.php b/application/formatter/BookmarkDefaultFormatter.php new file mode 100644 index 00000000..7550c556 --- /dev/null +++ b/application/formatter/BookmarkDefaultFormatter.php | |||
@@ -0,0 +1,81 @@ | |||
1 | <?php | ||
2 | |||
3 | namespace Shaarli\Formatter; | ||
4 | |||
5 | /** | ||
6 | * Class BookmarkDefaultFormatter | ||
7 | * | ||
8 | * Default bookmark formatter. | ||
9 | * Escape values for HTML display and automatically add link to URL and hashtags. | ||
10 | * | ||
11 | * @package Shaarli\Formatter | ||
12 | */ | ||
13 | class BookmarkDefaultFormatter extends BookmarkFormatter | ||
14 | { | ||
15 | /** | ||
16 | * @inheritdoc | ||
17 | */ | ||
18 | public function formatTitle($bookmark) | ||
19 | { | ||
20 | return escape($bookmark->getTitle()); | ||
21 | } | ||
22 | |||
23 | /** | ||
24 | * @inheritdoc | ||
25 | */ | ||
26 | public function formatDescription($bookmark) | ||
27 | { | ||
28 | $indexUrl = ! empty($this->contextData['index_url']) ? $this->contextData['index_url'] : ''; | ||
29 | return format_description(escape($bookmark->getDescription()), $indexUrl); | ||
30 | } | ||
31 | |||
32 | /** | ||
33 | * @inheritdoc | ||
34 | */ | ||
35 | protected function formatTagList($bookmark) | ||
36 | { | ||
37 | return escape($bookmark->getTags()); | ||
38 | } | ||
39 | |||
40 | /** | ||
41 | * @inheritdoc | ||
42 | */ | ||
43 | public function formatTagString($bookmark) | ||
44 | { | ||
45 | return implode(' ', $this->formatTagList($bookmark)); | ||
46 | } | ||
47 | |||
48 | /** | ||
49 | * @inheritdoc | ||
50 | */ | ||
51 | public function formatUrl($bookmark) | ||
52 | { | ||
53 | if (! empty($this->contextData['index_url']) && ( | ||
54 | startsWith($bookmark->getUrl(), '?') || startsWith($bookmark->getUrl(), '/') | ||
55 | )) { | ||
56 | return $this->contextData['index_url'] . escape($bookmark->getUrl()); | ||
57 | } | ||
58 | return escape($bookmark->getUrl()); | ||
59 | } | ||
60 | |||
61 | /** | ||
62 | * @inheritdoc | ||
63 | */ | ||
64 | protected function formatRealUrl($bookmark) | ||
65 | { | ||
66 | if (! empty($this->contextData['index_url']) && ( | ||
67 | startsWith($bookmark->getUrl(), '?') || startsWith($bookmark->getUrl(), '/') | ||
68 | )) { | ||
69 | return $this->contextData['index_url'] . escape($bookmark->getUrl()); | ||
70 | } | ||
71 | return escape($bookmark->getUrl()); | ||
72 | } | ||
73 | |||
74 | /** | ||
75 | * @inheritdoc | ||
76 | */ | ||
77 | protected function formatThumbnail($bookmark) | ||
78 | { | ||
79 | return escape($bookmark->getThumbnail()); | ||
80 | } | ||
81 | } | ||
diff --git a/application/formatter/BookmarkFormatter.php b/application/formatter/BookmarkFormatter.php new file mode 100644 index 00000000..c82c3452 --- /dev/null +++ b/application/formatter/BookmarkFormatter.php | |||
@@ -0,0 +1,256 @@ | |||
1 | <?php | ||
2 | |||
3 | namespace Shaarli\Formatter; | ||
4 | |||
5 | use DateTime; | ||
6 | use Shaarli\Config\ConfigManager; | ||
7 | use Shaarli\Bookmark\Bookmark; | ||
8 | |||
9 | /** | ||
10 | * Class BookmarkFormatter | ||
11 | * | ||
12 | * Abstract class processing all bookmark attributes through methods designed to be overridden. | ||
13 | * | ||
14 | * @package Shaarli\Formatter | ||
15 | */ | ||
16 | abstract class BookmarkFormatter | ||
17 | { | ||
18 | /** | ||
19 | * @var ConfigManager | ||
20 | */ | ||
21 | protected $conf; | ||
22 | |||
23 | /** | ||
24 | * @var array Additional parameters than can be used for specific formatting | ||
25 | * e.g. index_url for Feed formatting | ||
26 | */ | ||
27 | protected $contextData = []; | ||
28 | |||
29 | /** | ||
30 | * LinkDefaultFormatter constructor. | ||
31 | * @param ConfigManager $conf | ||
32 | */ | ||
33 | public function __construct(ConfigManager $conf) | ||
34 | { | ||
35 | $this->conf = $conf; | ||
36 | } | ||
37 | |||
38 | /** | ||
39 | * Convert a Bookmark into an array usable by templates and plugins. | ||
40 | * | ||
41 | * All Bookmark attributes are formatted through a format method | ||
42 | * that can be overridden in a formatter extending this class. | ||
43 | * | ||
44 | * @param Bookmark $bookmark instance | ||
45 | * | ||
46 | * @return array formatted representation of a Bookmark | ||
47 | */ | ||
48 | public function format($bookmark) | ||
49 | { | ||
50 | $out['id'] = $this->formatId($bookmark); | ||
51 | $out['shorturl'] = $this->formatShortUrl($bookmark); | ||
52 | $out['url'] = $this->formatUrl($bookmark); | ||
53 | $out['real_url'] = $this->formatRealUrl($bookmark); | ||
54 | $out['title'] = $this->formatTitle($bookmark); | ||
55 | $out['description'] = $this->formatDescription($bookmark); | ||
56 | $out['thumbnail'] = $this->formatThumbnail($bookmark); | ||
57 | $out['taglist'] = $this->formatTagList($bookmark); | ||
58 | $out['tags'] = $this->formatTagString($bookmark); | ||
59 | $out['sticky'] = $bookmark->isSticky(); | ||
60 | $out['private'] = $bookmark->isPrivate(); | ||
61 | $out['class'] = $this->formatClass($bookmark); | ||
62 | $out['created'] = $this->formatCreated($bookmark); | ||
63 | $out['updated'] = $this->formatUpdated($bookmark); | ||
64 | $out['timestamp'] = $this->formatCreatedTimestamp($bookmark); | ||
65 | $out['updated_timestamp'] = $this->formatUpdatedTimestamp($bookmark); | ||
66 | return $out; | ||
67 | } | ||
68 | |||
69 | /** | ||
70 | * Add additional data available to formatters. | ||
71 | * This is used for example to add `index_url` in description's links. | ||
72 | * | ||
73 | * @param string $key Context data key | ||
74 | * @param string $value Context data value | ||
75 | */ | ||
76 | public function addContextData($key, $value) | ||
77 | { | ||
78 | $this->contextData[$key] = $value; | ||
79 | } | ||
80 | |||
81 | /** | ||
82 | * Format ID | ||
83 | * | ||
84 | * @param Bookmark $bookmark instance | ||
85 | * | ||
86 | * @return int formatted ID | ||
87 | */ | ||
88 | protected function formatId($bookmark) | ||
89 | { | ||
90 | return $bookmark->getId(); | ||
91 | } | ||
92 | |||
93 | /** | ||
94 | * Format ShortUrl | ||
95 | * | ||
96 | * @param Bookmark $bookmark instance | ||
97 | * | ||
98 | * @return string formatted ShortUrl | ||
99 | */ | ||
100 | protected function formatShortUrl($bookmark) | ||
101 | { | ||
102 | return $bookmark->getShortUrl(); | ||
103 | } | ||
104 | |||
105 | /** | ||
106 | * Format Url | ||
107 | * | ||
108 | * @param Bookmark $bookmark instance | ||
109 | * | ||
110 | * @return string formatted Url | ||
111 | */ | ||
112 | protected function formatUrl($bookmark) | ||
113 | { | ||
114 | return $bookmark->getUrl(); | ||
115 | } | ||
116 | |||
117 | /** | ||
118 | * Format RealUrl | ||
119 | * Legacy: identical to Url | ||
120 | * | ||
121 | * @param Bookmark $bookmark instance | ||
122 | * | ||
123 | * @return string formatted RealUrl | ||
124 | */ | ||
125 | protected function formatRealUrl($bookmark) | ||
126 | { | ||
127 | return $bookmark->getUrl(); | ||
128 | } | ||
129 | |||
130 | /** | ||
131 | * Format Title | ||
132 | * | ||
133 | * @param Bookmark $bookmark instance | ||
134 | * | ||
135 | * @return string formatted Title | ||
136 | */ | ||
137 | protected function formatTitle($bookmark) | ||
138 | { | ||
139 | return $bookmark->getTitle(); | ||
140 | } | ||
141 | |||
142 | /** | ||
143 | * Format Description | ||
144 | * | ||
145 | * @param Bookmark $bookmark instance | ||
146 | * | ||
147 | * @return string formatted Description | ||
148 | */ | ||
149 | protected function formatDescription($bookmark) | ||
150 | { | ||
151 | return $bookmark->getDescription(); | ||
152 | } | ||
153 | |||
154 | /** | ||
155 | * Format Thumbnail | ||
156 | * | ||
157 | * @param Bookmark $bookmark instance | ||
158 | * | ||
159 | * @return string formatted Thumbnail | ||
160 | */ | ||
161 | protected function formatThumbnail($bookmark) | ||
162 | { | ||
163 | return $bookmark->getThumbnail(); | ||
164 | } | ||
165 | |||
166 | /** | ||
167 | * Format Tags | ||
168 | * | ||
169 | * @param Bookmark $bookmark instance | ||
170 | * | ||
171 | * @return array formatted Tags | ||
172 | */ | ||
173 | protected function formatTagList($bookmark) | ||
174 | { | ||
175 | return $bookmark->getTags(); | ||
176 | } | ||
177 | |||
178 | /** | ||
179 | * Format TagString | ||
180 | * | ||
181 | * @param Bookmark $bookmark instance | ||
182 | * | ||
183 | * @return string formatted TagString | ||
184 | */ | ||
185 | protected function formatTagString($bookmark) | ||
186 | { | ||
187 | return implode(' ', $bookmark->getTags()); | ||
188 | } | ||
189 | |||
190 | /** | ||
191 | * Format Class | ||
192 | * Used to add specific CSS class for a link | ||
193 | * | ||
194 | * @param Bookmark $bookmark instance | ||
195 | * | ||
196 | * @return string formatted Class | ||
197 | */ | ||
198 | protected function formatClass($bookmark) | ||
199 | { | ||
200 | return $bookmark->isPrivate() ? 'private' : ''; | ||
201 | } | ||
202 | |||
203 | /** | ||
204 | * Format Created | ||
205 | * | ||
206 | * @param Bookmark $bookmark instance | ||
207 | * | ||
208 | * @return DateTime instance | ||
209 | */ | ||
210 | protected function formatCreated(Bookmark $bookmark) | ||
211 | { | ||
212 | return $bookmark->getCreated(); | ||
213 | } | ||
214 | |||
215 | /** | ||
216 | * Format Updated | ||
217 | * | ||
218 | * @param Bookmark $bookmark instance | ||
219 | * | ||
220 | * @return DateTime instance | ||
221 | */ | ||
222 | protected function formatUpdated(Bookmark $bookmark) | ||
223 | { | ||
224 | return $bookmark->getUpdated(); | ||
225 | } | ||
226 | |||
227 | /** | ||
228 | * Format CreatedTimestamp | ||
229 | * | ||
230 | * @param Bookmark $bookmark instance | ||
231 | * | ||
232 | * @return int formatted CreatedTimestamp | ||
233 | */ | ||
234 | protected function formatCreatedTimestamp(Bookmark $bookmark) | ||
235 | { | ||
236 | if (! empty($bookmark->getCreated())) { | ||
237 | return $bookmark->getCreated()->getTimestamp(); | ||
238 | } | ||
239 | return 0; | ||
240 | } | ||
241 | |||
242 | /** | ||
243 | * Format UpdatedTimestamp | ||
244 | * | ||
245 | * @param Bookmark $bookmark instance | ||
246 | * | ||
247 | * @return int formatted UpdatedTimestamp | ||
248 | */ | ||
249 | protected function formatUpdatedTimestamp(Bookmark $bookmark) | ||
250 | { | ||
251 | if (! empty($bookmark->getUpdated())) { | ||
252 | return $bookmark->getUpdated()->getTimestamp(); | ||
253 | } | ||
254 | return 0; | ||
255 | } | ||
256 | } | ||
diff --git a/application/formatter/BookmarkMarkdownFormatter.php b/application/formatter/BookmarkMarkdownFormatter.php new file mode 100644 index 00000000..7797bfbf --- /dev/null +++ b/application/formatter/BookmarkMarkdownFormatter.php | |||
@@ -0,0 +1,204 @@ | |||
1 | <?php | ||
2 | |||
3 | namespace Shaarli\Formatter; | ||
4 | |||
5 | use Shaarli\Config\ConfigManager; | ||
6 | |||
7 | /** | ||
8 | * Class BookmarkMarkdownFormatter | ||
9 | * | ||
10 | * Format bookmark description into Markdown format. | ||
11 | * | ||
12 | * @package Shaarli\Formatter | ||
13 | */ | ||
14 | class BookmarkMarkdownFormatter extends BookmarkDefaultFormatter | ||
15 | { | ||
16 | /** | ||
17 | * When this tag is present in a bookmark, its description should not be processed with Markdown | ||
18 | */ | ||
19 | const NO_MD_TAG = 'nomarkdown'; | ||
20 | |||
21 | /** @var \Parsedown instance */ | ||
22 | protected $parsedown; | ||
23 | |||
24 | /** @var bool used to escape HTML in Markdown or not. | ||
25 | * It MUST be set to true for shared instance as HTML content can | ||
26 | * introduce XSS vulnerabilities. | ||
27 | */ | ||
28 | protected $escape; | ||
29 | |||
30 | /** | ||
31 | * @var array List of allowed protocols for links inside bookmark's description. | ||
32 | */ | ||
33 | protected $allowedProtocols; | ||
34 | |||
35 | /** | ||
36 | * LinkMarkdownFormatter constructor. | ||
37 | * | ||
38 | * @param ConfigManager $conf instance | ||
39 | */ | ||
40 | public function __construct(ConfigManager $conf) | ||
41 | { | ||
42 | parent::__construct($conf); | ||
43 | $this->parsedown = new \Parsedown(); | ||
44 | $this->escape = $conf->get('security.markdown_escape', true); | ||
45 | $this->allowedProtocols = $conf->get('security.allowed_protocols', []); | ||
46 | } | ||
47 | |||
48 | /** | ||
49 | * @inheritdoc | ||
50 | */ | ||
51 | public function formatDescription($bookmark) | ||
52 | { | ||
53 | if (in_array(self::NO_MD_TAG, $bookmark->getTags())) { | ||
54 | return parent::formatDescription($bookmark); | ||
55 | } | ||
56 | |||
57 | $processedDescription = $bookmark->getDescription(); | ||
58 | $processedDescription = $this->filterProtocols($processedDescription); | ||
59 | $processedDescription = $this->formatHashTags($processedDescription); | ||
60 | $processedDescription = $this->reverseEscapedHtml($processedDescription); | ||
61 | $processedDescription = $this->parsedown | ||
62 | ->setMarkupEscaped($this->escape) | ||
63 | ->setBreaksEnabled(true) | ||
64 | ->text($processedDescription); | ||
65 | $processedDescription = $this->sanitizeHtml($processedDescription); | ||
66 | |||
67 | if (!empty($processedDescription)) { | ||
68 | $processedDescription = '<div class="markdown">'. $processedDescription . '</div>'; | ||
69 | } | ||
70 | |||
71 | return $processedDescription; | ||
72 | } | ||
73 | |||
74 | /** | ||
75 | * Remove the NO markdown tag if it is present | ||
76 | * | ||
77 | * @inheritdoc | ||
78 | */ | ||
79 | protected function formatTagList($bookmark) | ||
80 | { | ||
81 | $out = parent::formatTagList($bookmark); | ||
82 | if (($pos = array_search(self::NO_MD_TAG, $out)) !== false) { | ||
83 | unset($out[$pos]); | ||
84 | return array_values($out); | ||
85 | } | ||
86 | return $out; | ||
87 | } | ||
88 | |||
89 | /** | ||
90 | * Replace not whitelisted protocols with http:// in given description. | ||
91 | * Also adds `index_url` to relative links if it's specified | ||
92 | * | ||
93 | * @param string $description input description text. | ||
94 | * | ||
95 | * @return string $description without malicious link. | ||
96 | */ | ||
97 | protected function filterProtocols($description) | ||
98 | { | ||
99 | $allowedProtocols = $this->allowedProtocols; | ||
100 | $indexUrl = ! empty($this->contextData['index_url']) ? $this->contextData['index_url'] : ''; | ||
101 | |||
102 | return preg_replace_callback( | ||
103 | '#]\((.*?)\)#is', | ||
104 | function ($match) use ($allowedProtocols, $indexUrl) { | ||
105 | $link = startsWith($match[1], '?') || startsWith($match[1], '/') ? $indexUrl : ''; | ||
106 | $link .= whitelist_protocols($match[1], $allowedProtocols); | ||
107 | return ']('. $link.')'; | ||
108 | }, | ||
109 | $description | ||
110 | ); | ||
111 | } | ||
112 | |||
113 | /** | ||
114 | * Replace hashtag in Markdown links format | ||
115 | * E.g. `#hashtag` becomes `[#hashtag](?addtag=hashtag)` | ||
116 | * It includes the index URL if specified. | ||
117 | * | ||
118 | * @param string $description | ||
119 | * | ||
120 | * @return string | ||
121 | */ | ||
122 | protected function formatHashTags($description) | ||
123 | { | ||
124 | $indexUrl = ! empty($this->contextData['index_url']) ? $this->contextData['index_url'] : ''; | ||
125 | |||
126 | /* | ||
127 | * To support unicode: http://stackoverflow.com/a/35498078/1484919 | ||
128 | * \p{Pc} - to match underscore | ||
129 | * \p{N} - numeric character in any script | ||
130 | * \p{L} - letter from any language | ||
131 | * \p{Mn} - any non marking space (accents, umlauts, etc) | ||
132 | */ | ||
133 | $regex = '/(^|\s)#([\p{Pc}\p{N}\p{L}\p{Mn}]+)/mui'; | ||
134 | $replacement = '$1[#$2]('. $indexUrl .'?addtag=$2)'; | ||
135 | |||
136 | $descriptionLines = explode(PHP_EOL, $description); | ||
137 | $descriptionOut = ''; | ||
138 | $codeBlockOn = false; | ||
139 | $lineCount = 0; | ||
140 | |||
141 | foreach ($descriptionLines as $descriptionLine) { | ||
142 | // Detect line of code: starting with 4 spaces, | ||
143 | // except lists which can start with +/*/- or `2.` after spaces. | ||
144 | $codeLineOn = preg_match('/^ +(?=[^\+\*\-])(?=(?!\d\.).)/', $descriptionLine) > 0; | ||
145 | // Detect and toggle block of code | ||
146 | if (!$codeBlockOn) { | ||
147 | $codeBlockOn = preg_match('/^```/', $descriptionLine) > 0; | ||
148 | } elseif (preg_match('/^```/', $descriptionLine) > 0) { | ||
149 | $codeBlockOn = false; | ||
150 | } | ||
151 | |||
152 | if (!$codeBlockOn && !$codeLineOn) { | ||
153 | $descriptionLine = preg_replace($regex, $replacement, $descriptionLine); | ||
154 | } | ||
155 | |||
156 | $descriptionOut .= $descriptionLine; | ||
157 | if ($lineCount++ < count($descriptionLines) - 1) { | ||
158 | $descriptionOut .= PHP_EOL; | ||
159 | } | ||
160 | } | ||
161 | |||
162 | return $descriptionOut; | ||
163 | } | ||
164 | |||
165 | /** | ||
166 | * Remove dangerous HTML tags (tags, iframe, etc.). | ||
167 | * Doesn't affect <code> content (already escaped by Parsedown). | ||
168 | * | ||
169 | * @param string $description input description text. | ||
170 | * | ||
171 | * @return string given string escaped. | ||
172 | */ | ||
173 | protected function sanitizeHtml($description) | ||
174 | { | ||
175 | $escapeTags = array( | ||
176 | 'script', | ||
177 | 'style', | ||
178 | 'link', | ||
179 | 'iframe', | ||
180 | 'frameset', | ||
181 | 'frame', | ||
182 | ); | ||
183 | foreach ($escapeTags as $tag) { | ||
184 | $description = preg_replace_callback( | ||
185 | '#<\s*'. $tag .'[^>]*>(.*</\s*'. $tag .'[^>]*>)?#is', | ||
186 | function ($match) { | ||
187 | return escape($match[0]); | ||
188 | }, | ||
189 | $description | ||
190 | ); | ||
191 | } | ||
192 | $description = preg_replace( | ||
193 | '#(<[^>]+\s)on[a-z]*="?[^ "]*"?#is', | ||
194 | '$1', | ||
195 | $description | ||
196 | ); | ||
197 | return $description; | ||
198 | } | ||
199 | |||
200 | protected function reverseEscapedHtml($description) | ||
201 | { | ||
202 | return unescape($description); | ||
203 | } | ||
204 | } | ||
diff --git a/application/formatter/BookmarkRawFormatter.php b/application/formatter/BookmarkRawFormatter.php new file mode 100644 index 00000000..bc372273 --- /dev/null +++ b/application/formatter/BookmarkRawFormatter.php | |||
@@ -0,0 +1,13 @@ | |||
1 | <?php | ||
2 | |||
3 | namespace Shaarli\Formatter; | ||
4 | |||
5 | /** | ||
6 | * Class BookmarkRawFormatter | ||
7 | * | ||
8 | * Used to retrieve bookmarks as array with raw values. | ||
9 | * Warning: Do NOT use this for HTML content as it can introduce XSS vulnerabilities. | ||
10 | * | ||
11 | * @package Shaarli\Formatter | ||
12 | */ | ||
13 | class BookmarkRawFormatter extends BookmarkFormatter {} | ||
diff --git a/application/formatter/FormatterFactory.php b/application/formatter/FormatterFactory.php new file mode 100644 index 00000000..0d2c0466 --- /dev/null +++ b/application/formatter/FormatterFactory.php | |||
@@ -0,0 +1,46 @@ | |||
1 | <?php | ||
2 | |||
3 | namespace Shaarli\Formatter; | ||
4 | |||
5 | use Shaarli\Config\ConfigManager; | ||
6 | |||
7 | /** | ||
8 | * Class FormatterFactory | ||
9 | * | ||
10 | * Helper class used to instantiate the proper BookmarkFormatter. | ||
11 | * | ||
12 | * @package Shaarli\Formatter | ||
13 | */ | ||
14 | class FormatterFactory | ||
15 | { | ||
16 | /** @var ConfigManager instance */ | ||
17 | protected $conf; | ||
18 | |||
19 | /** | ||
20 | * FormatterFactory constructor. | ||
21 | * | ||
22 | * @param ConfigManager $conf | ||
23 | */ | ||
24 | public function __construct(ConfigManager $conf) | ||
25 | { | ||
26 | $this->conf = $conf; | ||
27 | } | ||
28 | |||
29 | /** | ||
30 | * Instanciate a BookmarkFormatter depending on the configuration or provided formatter type. | ||
31 | * | ||
32 | * @param string|null $type force a specific type regardless of the configuration | ||
33 | * | ||
34 | * @return BookmarkFormatter instance. | ||
35 | */ | ||
36 | public function getFormatter($type = null) | ||
37 | { | ||
38 | $type = $type ? $type : $this->conf->get('formatter', 'default'); | ||
39 | $className = '\\Shaarli\\Formatter\\Bookmark'. ucfirst($type) .'Formatter'; | ||
40 | if (!class_exists($className)) { | ||
41 | $className = '\\Shaarli\\Formatter\\BookmarkDefaultFormatter'; | ||
42 | } | ||
43 | |||
44 | return new $className($this->conf); | ||
45 | } | ||
46 | } | ||
diff --git a/application/bookmark/LinkDB.php b/application/legacy/LegacyLinkDB.php index f01c7ee6..7ccf5e54 100644 --- a/application/bookmark/LinkDB.php +++ b/application/legacy/LegacyLinkDB.php | |||
@@ -1,17 +1,17 @@ | |||
1 | <?php | 1 | <?php |
2 | 2 | ||
3 | namespace Shaarli\Bookmark; | 3 | namespace Shaarli\Legacy; |
4 | 4 | ||
5 | use ArrayAccess; | 5 | use ArrayAccess; |
6 | use Countable; | 6 | use Countable; |
7 | use DateTime; | 7 | use DateTime; |
8 | use Iterator; | 8 | use Iterator; |
9 | use Shaarli\Bookmark\Exception\LinkNotFoundException; | 9 | use Shaarli\Bookmark\Exception\BookmarkNotFoundException; |
10 | use Shaarli\Exceptions\IOException; | 10 | use Shaarli\Exceptions\IOException; |
11 | use Shaarli\FileUtils; | 11 | use Shaarli\FileUtils; |
12 | 12 | ||
13 | /** | 13 | /** |
14 | * Data storage for links. | 14 | * Data storage for bookmarks. |
15 | * | 15 | * |
16 | * This object behaves like an associative array. | 16 | * This object behaves like an associative array. |
17 | * | 17 | * |
@@ -29,8 +29,8 @@ use Shaarli\FileUtils; | |||
29 | * - private: Is this link private? 0=no, other value=yes | 29 | * - private: Is this link private? 0=no, other value=yes |
30 | * - tags: tags attached to this entry (separated by spaces) | 30 | * - tags: tags attached to this entry (separated by spaces) |
31 | * - title Title of the link | 31 | * - title Title of the link |
32 | * - url URL of the link. Used for displayable links. | 32 | * - url URL of the link. Used for displayable bookmarks. |
33 | * Can be absolute or relative in the database but the relative links | 33 | * Can be absolute or relative in the database but the relative bookmarks |
34 | * will be converted to absolute ones in templates. | 34 | * will be converted to absolute ones in templates. |
35 | * - real_url Raw URL in stored in the DB (absolute or relative). | 35 | * - real_url Raw URL in stored in the DB (absolute or relative). |
36 | * - shorturl Permalink smallhash | 36 | * - shorturl Permalink smallhash |
@@ -49,11 +49,13 @@ use Shaarli\FileUtils; | |||
49 | * Example: | 49 | * Example: |
50 | * - DB: link #1 (2010-01-01) link #2 (2016-01-01) | 50 | * - DB: link #1 (2010-01-01) link #2 (2016-01-01) |
51 | * - Order: #2 #1 | 51 | * - Order: #2 #1 |
52 | * - Import links containing: link #3 (2013-01-01) | 52 | * - Import bookmarks containing: link #3 (2013-01-01) |
53 | * - New DB: link #1 (2010-01-01) link #2 (2016-01-01) link #3 (2013-01-01) | 53 | * - New DB: link #1 (2010-01-01) link #2 (2016-01-01) link #3 (2013-01-01) |
54 | * - Real order: #2 #3 #1 | 54 | * - Real order: #2 #3 #1 |
55 | * | ||
56 | * @deprecated | ||
55 | */ | 57 | */ |
56 | class LinkDB implements Iterator, Countable, ArrayAccess | 58 | class LegacyLinkDB implements Iterator, Countable, ArrayAccess |
57 | { | 59 | { |
58 | // Links are stored as a PHP serialized string | 60 | // Links are stored as a PHP serialized string |
59 | private $datastore; | 61 | private $datastore; |
@@ -61,7 +63,7 @@ class LinkDB implements Iterator, Countable, ArrayAccess | |||
61 | // Link date storage format | 63 | // Link date storage format |
62 | const LINK_DATE_FORMAT = 'Ymd_His'; | 64 | const LINK_DATE_FORMAT = 'Ymd_His'; |
63 | 65 | ||
64 | // List of links (associative array) | 66 | // List of bookmarks (associative array) |
65 | // - key: link date (e.g. "20110823_124546"), | 67 | // - key: link date (e.g. "20110823_124546"), |
66 | // - value: associative array (keys: title, description...) | 68 | // - value: associative array (keys: title, description...) |
67 | private $links; | 69 | private $links; |
@@ -71,7 +73,7 @@ class LinkDB implements Iterator, Countable, ArrayAccess | |||
71 | private $urls; | 73 | private $urls; |
72 | 74 | ||
73 | /** | 75 | /** |
74 | * @var array List of all links IDS mapped with their array offset. | 76 | * @var array List of all bookmarks IDS mapped with their array offset. |
75 | * Map: id->offset. | 77 | * Map: id->offset. |
76 | */ | 78 | */ |
77 | protected $ids; | 79 | protected $ids; |
@@ -82,10 +84,10 @@ class LinkDB implements Iterator, Countable, ArrayAccess | |||
82 | // Position in the $this->keys array (for the Iterator interface) | 84 | // Position in the $this->keys array (for the Iterator interface) |
83 | private $position; | 85 | private $position; |
84 | 86 | ||
85 | // Is the user logged in? (used to filter private links) | 87 | // Is the user logged in? (used to filter private bookmarks) |
86 | private $loggedIn; | 88 | private $loggedIn; |
87 | 89 | ||
88 | // Hide public links | 90 | // Hide public bookmarks |
89 | private $hidePublicLinks; | 91 | private $hidePublicLinks; |
90 | 92 | ||
91 | /** | 93 | /** |
@@ -95,7 +97,7 @@ class LinkDB implements Iterator, Countable, ArrayAccess | |||
95 | * | 97 | * |
96 | * @param string $datastore datastore file path. | 98 | * @param string $datastore datastore file path. |
97 | * @param boolean $isLoggedIn is the user logged in? | 99 | * @param boolean $isLoggedIn is the user logged in? |
98 | * @param boolean $hidePublicLinks if true all links are private. | 100 | * @param boolean $hidePublicLinks if true all bookmarks are private. |
99 | */ | 101 | */ |
100 | public function __construct( | 102 | public function __construct( |
101 | $datastore, | 103 | $datastore, |
@@ -280,7 +282,7 @@ You use the community supported version of the original Shaarli project, by Seba | |||
280 | */ | 282 | */ |
281 | private function read() | 283 | private function read() |
282 | { | 284 | { |
283 | // Public links are hidden and user not logged in => nothing to show | 285 | // Public bookmarks are hidden and user not logged in => nothing to show |
284 | if ($this->hidePublicLinks && !$this->loggedIn) { | 286 | if ($this->hidePublicLinks && !$this->loggedIn) { |
285 | $this->links = array(); | 287 | $this->links = array(); |
286 | return; | 288 | return; |
@@ -310,7 +312,7 @@ You use the community supported version of the original Shaarli project, by Seba | |||
310 | 312 | ||
311 | $link['sticky'] = isset($link['sticky']) ? $link['sticky'] : false; | 313 | $link['sticky'] = isset($link['sticky']) ? $link['sticky'] : false; |
312 | 314 | ||
313 | // To be able to load links before running the update, and prepare the update | 315 | // To be able to load bookmarks before running the update, and prepare the update |
314 | if (!isset($link['created'])) { | 316 | if (!isset($link['created'])) { |
315 | $link['id'] = $link['linkdate']; | 317 | $link['id'] = $link['linkdate']; |
316 | $link['created'] = DateTime::createFromFormat(self::LINK_DATE_FORMAT, $link['linkdate']); | 318 | $link['created'] = DateTime::createFromFormat(self::LINK_DATE_FORMAT, $link['linkdate']); |
@@ -375,13 +377,13 @@ You use the community supported version of the original Shaarli project, by Seba | |||
375 | * | 377 | * |
376 | * @return array $filtered array containing permalink data. | 378 | * @return array $filtered array containing permalink data. |
377 | * | 379 | * |
378 | * @throws LinkNotFoundException if the smallhash is malformed or doesn't match any link. | 380 | * @throws BookmarkNotFoundException if the smallhash is malformed or doesn't match any link. |
379 | */ | 381 | */ |
380 | public function filterHash($request) | 382 | public function filterHash($request) |
381 | { | 383 | { |
382 | $request = substr($request, 0, 6); | 384 | $request = substr($request, 0, 6); |
383 | $linkFilter = new LinkFilter($this->links); | 385 | $linkFilter = new LegacyLinkFilter($this->links); |
384 | return $linkFilter->filter(LinkFilter::$FILTER_HASH, $request); | 386 | return $linkFilter->filter(LegacyLinkFilter::$FILTER_HASH, $request); |
385 | } | 387 | } |
386 | 388 | ||
387 | /** | 389 | /** |
@@ -393,21 +395,21 @@ You use the community supported version of the original Shaarli project, by Seba | |||
393 | */ | 395 | */ |
394 | public function filterDay($request) | 396 | public function filterDay($request) |
395 | { | 397 | { |
396 | $linkFilter = new LinkFilter($this->links); | 398 | $linkFilter = new LegacyLinkFilter($this->links); |
397 | return $linkFilter->filter(LinkFilter::$FILTER_DAY, $request); | 399 | return $linkFilter->filter(LegacyLinkFilter::$FILTER_DAY, $request); |
398 | } | 400 | } |
399 | 401 | ||
400 | /** | 402 | /** |
401 | * Filter links according to search parameters. | 403 | * Filter bookmarks according to search parameters. |
402 | * | 404 | * |
403 | * @param array $filterRequest Search request content. Supported keys: | 405 | * @param array $filterRequest Search request content. Supported keys: |
404 | * - searchtags: list of tags | 406 | * - searchtags: list of tags |
405 | * - searchterm: term search | 407 | * - searchterm: term search |
406 | * @param bool $casesensitive Optional: Perform case sensitive filter | 408 | * @param bool $casesensitive Optional: Perform case sensitive filter |
407 | * @param string $visibility return only all/private/public links | 409 | * @param string $visibility return only all/private/public bookmarks |
408 | * @param bool $untaggedonly return only untagged links | 410 | * @param bool $untaggedonly return only untagged bookmarks |
409 | * | 411 | * |
410 | * @return array filtered links, all links if no suitable filter was provided. | 412 | * @return array filtered bookmarks, all bookmarks if no suitable filter was provided. |
411 | */ | 413 | */ |
412 | public function filterSearch( | 414 | public function filterSearch( |
413 | $filterRequest = array(), | 415 | $filterRequest = array(), |
@@ -420,19 +422,19 @@ You use the community supported version of the original Shaarli project, by Seba | |||
420 | $searchtags = isset($filterRequest['searchtags']) ? escape($filterRequest['searchtags']) : ''; | 422 | $searchtags = isset($filterRequest['searchtags']) ? escape($filterRequest['searchtags']) : ''; |
421 | $searchterm = isset($filterRequest['searchterm']) ? escape($filterRequest['searchterm']) : ''; | 423 | $searchterm = isset($filterRequest['searchterm']) ? escape($filterRequest['searchterm']) : ''; |
422 | 424 | ||
423 | // Search tags + fullsearch - blank string parameter will return all links. | 425 | // Search tags + fullsearch - blank string parameter will return all bookmarks. |
424 | $type = LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT; // == "vuotext" | 426 | $type = LegacyLinkFilter::$FILTER_TAG | LegacyLinkFilter::$FILTER_TEXT; // == "vuotext" |
425 | $request = [$searchtags, $searchterm]; | 427 | $request = [$searchtags, $searchterm]; |
426 | 428 | ||
427 | $linkFilter = new LinkFilter($this); | 429 | $linkFilter = new LegacyLinkFilter($this); |
428 | return $linkFilter->filter($type, $request, $casesensitive, $visibility, $untaggedonly); | 430 | return $linkFilter->filter($type, $request, $casesensitive, $visibility, $untaggedonly); |
429 | } | 431 | } |
430 | 432 | ||
431 | /** | 433 | /** |
432 | * Returns the list tags appearing in the links with the given tags | 434 | * Returns the list tags appearing in the bookmarks with the given tags |
433 | * | 435 | * |
434 | * @param array $filteringTags tags selecting the links to consider | 436 | * @param array $filteringTags tags selecting the bookmarks to consider |
435 | * @param string $visibility process only all/private/public links | 437 | * @param string $visibility process only all/private/public bookmarks |
436 | * | 438 | * |
437 | * @return array tag => linksCount | 439 | * @return array tag => linksCount |
438 | */ | 440 | */ |
@@ -471,12 +473,12 @@ You use the community supported version of the original Shaarli project, by Seba | |||
471 | } | 473 | } |
472 | 474 | ||
473 | /** | 475 | /** |
474 | * Rename or delete a tag across all links. | 476 | * Rename or delete a tag across all bookmarks. |
475 | * | 477 | * |
476 | * @param string $from Tag to rename | 478 | * @param string $from Tag to rename |
477 | * @param string $to New tag. If none is provided, the from tag will be deleted | 479 | * @param string $to New tag. If none is provided, the from tag will be deleted |
478 | * | 480 | * |
479 | * @return array|bool List of altered links or false on error | 481 | * @return array|bool List of altered bookmarks or false on error |
480 | */ | 482 | */ |
481 | public function renameTag($from, $to) | 483 | public function renameTag($from, $to) |
482 | { | 484 | { |
@@ -519,7 +521,7 @@ You use the community supported version of the original Shaarli project, by Seba | |||
519 | } | 521 | } |
520 | 522 | ||
521 | /** | 523 | /** |
522 | * Reorder links by creation date (newest first). | 524 | * Reorder bookmarks by creation date (newest first). |
523 | * | 525 | * |
524 | * Also update the urls and ids mapping arrays. | 526 | * Also update the urls and ids mapping arrays. |
525 | * | 527 | * |
@@ -562,7 +564,7 @@ You use the community supported version of the original Shaarli project, by Seba | |||
562 | } | 564 | } |
563 | 565 | ||
564 | /** | 566 | /** |
565 | * Returns a link offset in links array from its unique ID. | 567 | * Returns a link offset in bookmarks array from its unique ID. |
566 | * | 568 | * |
567 | * @param int $id Persistent ID of a link. | 569 | * @param int $id Persistent ID of a link. |
568 | * | 570 | * |
diff --git a/application/bookmark/LinkFilter.php b/application/legacy/LegacyLinkFilter.php index 9b966307..7cf93d60 100644 --- a/application/bookmark/LinkFilter.php +++ b/application/legacy/LegacyLinkFilter.php | |||
@@ -1,16 +1,18 @@ | |||
1 | <?php | 1 | <?php |
2 | 2 | ||
3 | namespace Shaarli\Bookmark; | 3 | namespace Shaarli\Legacy; |
4 | 4 | ||
5 | use Exception; | 5 | use Exception; |
6 | use Shaarli\Bookmark\Exception\LinkNotFoundException; | 6 | use Shaarli\Bookmark\Exception\BookmarkNotFoundException; |
7 | 7 | ||
8 | /** | 8 | /** |
9 | * Class LinkFilter. | 9 | * Class LinkFilter. |
10 | * | 10 | * |
11 | * Perform search and filter operation on link data list. | 11 | * Perform search and filter operation on link data list. |
12 | * | ||
13 | * @deprecated | ||
12 | */ | 14 | */ |
13 | class LinkFilter | 15 | class LegacyLinkFilter |
14 | { | 16 | { |
15 | /** | 17 | /** |
16 | * @var string permalinks. | 18 | * @var string permalinks. |
@@ -38,12 +40,12 @@ class LinkFilter | |||
38 | public static $HASHTAG_CHARS = '\p{Pc}\p{N}\p{L}\p{Mn}'; | 40 | public static $HASHTAG_CHARS = '\p{Pc}\p{N}\p{L}\p{Mn}'; |
39 | 41 | ||
40 | /** | 42 | /** |
41 | * @var LinkDB all available links. | 43 | * @var LegacyLinkDB all available links. |
42 | */ | 44 | */ |
43 | private $links; | 45 | private $links; |
44 | 46 | ||
45 | /** | 47 | /** |
46 | * @param LinkDB $links initialization. | 48 | * @param LegacyLinkDB $links initialization. |
47 | */ | 49 | */ |
48 | public function __construct($links) | 50 | public function __construct($links) |
49 | { | 51 | { |
@@ -84,10 +86,10 @@ class LinkFilter | |||
84 | $filtered = $this->links; | 86 | $filtered = $this->links; |
85 | } | 87 | } |
86 | if (!empty($request[0])) { | 88 | if (!empty($request[0])) { |
87 | $filtered = (new LinkFilter($filtered))->filterTags($request[0], $casesensitive, $visibility); | 89 | $filtered = (new LegacyLinkFilter($filtered))->filterTags($request[0], $casesensitive, $visibility); |
88 | } | 90 | } |
89 | if (!empty($request[1])) { | 91 | if (!empty($request[1])) { |
90 | $filtered = (new LinkFilter($filtered))->filterFulltext($request[1], $visibility); | 92 | $filtered = (new LegacyLinkFilter($filtered))->filterFulltext($request[1], $visibility); |
91 | } | 93 | } |
92 | return $filtered; | 94 | return $filtered; |
93 | case self::$FILTER_TEXT: | 95 | case self::$FILTER_TEXT: |
@@ -137,7 +139,7 @@ class LinkFilter | |||
137 | * | 139 | * |
138 | * @return array $filtered array containing permalink data. | 140 | * @return array $filtered array containing permalink data. |
139 | * | 141 | * |
140 | * @throws \Shaarli\Bookmark\Exception\LinkNotFoundException if the smallhash doesn't match any link. | 142 | * @throws BookmarkNotFoundException if the smallhash doesn't match any link. |
141 | */ | 143 | */ |
142 | private function filterSmallHash($smallHash) | 144 | private function filterSmallHash($smallHash) |
143 | { | 145 | { |
@@ -151,7 +153,7 @@ class LinkFilter | |||
151 | } | 153 | } |
152 | 154 | ||
153 | if (empty($filtered)) { | 155 | if (empty($filtered)) { |
154 | throw new LinkNotFoundException(); | 156 | throw new BookmarkNotFoundException(); |
155 | } | 157 | } |
156 | 158 | ||
157 | return $filtered; | 159 | return $filtered; |
diff --git a/application/legacy/LegacyUpdater.php b/application/legacy/LegacyUpdater.php new file mode 100644 index 00000000..3a5de79f --- /dev/null +++ b/application/legacy/LegacyUpdater.php | |||
@@ -0,0 +1,617 @@ | |||
1 | <?php | ||
2 | |||
3 | namespace Shaarli\Legacy; | ||
4 | |||
5 | use Exception; | ||
6 | use RainTPL; | ||
7 | use ReflectionClass; | ||
8 | use ReflectionException; | ||
9 | use ReflectionMethod; | ||
10 | use Shaarli\ApplicationUtils; | ||
11 | use Shaarli\Bookmark\Bookmark; | ||
12 | use Shaarli\Bookmark\BookmarkArray; | ||
13 | use Shaarli\Bookmark\LinkDB; | ||
14 | use Shaarli\Bookmark\BookmarkFilter; | ||
15 | use Shaarli\Bookmark\BookmarkIO; | ||
16 | use Shaarli\Config\ConfigJson; | ||
17 | use Shaarli\Config\ConfigManager; | ||
18 | use Shaarli\Config\ConfigPhp; | ||
19 | use Shaarli\Exceptions\IOException; | ||
20 | use Shaarli\Thumbnailer; | ||
21 | use Shaarli\Updater\Exception\UpdaterException; | ||
22 | |||
23 | /** | ||
24 | * Class updater. | ||
25 | * Used to update stuff when a new Shaarli's version is reached. | ||
26 | * Update methods are ran only once, and the stored in a JSON file. | ||
27 | * | ||
28 | * @deprecated | ||
29 | */ | ||
30 | class LegacyUpdater | ||
31 | { | ||
32 | /** | ||
33 | * @var array Updates which are already done. | ||
34 | */ | ||
35 | protected $doneUpdates; | ||
36 | |||
37 | /** | ||
38 | * @var LegacyLinkDB instance. | ||
39 | */ | ||
40 | protected $linkDB; | ||
41 | |||
42 | /** | ||
43 | * @var ConfigManager $conf Configuration Manager instance. | ||
44 | */ | ||
45 | protected $conf; | ||
46 | |||
47 | /** | ||
48 | * @var bool True if the user is logged in, false otherwise. | ||
49 | */ | ||
50 | protected $isLoggedIn; | ||
51 | |||
52 | /** | ||
53 | * @var array $_SESSION | ||
54 | */ | ||
55 | protected $session; | ||
56 | |||
57 | /** | ||
58 | * @var ReflectionMethod[] List of current class methods. | ||
59 | */ | ||
60 | protected $methods; | ||
61 | |||
62 | /** | ||
63 | * Object constructor. | ||
64 | * | ||
65 | * @param array $doneUpdates Updates which are already done. | ||
66 | * @param LegacyLinkDB $linkDB LinkDB instance. | ||
67 | * @param ConfigManager $conf Configuration Manager instance. | ||
68 | * @param boolean $isLoggedIn True if the user is logged in. | ||
69 | * @param array $session $_SESSION (by reference) | ||
70 | * | ||
71 | * @throws ReflectionException | ||
72 | */ | ||
73 | public function __construct($doneUpdates, $linkDB, $conf, $isLoggedIn, &$session = []) | ||
74 | { | ||
75 | $this->doneUpdates = $doneUpdates; | ||
76 | $this->linkDB = $linkDB; | ||
77 | $this->conf = $conf; | ||
78 | $this->isLoggedIn = $isLoggedIn; | ||
79 | $this->session = &$session; | ||
80 | |||
81 | // Retrieve all update methods. | ||
82 | $class = new ReflectionClass($this); | ||
83 | $this->methods = $class->getMethods(); | ||
84 | } | ||
85 | |||
86 | /** | ||
87 | * Run all new updates. | ||
88 | * Update methods have to start with 'updateMethod' and return true (on success). | ||
89 | * | ||
90 | * @return array An array containing ran updates. | ||
91 | * | ||
92 | * @throws UpdaterException If something went wrong. | ||
93 | */ | ||
94 | public function update() | ||
95 | { | ||
96 | $updatesRan = array(); | ||
97 | |||
98 | // If the user isn't logged in, exit without updating. | ||
99 | if ($this->isLoggedIn !== true) { | ||
100 | return $updatesRan; | ||
101 | } | ||
102 | |||
103 | if ($this->methods === null) { | ||
104 | throw new UpdaterException(t('Couldn\'t retrieve updater class methods.')); | ||
105 | } | ||
106 | |||
107 | foreach ($this->methods as $method) { | ||
108 | // Not an update method or already done, pass. | ||
109 | if (!startsWith($method->getName(), 'updateMethod') | ||
110 | || in_array($method->getName(), $this->doneUpdates) | ||
111 | ) { | ||
112 | continue; | ||
113 | } | ||
114 | |||
115 | try { | ||
116 | $method->setAccessible(true); | ||
117 | $res = $method->invoke($this); | ||
118 | // Update method must return true to be considered processed. | ||
119 | if ($res === true) { | ||
120 | $updatesRan[] = $method->getName(); | ||
121 | } | ||
122 | } catch (Exception $e) { | ||
123 | throw new UpdaterException($method, $e); | ||
124 | } | ||
125 | } | ||
126 | |||
127 | $this->doneUpdates = array_merge($this->doneUpdates, $updatesRan); | ||
128 | |||
129 | return $updatesRan; | ||
130 | } | ||
131 | |||
132 | /** | ||
133 | * @return array Updates methods already processed. | ||
134 | */ | ||
135 | public function getDoneUpdates() | ||
136 | { | ||
137 | return $this->doneUpdates; | ||
138 | } | ||
139 | |||
140 | /** | ||
141 | * Move deprecated options.php to config.php. | ||
142 | * | ||
143 | * Milestone 0.9 (old versioning) - shaarli/Shaarli#41: | ||
144 | * options.php is not supported anymore. | ||
145 | */ | ||
146 | public function updateMethodMergeDeprecatedConfigFile() | ||
147 | { | ||
148 | if (is_file($this->conf->get('resource.data_dir') . '/options.php')) { | ||
149 | include $this->conf->get('resource.data_dir') . '/options.php'; | ||
150 | |||
151 | // Load GLOBALS into config | ||
152 | $allowedKeys = array_merge(ConfigPhp::$ROOT_KEYS); | ||
153 | $allowedKeys[] = 'config'; | ||
154 | foreach ($GLOBALS as $key => $value) { | ||
155 | if (in_array($key, $allowedKeys)) { | ||
156 | $this->conf->set($key, $value); | ||
157 | } | ||
158 | } | ||
159 | $this->conf->write($this->isLoggedIn); | ||
160 | unlink($this->conf->get('resource.data_dir') . '/options.php'); | ||
161 | } | ||
162 | |||
163 | return true; | ||
164 | } | ||
165 | |||
166 | /** | ||
167 | * Move old configuration in PHP to the new config system in JSON format. | ||
168 | * | ||
169 | * Will rename 'config.php' into 'config.save.php' and create 'config.json.php'. | ||
170 | * It will also convert legacy setting keys to the new ones. | ||
171 | */ | ||
172 | public function updateMethodConfigToJson() | ||
173 | { | ||
174 | // JSON config already exists, nothing to do. | ||
175 | if ($this->conf->getConfigIO() instanceof ConfigJson) { | ||
176 | return true; | ||
177 | } | ||
178 | |||
179 | $configPhp = new ConfigPhp(); | ||
180 | $configJson = new ConfigJson(); | ||
181 | $oldConfig = $configPhp->read($this->conf->getConfigFile() . '.php'); | ||
182 | rename($this->conf->getConfigFileExt(), $this->conf->getConfigFile() . '.save.php'); | ||
183 | $this->conf->setConfigIO($configJson); | ||
184 | $this->conf->reload(); | ||
185 | |||
186 | $legacyMap = array_flip(ConfigPhp::$LEGACY_KEYS_MAPPING); | ||
187 | foreach (ConfigPhp::$ROOT_KEYS as $key) { | ||
188 | $this->conf->set($legacyMap[$key], $oldConfig[$key]); | ||
189 | } | ||
190 | |||
191 | // Set sub config keys (config and plugins) | ||
192 | $subConfig = array('config', 'plugins'); | ||
193 | foreach ($subConfig as $sub) { | ||
194 | foreach ($oldConfig[$sub] as $key => $value) { | ||
195 | if (isset($legacyMap[$sub . '.' . $key])) { | ||
196 | $configKey = $legacyMap[$sub . '.' . $key]; | ||
197 | } else { | ||
198 | $configKey = $sub . '.' . $key; | ||
199 | } | ||
200 | $this->conf->set($configKey, $value); | ||
201 | } | ||
202 | } | ||
203 | |||
204 | try { | ||
205 | $this->conf->write($this->isLoggedIn); | ||
206 | return true; | ||
207 | } catch (IOException $e) { | ||
208 | error_log($e->getMessage()); | ||
209 | return false; | ||
210 | } | ||
211 | } | ||
212 | |||
213 | /** | ||
214 | * Escape settings which have been manually escaped in every request in previous versions: | ||
215 | * - general.title | ||
216 | * - general.header_link | ||
217 | * - redirector.url | ||
218 | * | ||
219 | * @return bool true if the update is successful, false otherwise. | ||
220 | */ | ||
221 | public function updateMethodEscapeUnescapedConfig() | ||
222 | { | ||
223 | try { | ||
224 | $this->conf->set('general.title', escape($this->conf->get('general.title'))); | ||
225 | $this->conf->set('general.header_link', escape($this->conf->get('general.header_link'))); | ||
226 | $this->conf->write($this->isLoggedIn); | ||
227 | } catch (Exception $e) { | ||
228 | error_log($e->getMessage()); | ||
229 | return false; | ||
230 | } | ||
231 | return true; | ||
232 | } | ||
233 | |||
234 | /** | ||
235 | * Update the database to use the new ID system, which replaces linkdate primary keys. | ||
236 | * Also, creation and update dates are now DateTime objects (done by LinkDB). | ||
237 | * | ||
238 | * Since this update is very sensitve (changing the whole database), the datastore will be | ||
239 | * automatically backed up into the file datastore.<datetime>.php. | ||
240 | * | ||
241 | * LinkDB also adds the field 'shorturl' with the precedent format (linkdate smallhash), | ||
242 | * which will be saved by this method. | ||
243 | * | ||
244 | * @return bool true if the update is successful, false otherwise. | ||
245 | */ | ||
246 | public function updateMethodDatastoreIds() | ||
247 | { | ||
248 | $first = 'update'; | ||
249 | foreach ($this->linkDB as $key => $link) { | ||
250 | $first = $key; | ||
251 | break; | ||
252 | } | ||
253 | |||
254 | // up to date database | ||
255 | if (is_int($first)) { | ||
256 | return true; | ||
257 | } | ||
258 | |||
259 | $save = $this->conf->get('resource.data_dir') . '/datastore.' . date('YmdHis') . '.php'; | ||
260 | copy($this->conf->get('resource.datastore'), $save); | ||
261 | |||
262 | $links = array(); | ||
263 | foreach ($this->linkDB as $offset => $value) { | ||
264 | $links[] = $value; | ||
265 | unset($this->linkDB[$offset]); | ||
266 | } | ||
267 | $links = array_reverse($links); | ||
268 | $cpt = 0; | ||
269 | foreach ($links as $l) { | ||
270 | unset($l['linkdate']); | ||
271 | $l['id'] = $cpt; | ||
272 | $this->linkDB[$cpt++] = $l; | ||
273 | } | ||
274 | |||
275 | $this->linkDB->save($this->conf->get('resource.page_cache')); | ||
276 | $this->linkDB->reorder(); | ||
277 | |||
278 | return true; | ||
279 | } | ||
280 | |||
281 | /** | ||
282 | * Rename tags starting with a '-' to work with tag exclusion search. | ||
283 | */ | ||
284 | public function updateMethodRenameDashTags() | ||
285 | { | ||
286 | $linklist = $this->linkDB->filterSearch(); | ||
287 | foreach ($linklist as $key => $link) { | ||
288 | $link['tags'] = preg_replace('/(^| )\-/', '$1', $link['tags']); | ||
289 | $link['tags'] = implode(' ', array_unique(BookmarkFilter::tagsStrToArray($link['tags'], true))); | ||
290 | $this->linkDB[$key] = $link; | ||
291 | } | ||
292 | $this->linkDB->save($this->conf->get('resource.page_cache')); | ||
293 | return true; | ||
294 | } | ||
295 | |||
296 | /** | ||
297 | * Initialize API settings: | ||
298 | * - api.enabled: true | ||
299 | * - api.secret: generated secret | ||
300 | */ | ||
301 | public function updateMethodApiSettings() | ||
302 | { | ||
303 | if ($this->conf->exists('api.secret')) { | ||
304 | return true; | ||
305 | } | ||
306 | |||
307 | $this->conf->set('api.enabled', true); | ||
308 | $this->conf->set( | ||
309 | 'api.secret', | ||
310 | generate_api_secret( | ||
311 | $this->conf->get('credentials.login'), | ||
312 | $this->conf->get('credentials.salt') | ||
313 | ) | ||
314 | ); | ||
315 | $this->conf->write($this->isLoggedIn); | ||
316 | return true; | ||
317 | } | ||
318 | |||
319 | /** | ||
320 | * New setting: theme name. If the default theme is used, nothing to do. | ||
321 | * | ||
322 | * If the user uses a custom theme, raintpl_tpl dir is updated to the parent directory, | ||
323 | * and the current theme is set as default in the theme setting. | ||
324 | * | ||
325 | * @return bool true if the update is successful, false otherwise. | ||
326 | */ | ||
327 | public function updateMethodDefaultTheme() | ||
328 | { | ||
329 | // raintpl_tpl isn't the root template directory anymore. | ||
330 | // We run the update only if this folder still contains the template files. | ||
331 | $tplDir = $this->conf->get('resource.raintpl_tpl'); | ||
332 | $tplFile = $tplDir . '/linklist.html'; | ||
333 | if (!file_exists($tplFile)) { | ||
334 | return true; | ||
335 | } | ||
336 | |||
337 | $parent = dirname($tplDir); | ||
338 | $this->conf->set('resource.raintpl_tpl', $parent); | ||
339 | $this->conf->set('resource.theme', trim(str_replace($parent, '', $tplDir), '/')); | ||
340 | $this->conf->write($this->isLoggedIn); | ||
341 | |||
342 | // Dependency injection gore | ||
343 | RainTPL::$tpl_dir = $tplDir; | ||
344 | |||
345 | return true; | ||
346 | } | ||
347 | |||
348 | /** | ||
349 | * Move the file to inc/user.css to data/user.css. | ||
350 | * | ||
351 | * Note: Due to hardcoded paths, it's not unit testable. But one line of code should be fine. | ||
352 | * | ||
353 | * @return bool true if the update is successful, false otherwise. | ||
354 | */ | ||
355 | public function updateMethodMoveUserCss() | ||
356 | { | ||
357 | if (!is_file('inc/user.css')) { | ||
358 | return true; | ||
359 | } | ||
360 | |||
361 | return rename('inc/user.css', 'data/user.css'); | ||
362 | } | ||
363 | |||
364 | /** | ||
365 | * * `markdown_escape` is a new setting, set to true as default. | ||
366 | * | ||
367 | * If the markdown plugin was already enabled, escaping is disabled to avoid | ||
368 | * breaking existing entries. | ||
369 | */ | ||
370 | public function updateMethodEscapeMarkdown() | ||
371 | { | ||
372 | if ($this->conf->exists('security.markdown_escape')) { | ||
373 | return true; | ||
374 | } | ||
375 | |||
376 | if (in_array('markdown', $this->conf->get('general.enabled_plugins'))) { | ||
377 | $this->conf->set('security.markdown_escape', false); | ||
378 | } else { | ||
379 | $this->conf->set('security.markdown_escape', true); | ||
380 | } | ||
381 | $this->conf->write($this->isLoggedIn); | ||
382 | |||
383 | return true; | ||
384 | } | ||
385 | |||
386 | /** | ||
387 | * Add 'http://' to Piwik URL the setting is set. | ||
388 | * | ||
389 | * @return bool true if the update is successful, false otherwise. | ||
390 | */ | ||
391 | public function updateMethodPiwikUrl() | ||
392 | { | ||
393 | if (!$this->conf->exists('plugins.PIWIK_URL') || startsWith($this->conf->get('plugins.PIWIK_URL'), 'http')) { | ||
394 | return true; | ||
395 | } | ||
396 | |||
397 | $this->conf->set('plugins.PIWIK_URL', 'http://' . $this->conf->get('plugins.PIWIK_URL')); | ||
398 | $this->conf->write($this->isLoggedIn); | ||
399 | |||
400 | return true; | ||
401 | } | ||
402 | |||
403 | /** | ||
404 | * Use ATOM feed as default. | ||
405 | */ | ||
406 | public function updateMethodAtomDefault() | ||
407 | { | ||
408 | if (!$this->conf->exists('feed.show_atom') || $this->conf->get('feed.show_atom') === true) { | ||
409 | return true; | ||
410 | } | ||
411 | |||
412 | $this->conf->set('feed.show_atom', true); | ||
413 | $this->conf->write($this->isLoggedIn); | ||
414 | |||
415 | return true; | ||
416 | } | ||
417 | |||
418 | /** | ||
419 | * Update updates.check_updates_branch setting. | ||
420 | * | ||
421 | * If the current major version digit matches the latest branch | ||
422 | * major version digit, we set the branch to `latest`, | ||
423 | * otherwise we'll check updates on the `stable` branch. | ||
424 | * | ||
425 | * No update required for the dev version. | ||
426 | * | ||
427 | * Note: due to hardcoded URL and lack of dependency injection, this is not unit testable. | ||
428 | * | ||
429 | * FIXME! This needs to be removed when we switch to first digit major version | ||
430 | * instead of the second one since the versionning process will change. | ||
431 | */ | ||
432 | public function updateMethodCheckUpdateRemoteBranch() | ||
433 | { | ||
434 | if (SHAARLI_VERSION === 'dev' || $this->conf->get('updates.check_updates_branch') === 'latest') { | ||
435 | return true; | ||
436 | } | ||
437 | |||
438 | // Get latest branch major version digit | ||
439 | $latestVersion = ApplicationUtils::getLatestGitVersionCode( | ||
440 | 'https://raw.githubusercontent.com/shaarli/Shaarli/latest/shaarli_version.php', | ||
441 | 5 | ||
442 | ); | ||
443 | if (preg_match('/(\d+)\.\d+$/', $latestVersion, $matches) === false) { | ||
444 | return false; | ||
445 | } | ||
446 | $latestMajor = $matches[1]; | ||
447 | |||
448 | // Get current major version digit | ||
449 | preg_match('/(\d+)\.\d+$/', SHAARLI_VERSION, $matches); | ||
450 | $currentMajor = $matches[1]; | ||
451 | |||
452 | if ($currentMajor === $latestMajor) { | ||
453 | $branch = 'latest'; | ||
454 | } else { | ||
455 | $branch = 'stable'; | ||
456 | } | ||
457 | $this->conf->set('updates.check_updates_branch', $branch); | ||
458 | $this->conf->write($this->isLoggedIn); | ||
459 | return true; | ||
460 | } | ||
461 | |||
462 | /** | ||
463 | * Reset history store file due to date format change. | ||
464 | */ | ||
465 | public function updateMethodResetHistoryFile() | ||
466 | { | ||
467 | if (is_file($this->conf->get('resource.history'))) { | ||
468 | unlink($this->conf->get('resource.history')); | ||
469 | } | ||
470 | return true; | ||
471 | } | ||
472 | |||
473 | /** | ||
474 | * Save the datastore -> the link order is now applied when bookmarks are saved. | ||
475 | */ | ||
476 | public function updateMethodReorderDatastore() | ||
477 | { | ||
478 | $this->linkDB->save($this->conf->get('resource.page_cache')); | ||
479 | return true; | ||
480 | } | ||
481 | |||
482 | /** | ||
483 | * Change privateonly session key to visibility. | ||
484 | */ | ||
485 | public function updateMethodVisibilitySession() | ||
486 | { | ||
487 | if (isset($_SESSION['privateonly'])) { | ||
488 | unset($_SESSION['privateonly']); | ||
489 | $_SESSION['visibility'] = 'private'; | ||
490 | } | ||
491 | return true; | ||
492 | } | ||
493 | |||
494 | /** | ||
495 | * Add download size and timeout to the configuration file | ||
496 | * | ||
497 | * @return bool true if the update is successful, false otherwise. | ||
498 | */ | ||
499 | public function updateMethodDownloadSizeAndTimeoutConf() | ||
500 | { | ||
501 | if ($this->conf->exists('general.download_max_size') | ||
502 | && $this->conf->exists('general.download_timeout') | ||
503 | ) { | ||
504 | return true; | ||
505 | } | ||
506 | |||
507 | if (!$this->conf->exists('general.download_max_size')) { | ||
508 | $this->conf->set('general.download_max_size', 1024 * 1024 * 4); | ||
509 | } | ||
510 | |||
511 | if (!$this->conf->exists('general.download_timeout')) { | ||
512 | $this->conf->set('general.download_timeout', 30); | ||
513 | } | ||
514 | |||
515 | $this->conf->write($this->isLoggedIn); | ||
516 | return true; | ||
517 | } | ||
518 | |||
519 | /** | ||
520 | * * Move thumbnails management to WebThumbnailer, coming with new settings. | ||
521 | */ | ||
522 | public function updateMethodWebThumbnailer() | ||
523 | { | ||
524 | if ($this->conf->exists('thumbnails.mode')) { | ||
525 | return true; | ||
526 | } | ||
527 | |||
528 | $thumbnailsEnabled = extension_loaded('gd') && $this->conf->get('thumbnail.enable_thumbnails', true); | ||
529 | $this->conf->set('thumbnails.mode', $thumbnailsEnabled ? Thumbnailer::MODE_ALL : Thumbnailer::MODE_NONE); | ||
530 | $this->conf->set('thumbnails.width', 125); | ||
531 | $this->conf->set('thumbnails.height', 90); | ||
532 | $this->conf->remove('thumbnail'); | ||
533 | $this->conf->write(true); | ||
534 | |||
535 | if ($thumbnailsEnabled) { | ||
536 | $this->session['warnings'][] = t( | ||
537 | 'You have enabled or changed thumbnails mode. <a href="?do=thumbs_update">Please synchronize them</a>.' | ||
538 | ); | ||
539 | } | ||
540 | |||
541 | return true; | ||
542 | } | ||
543 | |||
544 | /** | ||
545 | * Set sticky = false on all bookmarks | ||
546 | * | ||
547 | * @return bool true if the update is successful, false otherwise. | ||
548 | */ | ||
549 | public function updateMethodSetSticky() | ||
550 | { | ||
551 | foreach ($this->linkDB as $key => $link) { | ||
552 | if (isset($link['sticky'])) { | ||
553 | return true; | ||
554 | } | ||
555 | $link['sticky'] = false; | ||
556 | $this->linkDB[$key] = $link; | ||
557 | } | ||
558 | |||
559 | $this->linkDB->save($this->conf->get('resource.page_cache')); | ||
560 | |||
561 | return true; | ||
562 | } | ||
563 | |||
564 | /** | ||
565 | * Remove redirector settings. | ||
566 | */ | ||
567 | public function updateMethodRemoveRedirector() | ||
568 | { | ||
569 | $this->conf->remove('redirector'); | ||
570 | $this->conf->write(true); | ||
571 | return true; | ||
572 | } | ||
573 | |||
574 | /** | ||
575 | * Migrate the legacy arrays to Bookmark objects. | ||
576 | * Also make a backup of the datastore. | ||
577 | */ | ||
578 | public function updateMethodMigrateDatabase() | ||
579 | { | ||
580 | $save = $this->conf->get('resource.data_dir') . '/datastore.' . date('YmdHis') . '_1.php'; | ||
581 | if (! copy($this->conf->get('resource.datastore'), $save)) { | ||
582 | die('Could not backup the datastore.'); | ||
583 | } | ||
584 | |||
585 | $linksArray = new BookmarkArray(); | ||
586 | foreach ($this->linkDB as $key => $link) { | ||
587 | $linksArray[$key] = (new Bookmark())->fromArray($link); | ||
588 | } | ||
589 | $linksIo = new BookmarkIO($this->conf); | ||
590 | $linksIo->write($linksArray); | ||
591 | |||
592 | return true; | ||
593 | } | ||
594 | |||
595 | /** | ||
596 | * Write the `formatter` setting in config file. | ||
597 | * Use markdown if the markdown plugin is enabled, the default one otherwise. | ||
598 | * Also remove markdown plugin setting as it is now integrated to the core. | ||
599 | */ | ||
600 | public function updateMethodFormatterSetting() | ||
601 | { | ||
602 | if (!$this->conf->exists('formatter') || $this->conf->get('formatter') === 'default') { | ||
603 | $enabledPlugins = $this->conf->get('general.enabled_plugins'); | ||
604 | if (($pos = array_search('markdown', $enabledPlugins)) !== false) { | ||
605 | $formatter = 'markdown'; | ||
606 | unset($enabledPlugins[$pos]); | ||
607 | $this->conf->set('general.enabled_plugins', array_values($enabledPlugins)); | ||
608 | } else { | ||
609 | $formatter = 'default'; | ||
610 | } | ||
611 | $this->conf->set('formatter', $formatter); | ||
612 | $this->conf->write(true); | ||
613 | } | ||
614 | |||
615 | return true; | ||
616 | } | ||
617 | } | ||
diff --git a/application/netscape/NetscapeBookmarkUtils.php b/application/netscape/NetscapeBookmarkUtils.php index 28665941..d64eef7f 100644 --- a/application/netscape/NetscapeBookmarkUtils.php +++ b/application/netscape/NetscapeBookmarkUtils.php | |||
@@ -7,8 +7,10 @@ use DateTimeZone; | |||
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 | } |