Return JSON responses in auth, token and micropub API
[anoweco.git] / www / micropub.php
1 <?php
2 namespace anoweco;
3 /**
4  * Micropub endpoint that stores comments in the database
5  *
6  * @author Christian Weiske <cweiske@cweiske.de>
7  */
8 header('HTTP/1.0 500 Internal Server Error');
9 require 'www-header.php';
10
11 /**
12  * Send out a micropub error
13  *
14  * @param string $status      HTTP status code line
15  * @param string $code        One of the allowed status types:
16  *                            - forbidden
17  *                            - insufficient_scope
18  *                            - invalid_request
19  *                            - not_found
20  * @param string $description
21  */
22 function mpError($status, $code, $description)
23 {
24     header($status);
25     header('Content-Type: application/json');
26     echo json_encode(
27         ['error' => $code, 'error_description' => $description]
28     ) . "\n";
29     exit(1);
30 }
31
32 function validateToken($token)
33 {
34     $ctx = stream_context_create(
35         array(
36             'http' => array(
37                 'header' => array(
38                     'Authorization: Bearer ' . $token,
39                     'Accept: application/json',
40                 ),
41                 'ignore_errors' => true,
42             ),
43         )
44     );
45     //FIXME: make hard-coded token server URL configurable
46     $res = @file_get_contents(Urls::full('/token.php'), false, $ctx);
47     list($dummy, $code, $msg) = explode(' ', $http_response_header[0]);
48     if ($code != 200) {
49         mpError(
50             'HTTP/1.0 403 Forbidden',
51             'forbidden',
52             'Error verifying bearer token: ' . trim($res)
53         );
54     }
55
56     $data = json_decode($res, true);
57     //FIXME: they spit out non-micropub json error responess
58     verifyParameter($data, 'me');
59     verifyParameter($data, 'client_id');
60     verifyParameter($data, 'scope');
61
62     return [$data['me'], $data['client_id'], $data['scope']];
63 }
64
65 function handleCreate($json, $token)
66 {
67     list($me, $client_id, $scope) = validateToken($token);
68     $userId = Urls::userId($me);
69     if ($userId === null) {
70         mpError(
71             'HTTP/1.0 403 Forbidden',
72             'forbidden',
73             'Invalid user URL'
74         );
75     }
76     $storage = new Storage();
77     $rowUser = $storage->getUser($userId);
78     if ($rowUser === null) {
79         mpError(
80             'HTTP/1.0 403 Forbidden',
81             'forbidden',
82             'User not found: ' . $userId
83         );
84     }
85
86     $storage = new Storage();
87     $lb      = new Linkback();
88     try {
89         $id = $storage->addComment($json, $userId);
90         $lb->ping($id);
91
92         header('HTTP/1.0 201 Created');
93         header('Location: ' . Urls::full(Urls::comment($id)));
94         exit();
95     } catch (\Exception $e) {
96         if ($e->getCode() == 400) {
97             mpError(
98                 'HTTP/1.0 400 Bad Request',
99                 'invalid_request',
100                 $e->getMessage()
101             );
102         }
103
104         mpError(
105             'HTTP/1.0 500 Internal Server Error',
106             'this_violates_the_spec',
107             $e->getMessage()
108         );
109         exit();
110     }
111 }
112
113 function getTokenFromHeader()
114 {
115     if (isset($_SERVER['HTTP_AUTHORIZATION'])
116         && $_SERVER['HTTP_AUTHORIZATION'] != ''
117     ) {
118         $auth = $_SERVER['HTTP_AUTHORIZATION'];
119     } else if (isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])
120         && $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] != ''
121     ) {
122         //php-cgi has it there
123         $auth = $_SERVER['REDIRECT_HTTP_AUTHORIZATION'];
124     } else {
125         mpError(
126             'HTTP/1.0 403 Forbidden', 'forbidden',
127             'Authorization HTTP header missing'
128         );
129     }
130     if (strpos($auth, ' ') === false) {
131         mpError(
132             'HTTP/1.0 403 Forbidden', 'forbidden',
133             'Authorization header must start with "Bearer "'
134         );
135     }
136     list($bearer, $token) = explode(' ', $auth, 2);
137     if ($bearer !== 'Bearer') {
138         mpError(
139             'HTTP/1.0 403 Forbidden', 'forbidden',
140             'Authorization header must start with "Bearer "'
141         );
142     }
143     return trim($token);
144 }
145
146
147 if ($_SERVER['REQUEST_METHOD'] == 'GET') {
148     if (!isset($_GET['q'])) {
149         mpError(
150             'HTTP/1.1 400 Bad Request',
151             'invalid_request',
152             'Parameter "q" missing.'
153         );
154     } else if ($_GET['q'] === 'config') {
155         header('HTTP/1.0 200 OK');
156         header('Content-Type: application/json');
157         echo '{}';
158         exit();
159     } else if ($_GET['q'] === 'syndicate-to') {
160         header('HTTP/1.0 200 OK');
161         header('Content-Type: application/json');
162         echo '{}';
163         exit();
164     } else {
165         //FIXME: maybe implement $q=source
166         header('HTTP/1.1 501 Not Implemented');
167         header('Content-Type: text/plain');
168         echo 'Unsupported "q" value: ' . $_GET['q'] . "\n";
169         exit();
170     }
171 } else if ($_SERVER['REQUEST_METHOD'] == 'POST') {
172     if (!isset($_SERVER['CONTENT_TYPE'])) {
173         mpError(
174             'HTTP/1.1 400 Bad Request',
175             'invalid_request',
176             'Content-Type header missing.'
177         );
178     }
179     list($ctype) = explode(';', $_SERVER['CONTENT_TYPE'], 2);
180     $ctype = trim($ctype);
181     if ($ctype == 'application/x-www-form-urlencoded'
182         || $ctype == 'multipart/form-data'
183     ) {
184         if (!isset($_POST['action'])) {
185             $_POST['action'] = 'create';
186         }
187         if ($_POST['action'] != 'create') {
188             header('HTTP/1.1 501 Not Implemented');
189             header('Content-Type: text/plain');
190             echo "Creation of posts supported only\n";
191             exit();
192         }
193
194         $data = $_POST;
195         $base = (object) [
196             'type' => ['h-entry'],
197         ];
198         if (isset($data['h'])) {
199             $base->type = ['h-' . $data['h']];
200             unset($data['h']);
201         }
202         if (isset($data['access_token'])) {
203             $token = $data['access_token'];
204             unset($data['access_token']);
205         } else {
206             $token = getTokenFromHeader();
207         }
208         //reserved properties
209         foreach (['q', 'url', 'action'] as $key) {
210             if (isset($data[$key])) {
211                 $base->$key = $data[$key];
212                 unset($data[$key]);
213             }
214         }
215         //"mp-" reserved for future use
216         foreach ($data as $key => $value) {
217             if (substr($key, 0, 3) == 'mp-') {
218                 $base->$key = $value;
219                 unset($data[$key]);
220             } else if (!is_array($value)) {
221                 //convert to array
222                 $data[$key] = [$value];
223             }
224         }
225         $json = $base;
226         $json->properties = (object) $data;
227         handleCreate($json, $token);
228     } else if ($ctype == 'application/json') {
229         $input = file_get_contents('php://input');
230         $json  = json_decode($input);
231         if ($json === null) {
232             mpError(
233                 'HTTP/1.1 400 Bad Request',
234                 'invalid_request',
235                 'Invalid JSON'
236             );
237         }
238         $token = getTokenFromHeader();
239         handleCreate($json, $token);
240     } else {
241         mpError(
242             'HTTP/1.1 400 Bad Request',
243             'invalid_request',
244             'Unsupported POST content type'
245         );
246     }
247 } else {
248     mpError(
249         'HTTP/1.0 400 Bad Request',
250         'invalid_request',
251         'Unsupported HTTP request method'
252     );
253 }
254 ?>