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