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