cgi.c - frontends - front-ends for some sites (experiment) | |
Log | |
Files | |
Refs | |
README | |
LICENSE | |
--- | |
cgi.c (9039B) | |
--- | |
1 #include <sys/socket.h> | |
2 #include <sys/types.h> | |
3 | |
4 #include <ctype.h> | |
5 #include <errno.h> | |
6 #include <netdb.h> | |
7 #include <stdarg.h> | |
8 #include <stdio.h> | |
9 #include <stdlib.h> | |
10 #include <string.h> | |
11 #include <time.h> | |
12 #include <unistd.h> | |
13 | |
14 #include "reddit.h" | |
15 | |
16 #define OUT(s) (fputs((s), stdout)) | |
17 | |
18 extern char **environ; | |
19 | |
20 static struct list_response *response; | |
21 static time_t now; | |
22 | |
23 /* CGI parameters */ | |
24 static char rawsearch[4096], search[4096]; | |
25 static char subreddit[1024], mode[16], slimit[32], order[16]; | |
26 static long limit = 100; | |
27 | |
28 void | |
29 parsecgi(void) | |
30 { | |
31 char *query, *p; | |
32 size_t len; | |
33 | |
34 if (!(query = getenv("QUERY_STRING"))) | |
35 query = ""; | |
36 | |
37 /* subreddit: select subreddit */ | |
38 if ((p = getparam(query, "subreddit"))) { | |
39 if (decodeparam(subreddit, sizeof(subreddit), p) == -1) | |
40 subreddit[0] = '\0'; | |
41 } | |
42 | |
43 /* order */ | |
44 if ((p = getparam(query, "o"))) { | |
45 if (decodeparam(order, sizeof(order), p) == -1 || | |
46 (strcmp(order, "hot") && | |
47 strcmp(order, "new") && | |
48 strcmp(order, "rising") && | |
49 strcmp(order, "controversial"))) | |
50 order[0] = '\0'; | |
51 } | |
52 if (!order[0]) | |
53 snprintf(order, sizeof(order), "hot"); | |
54 | |
55 #if 0 | |
56 /* TODO */ | |
57 /* page */ | |
58 if ((p = getparam(query, "page"))) { | |
59 if (decodeparam(page, sizeof(page), p) == -1) | |
60 page[0] = '\0'; | |
61 /* check if it's a number >= 0 and < 100 */ | |
62 errno = 0; | |
63 curpage = strtol(page, NULL, 10); | |
64 if (errno || curpage < 0 || curpage > 100) { | |
65 curpage = 1; | |
66 page[0] = '\0'; | |
67 } | |
68 } | |
69 #endif | |
70 | |
71 /* limit */ | |
72 if ((p = getparam(query, "limit"))) { | |
73 if (decodeparam(slimit, sizeof(slimit), p) == -1) | |
74 slimit[0] = '\0'; | |
75 /* check if it's a number > 0 and < 100 */ | |
76 errno = 0; | |
77 limit = strtol(slimit, NULL, 10); | |
78 if (errno || limit <= 0 || limit > 100) { | |
79 limit = 100; /* default */ | |
80 slimit[0] = '\0'; | |
81 } | |
82 } | |
83 | |
84 /* mode */ | |
85 if ((p = getparam(query, "m"))) { | |
86 if (decodeparam(mode, sizeof(mode), p) != -1) { | |
87 /* fixup first character (label) for matching */ | |
88 if (mode[0]) | |
89 mode[0] = tolower((unsigned char)mode[0]… | |
90 /* allowed themes */ | |
91 if (strcmp(mode, "light") && | |
92 strcmp(mode, "dark") && | |
93 strcmp(mode, "pink") && | |
94 strcmp(mode, "templeos")) | |
95 mode[0] = '\0'; | |
96 } | |
97 } | |
98 if (!mode[0]) | |
99 snprintf(mode, sizeof(mode), "light"); | |
100 | |
101 /* search */ | |
102 if ((p = getparam(query, "q"))) { | |
103 if ((len = strcspn(p, "&")) && len + 1 < sizeof(rawsearc… | |
104 memcpy(rawsearch, p, len); | |
105 rawsearch[len] = '\0'; | |
106 } | |
107 | |
108 if (decodeparam(search, sizeof(search), p) == -1) { | |
109 OUT("Status: 401 Bad Request\r\n\r\n"); | |
110 exit(1); | |
111 } | |
112 } | |
113 } | |
114 | |
115 int | |
116 render(struct list_response *r) | |
117 { | |
118 struct item *items = r->items, *item; | |
119 char tmp[64], timebuf[32], baseurl[256]; | |
120 char *start, *end; | |
121 int i; | |
122 | |
123 #if 0 | |
124 if (pledge("stdio", NULL) == -1) { | |
125 OUT("Status: 500 Internal Server Error\r\n\r\n"); | |
126 exit(1); | |
127 } | |
128 #endif | |
129 | |
130 OUT( | |
131 "Content-Type: text/html; charset=utf-8\r\n\r\n" | |
132 "<!DOCTYPE html>\n<html>\n<head>\n" | |
133 "<meta name=\"referrer\" content=\"no-referrer\" />\n" | |
134 "<meta http-equiv=\"Content-Type\" content=\"text/html; … | |
135 "<title>"); | |
136 if (r->nitems && subreddit[0]) { | |
137 OUT("r/"); | |
138 xmlencode(subreddit); | |
139 OUT(" - Reddit"); | |
140 } else { | |
141 OUT("Reddit"); | |
142 } | |
143 OUT("</title>\n"); | |
144 OUT( | |
145 "<link rel=\"stylesheet\" href=\"css/"); | |
146 xmlencode(mode); | |
147 OUT( | |
148 ".css\" type=\"text/css\" media=\"screen\" />\n" | |
149 "<link rel=\"icon\" type=\"image/png\" href=\"/favicon.p… | |
150 "<meta content=\"width=device-width\" name=\"viewport\" … | |
151 "</head>\n" | |
152 "<body class=\"search\">\n" | |
153 "<form method=\"get\" action=\"\">\n"); | |
154 | |
155 OUT("<input type=\"hidden\" name=\"m\" value=\""); | |
156 xmlencode(mode); | |
157 OUT("\" />\n"); | |
158 if (subreddit[0]) { | |
159 OUT("<input type=\"hidden\" name=\"subreddit\" value=\""… | |
160 xmlencode(subreddit); | |
161 OUT("\" />\n"); | |
162 } | |
163 OUT("<input type=\"hidden\" name=\"limit\" value=\""); | |
164 xmlencode("100"); /* TODO maybe */ | |
165 OUT("\" />\n"); | |
166 OUT("<input type=\"hidden\" name=\"order\" value=\""); | |
167 xmlencode("hot"); /* TODO maybe */ | |
168 OUT("\" />\n"); | |
169 | |
170 OUT( | |
171 "<table class=\"search\" width=\"100%\" border=\"0\" cel… | |
172 "<tr>\n" | |
173 "<td width=\"100%\" class=\"input\">\n" | |
174 " <input type=\"search\" name=\"q\" value=\"\" pl… | |
175 "</td>\n" | |
176 "<td nowrap class=\"nowrap\">\n" | |
177 " <input type=\"submit\" value=\"Search\" class=\… | |
178 " <select name=\"o\" title=\"Order by\" accesskey… | |
179 " <option value=\"hot\" selected=\"select… | |
180 " <option value=\"new\">New</option>\n" | |
181 " <option value=\"rising\">Rising</option… | |
182 " <option value=\"controversial\">Controv… | |
183 " </select>\n" | |
184 " <label for=\"m\">Style: </label>\n"); | |
185 | |
186 if (!strcmp(mode, "light")) | |
187 OUT("\t\t<input type=\"submit\" name=\"m\" value=\"Dark\… | |
188 else | |
189 OUT("\t\t<input type=\"submit\" name=\"m\" value=\"Light… | |
190 | |
191 OUT( | |
192 " </td>\n" | |
193 "</tr>\n" | |
194 "</table>\n" | |
195 "</form>\n"); | |
196 | |
197 if (r->nitems) { | |
198 OUT( | |
199 "<hr/>\n" | |
200 "<table class=\"items\" width=\"100%\" border=\"… | |
201 "<tbody>\n"); | |
202 | |
203 for (i = 0; i < r->nitems; i++) { | |
204 item = r->items + i; | |
205 | |
206 OUT( | |
207 "<tr>\n" | |
208 "<td valign=\"middle\" align=\"center\" class=\"… | |
209 " \n"); | |
210 printf("%zu", item->ups); /* upvotes / score */ | |
211 OUT( | |
212 "</td>\n" | |
213 "<td valign=\"middle\" align=\"center\" class=\"… | |
214 | |
215 if (item->thumbnail[0] && !strncmp(item->thumbna… | |
216 OUT("<a href=\""); | |
217 | |
218 /* link directly to dash url for video */ | |
219 if (item->is_video) | |
220 xmlencode(item->dash_url); | |
221 else | |
222 xmlencode(item->url); | |
223 | |
224 OUT("\"><img src=\""); | |
225 xmlencode(item->thumbnail); | |
226 OUT("\" width=\"140\" alt=\"\" /></a>"); | |
227 } | |
228 | |
229 OUT( | |
230 "</td>\n" | |
231 "<td valign=\"top\">\n" | |
232 " <h2><a href=\""); | |
233 /* link directly to dash url for video */ | |
234 if (item->is_video) | |
235 xmlencode(item->dash_url); | |
236 else | |
237 xmlencode(item->url); | |
238 OUT("\">"); | |
239 | |
240 /* base url of url: somesite.org */ | |
241 baseurl[0] = '\0'; | |
242 if (!item->is_video && item->url[0]) { | |
243 if ((start = strstr(item->url, "://"))) { | |
244 start += strlen("://"); | |
245 if ((end = strstr(start, "/"))) { | |
246 if (end - start + 1 < si… | |
247 memcpy(baseurl, … | |
248 baseurl[end - st… | |
249 } | |
250 } | |
251 } | |
252 } | |
253 | |
254 if (item->is_video) | |
255 OUT("[video] "); | |
256 xmlencode(item->title); | |
257 | |
258 strftime(timebuf, sizeof(timebuf), "%Y-%m-%d %H:… | |
259 | |
260 OUT("</a></h2>\n"); | |
261 OUT( | |
262 " <time datetime=\""); | |
263 OUT(timebuf); | |
264 OUT("\">Submitted "); | |
265 if (!friendlytime(item->created_utc)) { | |
266 OUT("on "); | |
267 OUT(timebuf); | |
268 } | |
269 OUT(" by</time>\n" | |
270 " <a href=\"\">"); | |
271 xmlencode(item->author); | |
272 OUT("</a>\n"); | |
273 | |
274 /* global view: show subreddit */ | |
275 if (!subreddit[0]) { | |
276 /* TODO: link */ | |
277 OUT(" in r/"); | |
278 xmlencode(item->subreddit); | |
279 } | |
280 | |
281 OUT( | |
282 " <br/>\n" | |
283 " <br/>\n" | |
284 " <a href=\"https://old.reddit.com/"); | |
285 xmlencode(item->permalink); | |
286 OUT("\">"); | |
287 | |
288 printf("%zu", item->num_comments); | |
289 | |
290 OUT(" comments</a>\n" | |
291 "</td>\n" | |
292 "<td valign=\"top\" align=\"right\" class=\"tags… | |
293 " <span class=\"base\">"); | |
294 OUT(baseurl); | |
295 OUT( | |
296 "</span>\n" | |
297 " <br/>\n" | |
298 " <span class=\"tag\">"); | |
299 | |
300 /* TODO: + flair color */ | |
301 xmlencode(item->link_flair_text); | |
302 OUT(" </span>\n" | |
303 "</td>\n" | |
304 "</tr>\n"); | |
305 | |
306 OUT("<tr><td colspan=\"4\"><hr/></td></tr>\n"); | |
307 } | |
308 OUT("</tbody>\n"); | |
309 | |
310 /* pagination does not work for user/channel search */ | |
311 if (r->before[0] || r->after[0]) { | |
312 OUT( | |
313 "<tfoot>\n" | |
314 "<tr>\n" | |
315 "\t<td align=\"left\" class=\"nowrap\" n… | |
316 if (r->before[0]) { | |
317 OUT("\t\t<a href=\"?q="); | |
318 xmlencode(""); // TODO: remove q param l… | |
319 OUT("&before="); | |
320 xmlencode(r->before); | |
321 OUT("&m="); | |
322 xmlencode(mode); | |
323 OUT("\" rel=\"prev\" accesskey=\"p\">&la… | |
324 } | |
325 if (r->after[0]) { | |
326 OUT( | |
327 "\t</td>\n\t<td colspan=\"2\"></… | |
328 "\t<td align=\"right\" class=\"a… | |
329 OUT("\t\t<a href=\"?q="); // TODO: remov… | |
330 xmlencode(""); // | |
331 OUT("&after="); | |
332 xmlencode(r->after); | |
333 OUT("&m="); | |
334 xmlencode(mode); | |
335 OUT("\" rel=\"next\" accesskey=\"n\">nex… | |
336 } | |
337 | |
338 OUT( | |
339 "\t</td>\n" | |
340 "</tr>\n" | |
341 "</tfoot>\n"); | |
342 } | |
343 OUT("</table>\n"); | |
344 } | |
345 | |
346 OUT("</body>\n</html>\n"); | |
347 | |
348 return 0; | |
349 } | |
350 | |
351 int | |
352 main(void) | |
353 { | |
354 struct list_response *r; | |
355 | |
356 #if 0 | |
357 if (pledge("stdio dns inet rpath unveil", NULL) == -1 || | |
358 unveil(TLS_CA_CERT_FILE, "r") == -1 || | |
359 unveil(NULL, NULL) == -1) { | |
360 OUT("Status: 500 Internal Server Error\r\n\r\n"); | |
361 exit(1); | |
362 } | |
363 #endif | |
364 | |
365 parsecgi(); | |
366 | |
367 #if 0 | |
368 if (!rawsearch[0] && !chan[0] && !user[0]) | |
369 goto show; | |
370 #endif | |
371 | |
372 r = reddit_list(subreddit, ""); // TODO: page | |
373 if (!r || r->nitems <= 0) { | |
374 OUT("Status: 500 Internal Server Error\r\n\r\n"); | |
375 exit(1); | |
376 } | |
377 | |
378 show: | |
379 now = time(NULL); | |
380 render(r); | |
381 | |
382 return 0; | |
383 } |