diff options
Diffstat (limited to 'application/api')
-rw-r--r-- | application/api/ApiMiddleware.php | 5 | ||||
-rw-r--r-- | application/api/ApiUtils.php | 61 | ||||
-rw-r--r-- | application/api/controllers/ApiController.php | 19 | ||||
-rw-r--r-- | application/api/controllers/History.php | 70 | ||||
-rw-r--r-- | application/api/controllers/Links.php | 112 |
5 files changed, 261 insertions, 6 deletions
diff --git a/application/api/ApiMiddleware.php b/application/api/ApiMiddleware.php index 4120f7a9..ff209393 100644 --- a/application/api/ApiMiddleware.php +++ b/application/api/ApiMiddleware.php | |||
@@ -4,6 +4,7 @@ namespace Shaarli\Api; | |||
4 | use Shaarli\Api\Exceptions\ApiException; | 4 | use Shaarli\Api\Exceptions\ApiException; |
5 | use Shaarli\Api\Exceptions\ApiAuthorizationException; | 5 | use Shaarli\Api\Exceptions\ApiAuthorizationException; |
6 | 6 | ||
7 | use Shaarli\Config\ConfigManager; | ||
7 | use Slim\Container; | 8 | use Slim\Container; |
8 | use Slim\Http\Request; | 9 | use Slim\Http\Request; |
9 | use Slim\Http\Response; | 10 | use Slim\Http\Response; |
@@ -31,7 +32,7 @@ class ApiMiddleware | |||
31 | protected $container; | 32 | protected $container; |
32 | 33 | ||
33 | /** | 34 | /** |
34 | * @var \ConfigManager instance. | 35 | * @var ConfigManager instance. |
35 | */ | 36 | */ |
36 | protected $conf; | 37 | protected $conf; |
37 | 38 | ||
@@ -121,7 +122,7 @@ class ApiMiddleware | |||
121 | * | 122 | * |
122 | * FIXME! LinkDB could use a refactoring to avoid this trick. | 123 | * FIXME! LinkDB could use a refactoring to avoid this trick. |
123 | * | 124 | * |
124 | * @param \ConfigManager $conf instance. | 125 | * @param ConfigManager $conf instance. |
125 | */ | 126 | */ |
126 | protected function setLinkDb($conf) | 127 | protected function setLinkDb($conf) |
127 | { | 128 | { |
diff --git a/application/api/ApiUtils.php b/application/api/ApiUtils.php index d4015865..f154bb52 100644 --- a/application/api/ApiUtils.php +++ b/application/api/ApiUtils.php | |||
@@ -12,7 +12,7 @@ class ApiUtils | |||
12 | /** | 12 | /** |
13 | * Validates a JWT token authenticity. | 13 | * Validates a JWT token authenticity. |
14 | * | 14 | * |
15 | * @param string $token JWT token extracted from the headers. | 15 | * @param string $token JWT token extracted from the headers. |
16 | * @param string $secret API secret set in the settings. | 16 | * @param string $secret API secret set in the settings. |
17 | * | 17 | * |
18 | * @throws ApiAuthorizationException the token is not valid. | 18 | * @throws ApiAuthorizationException the token is not valid. |
@@ -50,7 +50,7 @@ class ApiUtils | |||
50 | /** | 50 | /** |
51 | * Format a Link for the REST API. | 51 | * Format a Link for the REST API. |
52 | * | 52 | * |
53 | * @param array $link Link data read from the datastore. | 53 | * @param array $link Link data read from the datastore. |
54 | * @param string $indexUrl Shaarli's index URL (used for relative URL). | 54 | * @param string $indexUrl Shaarli's index URL (used for relative URL). |
55 | * | 55 | * |
56 | * @return array Link data formatted for the REST API. | 56 | * @return array Link data formatted for the REST API. |
@@ -77,4 +77,61 @@ class ApiUtils | |||
77 | } | 77 | } |
78 | return $out; | 78 | return $out; |
79 | } | 79 | } |
80 | |||
81 | /** | ||
82 | * Convert a link given through a request, to a valid link for LinkDB. | ||
83 | * | ||
84 | * If no URL is provided, it will generate a local note URL. | ||
85 | * If no title is provided, it will use the URL as title. | ||
86 | * | ||
87 | * @param array $input Request Link. | ||
88 | * @param bool $defaultPrivate Request Link. | ||
89 | * | ||
90 | * @return array Formatted link. | ||
91 | */ | ||
92 | public static function buildLinkFromRequest($input, $defaultPrivate) | ||
93 | { | ||
94 | $input['url'] = ! empty($input['url']) ? cleanup_url($input['url']) : ''; | ||
95 | if (isset($input['private'])) { | ||
96 | $private = filter_var($input['private'], FILTER_VALIDATE_BOOLEAN); | ||
97 | } else { | ||
98 | $private = $defaultPrivate; | ||
99 | } | ||
100 | |||
101 | $link = [ | ||
102 | 'title' => ! empty($input['title']) ? $input['title'] : $input['url'], | ||
103 | 'url' => $input['url'], | ||
104 | 'description' => ! empty($input['description']) ? $input['description'] : '', | ||
105 | 'tags' => ! empty($input['tags']) ? implode(' ', $input['tags']) : '', | ||
106 | 'private' => $private, | ||
107 | 'created' => new \DateTime(), | ||
108 | ]; | ||
109 | return $link; | ||
110 | } | ||
111 | |||
112 | /** | ||
113 | * Update link fields using an updated link object. | ||
114 | * | ||
115 | * @param array $oldLink data | ||
116 | * @param array $newLink data | ||
117 | * | ||
118 | * @return array $oldLink updated with $newLink values | ||
119 | */ | ||
120 | public static function updateLink($oldLink, $newLink) | ||
121 | { | ||
122 | foreach (['title', 'url', 'description', 'tags', 'private'] as $field) { | ||
123 | $oldLink[$field] = $newLink[$field]; | ||
124 | } | ||
125 | $oldLink['updated'] = new \DateTime(); | ||
126 | |||
127 | if (empty($oldLink['url'])) { | ||
128 | $oldLink['url'] = '?' . $oldLink['shorturl']; | ||
129 | } | ||
130 | |||
131 | if (empty($oldLink['title'])) { | ||
132 | $oldLink['title'] = $oldLink['url']; | ||
133 | } | ||
134 | |||
135 | return $oldLink; | ||
136 | } | ||
80 | } | 137 | } |
diff --git a/application/api/controllers/ApiController.php b/application/api/controllers/ApiController.php index 1dd47f17..3be85b98 100644 --- a/application/api/controllers/ApiController.php +++ b/application/api/controllers/ApiController.php | |||
@@ -2,6 +2,7 @@ | |||
2 | 2 | ||
3 | namespace Shaarli\Api\Controllers; | 3 | namespace Shaarli\Api\Controllers; |
4 | 4 | ||
5 | use Shaarli\Config\ConfigManager; | ||
5 | use \Slim\Container; | 6 | use \Slim\Container; |
6 | 7 | ||
7 | /** | 8 | /** |
@@ -19,7 +20,7 @@ abstract class ApiController | |||
19 | protected $ci; | 20 | protected $ci; |
20 | 21 | ||
21 | /** | 22 | /** |
22 | * @var \ConfigManager | 23 | * @var ConfigManager |
23 | */ | 24 | */ |
24 | protected $conf; | 25 | protected $conf; |
25 | 26 | ||
@@ -29,6 +30,11 @@ abstract class ApiController | |||
29 | protected $linkDb; | 30 | protected $linkDb; |
30 | 31 | ||
31 | /** | 32 | /** |
33 | * @var \History | ||
34 | */ | ||
35 | protected $history; | ||
36 | |||
37 | /** | ||
32 | * @var int|null JSON style option. | 38 | * @var int|null JSON style option. |
33 | */ | 39 | */ |
34 | protected $jsonStyle; | 40 | protected $jsonStyle; |
@@ -45,10 +51,21 @@ abstract class ApiController | |||
45 | $this->ci = $ci; | 51 | $this->ci = $ci; |
46 | $this->conf = $ci->get('conf'); | 52 | $this->conf = $ci->get('conf'); |
47 | $this->linkDb = $ci->get('db'); | 53 | $this->linkDb = $ci->get('db'); |
54 | $this->history = $ci->get('history'); | ||
48 | if ($this->conf->get('dev.debug', false)) { | 55 | if ($this->conf->get('dev.debug', false)) { |
49 | $this->jsonStyle = JSON_PRETTY_PRINT; | 56 | $this->jsonStyle = JSON_PRETTY_PRINT; |
50 | } else { | 57 | } else { |
51 | $this->jsonStyle = null; | 58 | $this->jsonStyle = null; |
52 | } | 59 | } |
53 | } | 60 | } |
61 | |||
62 | /** | ||
63 | * Get the container. | ||
64 | * | ||
65 | * @return Container | ||
66 | */ | ||
67 | public function getCi() | ||
68 | { | ||
69 | return $this->ci; | ||
70 | } | ||
54 | } | 71 | } |
diff --git a/application/api/controllers/History.php b/application/api/controllers/History.php new file mode 100644 index 00000000..2ff9deaf --- /dev/null +++ b/application/api/controllers/History.php | |||
@@ -0,0 +1,70 @@ | |||
1 | <?php | ||
2 | |||
3 | |||
4 | namespace Shaarli\Api\Controllers; | ||
5 | |||
6 | use Shaarli\Api\Exceptions\ApiBadParametersException; | ||
7 | use Slim\Http\Request; | ||
8 | use Slim\Http\Response; | ||
9 | |||
10 | /** | ||
11 | * Class History | ||
12 | * | ||
13 | * REST API Controller: /history | ||
14 | * | ||
15 | * @package Shaarli\Api\Controllers | ||
16 | */ | ||
17 | class History extends ApiController | ||
18 | { | ||
19 | /** | ||
20 | * Service providing operation regarding Shaarli datastore and settings. | ||
21 | * | ||
22 | * @param Request $request Slim request. | ||
23 | * @param Response $response Slim response. | ||
24 | * | ||
25 | * @return Response response. | ||
26 | * | ||
27 | * @throws ApiBadParametersException Invalid parameters. | ||
28 | */ | ||
29 | public function getHistory($request, $response) | ||
30 | { | ||
31 | $history = $this->history->getHistory(); | ||
32 | |||
33 | // Return history operations from the {offset}th, starting from {since}. | ||
34 | $since = \DateTime::createFromFormat(\DateTime::ATOM, $request->getParam('since')); | ||
35 | $offset = $request->getParam('offset'); | ||
36 | if (empty($offset)) { | ||
37 | $offset = 0; | ||
38 | } | ||
39 | else if (ctype_digit($offset)) { | ||
40 | $offset = (int) $offset; | ||
41 | } else { | ||
42 | throw new ApiBadParametersException('Invalid offset'); | ||
43 | } | ||
44 | |||
45 | // limit parameter is either a number of links or 'all' for everything. | ||
46 | $limit = $request->getParam('limit'); | ||
47 | if (empty($limit)) { | ||
48 | $limit = count($history); | ||
49 | } else if (ctype_digit($limit)) { | ||
50 | $limit = (int) $limit; | ||
51 | } else { | ||
52 | throw new ApiBadParametersException('Invalid limit'); | ||
53 | } | ||
54 | |||
55 | $out = []; | ||
56 | $i = 0; | ||
57 | foreach ($history as $entry) { | ||
58 | if ((! empty($since) && $entry['datetime'] <= $since) || count($out) >= $limit) { | ||
59 | break; | ||
60 | } | ||
61 | if (++$i > $offset) { | ||
62 | $out[$i] = $entry; | ||
63 | $out[$i]['datetime'] = $out[$i]['datetime']->format(\DateTime::ATOM); | ||
64 | } | ||
65 | } | ||
66 | $out = array_values($out); | ||
67 | |||
68 | return $response->withJson($out, 200, $this->jsonStyle); | ||
69 | } | ||
70 | } | ||
diff --git a/application/api/controllers/Links.php b/application/api/controllers/Links.php index d4f1a09c..eb78dd26 100644 --- a/application/api/controllers/Links.php +++ b/application/api/controllers/Links.php | |||
@@ -97,11 +97,121 @@ class Links extends ApiController | |||
97 | */ | 97 | */ |
98 | public function getLink($request, $response, $args) | 98 | public function getLink($request, $response, $args) |
99 | { | 99 | { |
100 | if (! isset($this->linkDb[$args['id']])) { | 100 | if (!isset($this->linkDb[$args['id']])) { |
101 | throw new ApiLinkNotFoundException(); | 101 | throw new ApiLinkNotFoundException(); |
102 | } | 102 | } |
103 | $index = index_url($this->ci['environment']); | 103 | $index = index_url($this->ci['environment']); |
104 | $out = ApiUtils::formatLink($this->linkDb[$args['id']], $index); | 104 | $out = ApiUtils::formatLink($this->linkDb[$args['id']], $index); |
105 | |||
105 | return $response->withJson($out, 200, $this->jsonStyle); | 106 | return $response->withJson($out, 200, $this->jsonStyle); |
106 | } | 107 | } |
108 | |||
109 | /** | ||
110 | * Creates a new link from posted request body. | ||
111 | * | ||
112 | * @param Request $request Slim request. | ||
113 | * @param Response $response Slim response. | ||
114 | * | ||
115 | * @return Response response. | ||
116 | */ | ||
117 | public function postLink($request, $response) | ||
118 | { | ||
119 | $data = $request->getParsedBody(); | ||
120 | $link = ApiUtils::buildLinkFromRequest($data, $this->conf->get('privacy.default_private_links')); | ||
121 | // duplicate by URL, return 409 Conflict | ||
122 | if (! empty($link['url']) && ! empty($dup = $this->linkDb->getLinkFromUrl($link['url']))) { | ||
123 | return $response->withJson( | ||
124 | ApiUtils::formatLink($dup, index_url($this->ci['environment'])), | ||
125 | 409, | ||
126 | $this->jsonStyle | ||
127 | ); | ||
128 | } | ||
129 | |||
130 | $link['id'] = $this->linkDb->getNextId(); | ||
131 | $link['shorturl'] = link_small_hash($link['created'], $link['id']); | ||
132 | |||
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) | ||
148 | ->withJson($out, 201, $this->jsonStyle); | ||
149 | } | ||
150 | |||
151 | /** | ||
152 | * Updates an existing link from posted request body. | ||
153 | * | ||
154 | * @param Request $request Slim request. | ||
155 | * @param Response $response Slim response. | ||
156 | * @param array $args Path parameters. including the ID. | ||
157 | * | ||
158 | * @return Response response. | ||
159 | * | ||
160 | * @throws ApiLinkNotFoundException generating a 404 error. | ||
161 | */ | ||
162 | public function putLink($request, $response, $args) | ||
163 | { | ||
164 | if (! isset($this->linkDb[$args['id']])) { | ||
165 | throw new ApiLinkNotFoundException(); | ||
166 | } | ||
167 | |||
168 | $index = index_url($this->ci['environment']); | ||
169 | $data = $request->getParsedBody(); | ||
170 | |||
171 | $requestLink = ApiUtils::buildLinkFromRequest($data, $this->conf->get('privacy.default_private_links')); | ||
172 | // duplicate URL on a different link, return 409 Conflict | ||
173 | if (! empty($requestLink['url']) | ||
174 | && ! empty($dup = $this->linkDb->getLinkFromUrl($requestLink['url'])) | ||
175 | && $dup['id'] != $args['id'] | ||
176 | ) { | ||
177 | return $response->withJson( | ||
178 | ApiUtils::formatLink($dup, $index), | ||
179 | 409, | ||
180 | $this->jsonStyle | ||
181 | ); | ||
182 | } | ||
183 | |||
184 | $responseLink = $this->linkDb[$args['id']]; | ||
185 | $responseLink = ApiUtils::updateLink($responseLink, $requestLink); | ||
186 | $this->linkDb[$responseLink['id']] = $responseLink; | ||
187 | $this->linkDb->save($this->conf->get('resource.page_cache')); | ||
188 | $this->history->updateLink($responseLink); | ||
189 | |||
190 | $out = ApiUtils::formatLink($responseLink, $index); | ||
191 | return $response->withJson($out, 200, $this->jsonStyle); | ||
192 | } | ||
193 | |||
194 | /** | ||
195 | * Delete an existing link by its ID. | ||
196 | * | ||
197 | * @param Request $request Slim request. | ||
198 | * @param Response $response Slim response. | ||
199 | * @param array $args Path parameters. including the ID. | ||
200 | * | ||
201 | * @return Response response. | ||
202 | * | ||
203 | * @throws ApiLinkNotFoundException generating a 404 error. | ||
204 | */ | ||
205 | public function deleteLink($request, $response, $args) | ||
206 | { | ||
207 | if (! isset($this->linkDb[$args['id']])) { | ||
208 | throw new ApiLinkNotFoundException(); | ||
209 | } | ||
210 | $link = $this->linkDb[$args['id']]; | ||
211 | unset($this->linkDb[(int) $args['id']]); | ||
212 | $this->linkDb->save($this->conf->get('resource.page_cache')); | ||
213 | $this->history->deleteLink($link); | ||
214 | |||
215 | return $response->withStatus(204); | ||
216 | } | ||
107 | } | 217 | } |