mittag.c (8474B)
1 /** 2 * Lunch Poll Legacy 3 * Copyright 2024 - 2026 Matthias Balk 4 */ 5 6 #include <errno.h> 7 #include <regex.h> 8 #include <stdio.h> 9 #include <stdlib.h> 10 #include <string.h> 11 #include <time.h> 12 13 #include <sqlite3.h> 14 15 #include "http.h" 16 #include "date-utils.h" 17 #include "utils.h" 18 #include "mittag.h" 19 #include "config.h" 20 21 22 static sqlite3 *db; 23 static char _header_printed = 0; 24 25 26 static int get_votes_callback(void *unused, 27 int num_cols, 28 char **col_values, 29 char **col_names) 30 { 31 /* TODO: direct output is not always beautiful, think about alternatives */ 32 if (!_header_printed) { 33 _header_printed = 1; 34 header_status(200); 35 header("X-OSSE", PROG_VERSION); 36 header("Content-Type", "text/html; charset=utf-8"); 37 header_end(); 38 39 puts("<!DOCTYPE html>"); 40 puts("<html lang=\"de\"><head><meta charset=\"utf-8\">"); 41 puts("<meta name=\"robots\" content=\"noindex\">"); 42 puts("<meta name=\"viewport\" content=\"width=device-width, " 43 "initial-scale=1.0\">"); 44 puts("<style> <!--"); 45 puts("table { tr:nth-child(odd) { background-color: #eee; } }"); 46 puts("--> </style>"); 47 puts("<title>Alle mampfen Mamba. Mampfred auch.</title></head>"); 48 puts("<body><form action=\"/mittag.cgi/votes\" method=\"POST\"><table>"); 49 } 50 51 if (strlen(col_values[2] /* r_url */)) { 52 printf("<tr><td><input type=\"checkbox\" name=\"restaurant_id\" " 53 "value=\"%s\"></td><td>%s</td><td>%s</td><td><a href=\"%s\" " 54 "target=\"_blank\">%s</a></td></tr>\n", 55 col_values[4], /* r_id */ 56 col_values[3], /* num_votes */ 57 col_values[0], /* voters */ 58 col_values[2], /* r_url */ 59 col_values[1]); /* r_name */ 60 } 61 else { 62 printf("<tr><td><input type=\"checkbox\" name=\"restaurant_id\" " 63 "value=\"%s\"></td><td>%s</td><td>%s</td><td>%s</td></tr>\n", 64 col_values[4], /* r_id */ 65 col_values[3], /* num_votes */ 66 col_values[0], /* voters */ 67 col_values[1]); /* r_name */ 68 } 69 70 return 0; 71 } 72 73 static void get_votes(void) 74 { 75 char date[MAX_LEN_DATE + 1] = { 0 }; 76 77 char *query_string = getenv("QUERY_STRING"); 78 if (query_string != NULL) { 79 char* params[MAX_POST_SIZE]; 80 split_and_decode_form_params(query_string, params); 81 82 for (int idx = 0; idx < MAX_PARAMS_COUNT && params[idx] != NULL; idx++) { 83 if (strstr(params[idx], "date=") == params[idx]) { 84 strlcpy(date, 85 strchr(params[idx], '=') + 1, 86 (MAX_LEN_DATE + 1) * sizeof(char)); 87 break; 88 } 89 } 90 } 91 92 if (*date == 0) { 93 time_t t = time(NULL); 94 struct tm *tm = gmtime(&t); 95 strftime(date, (MAX_LEN_DATE + 1) * sizeof(char), "%Y-%m-%d", tm); 96 } 97 98 char buffer[350]; 99 snprintf(buffer, 350 * sizeof(char), 100 "SELECT " 101 "CASE WHEN GROUP_CONCAT(v.voter) IS NOT NULL THEN " 102 "GROUP_CONCAT(v.voter, ', ') ELSE '' END AS 'voters', " 103 "r.name, " 104 "r.url, " 105 "COUNT(v.id) AS 'num_votes', " 106 "r.id " 107 "FROM " 108 "restaurant r " 109 "LEFT JOIN vote v " 110 "ON v.restaurant_id = r.id AND " 111 "v.date = '%s' " 112 "WHERE " 113 "r.days_open & %d <> 0 " 114 "GROUP BY " 115 "r.id " 116 "ORDER BY " 117 "num_votes DESC, " 118 "r.category ASC, " 119 "r.id ASC;", 120 date, get_day_of_week(date)); 121 char *zErrMsg = NULL; 122 int rc = sqlite3_exec(db, buffer, get_votes_callback, 0, &zErrMsg); 123 if (rc != SQLITE_OK) { 124 _header_printed = 1; 125 header_status(500); 126 header_end(); 127 128 fprintf(stderr, "SQL error: %s\n", zErrMsg); 129 sqlite3_free(zErrMsg); 130 return; 131 } 132 133 if (!_header_printed) { 134 header_status(404); 135 header_end(); 136 return; 137 } 138 139 puts("</table>Name: <input type=\"text\" name=\"name\">"); 140 puts("<input type=\"submit\" value=\"Abstimmen\"></form>"); 141 printf("<p>%s %s -- %s</p>\n", 142 PROG_NAME, PROG_VERSION, COPYRIGHT); 143 if (SOURCE_CODE_DOWNLOAD_URL) { 144 printf("<p><a href=\"%s\" target=\"_blank\">Source Code</a></p>\n", 145 SOURCE_CODE_DOWNLOAD_URL); 146 } 147 puts("</body></html>"); 148 } 149 150 static void post_votes(void) 151 { 152 char *content_type = getenv("CONTENT_TYPE"); 153 if (content_type == NULL || 154 strcmp("application/x-www-form-urlencoded", content_type) != 0) { 155 header_status(415); 156 header_end(); 157 return; 158 } 159 160 char buffer[MAX_POST_SIZE] = { 0 }; 161 fread(buffer, sizeof(*buffer), MAX_POST_SIZE - 1, stdin); 162 if (ferror(stdin)) { 163 const char* err = strerror(errno); 164 fputs(err, stderr); 165 err_exit(err); 166 } 167 if (!feof(stdin)) { 168 fprintf(stderr, 169 "error: posted data exceeds maximum supported size of %lu bytes\n", 170 MAX_POST_SIZE * sizeof(*buffer)); 171 err_exit("error: posted data exceeds maximum supported size"); 172 } 173 174 char* params[MAX_POST_SIZE]; 175 split_and_decode_form_params(buffer, params); 176 177 char *name = NULL; 178 for (int idx = 0; params[idx] != NULL; idx++) { 179 if (strstr(params[idx], "name=") == params[idx]) { 180 name = strchr(params[idx], '=') + 1; 181 break; 182 } 183 } 184 /* TODO: trim beginning and trailing whitespaces from name: 185 * 'w3m' adds a trailing newline... */ 186 187 if (name != NULL) name = trim(name); 188 189 if (name == NULL || 190 *name == '\0' || 191 strlen(name) > MAX_LEN_NAME) { 192 header_status(400); 193 header("Content-Type", "text/plain; charset=utf-8"); 194 header_end(); 195 puts("required field 'name' is missing or too long"); 196 return; 197 } 198 199 char date[MAX_LEN_DATE + 1] = { 0 }; 200 time_t t = time(NULL); 201 struct tm *tm = gmtime(&t); 202 strftime(date, (MAX_LEN_DATE + 1) * sizeof(char), "%Y-%m-%d", tm); 203 204 205 char stmnt[48 + MAX_LEN_NAME + MAX_LEN_DATE]; 206 snprintf(stmnt, (48 + MAX_LEN_NAME + MAX_LEN_DATE) * sizeof(char), 207 "DELETE FROM vote WHERE voter = '%s' AND date = '%s'", 208 name, date); 209 210 char *zErrMsg = NULL; 211 int rc = sqlite3_exec(db, stmnt, NULL, 0, &zErrMsg); 212 if (rc != SQLITE_OK) { 213 _header_printed = 1; 214 header_status(500); 215 header_end(); 216 217 fprintf(stderr, "SQL error: %s\n", zErrMsg); 218 sqlite3_free(zErrMsg); 219 return; 220 } 221 222 for (int idx = 0; params[idx] != NULL; idx++) { 223 if (strstr(params[idx], "restaurant_id=") == params[idx]) { 224 char *id = strchr(params[idx], '=') + 1; 225 226 char stmnt[64 + MAX_LEN_NAME + MAX_LEN_DATE + MAX_LEN_RESTAURANT_ID]; 227 snprintf(stmnt, 228 (64 + MAX_LEN_NAME + MAX_LEN_DATE + MAX_LEN_RESTAURANT_ID) 229 * sizeof(char), 230 "INSERT INTO vote (voter, date, restaurant_id) " 231 "VALUES ('%s', '%s', %s)", 232 name, date, id); 233 234 char *zErrMsg = NULL; 235 int rc = sqlite3_exec(db, stmnt, NULL, 0, &zErrMsg); 236 if (rc != SQLITE_OK) { 237 _header_printed = 1; 238 header_status(500); 239 header_end(); 240 241 fprintf(stderr, "SQL error: %s\n", zErrMsg); 242 sqlite3_free(zErrMsg); 243 return; 244 } 245 } 246 } 247 248 header_status(303); 249 header("Location", "/mittag.cgi/votes/"); 250 header_end(); 251 } 252 253 static void open_database(void) 254 { 255 /* directory which contains the database must be writeable! */ 256 /* TODO: pledge? 257 TODO: OPEN-Flag dependent on GET (READ ONLY) / POST (WRITE ONLY) */ 258 if (sqlite3_open_v2("var/mittag.db", &db, SQLITE_OPEN_READWRITE, NULL) 259 != SQLITE_OK) 260 { 261 const char* err = sqlite3_errmsg(db); 262 fputs(err, stderr); 263 sqlite3_close(db); 264 err_exit("sqlite3_open error"); 265 } 266 } 267 268 static void close_database(void) 269 { 270 sqlite3_close(db); 271 } 272 273 int main(int argc, char **argv) 274 { 275 Route *r; 276 char path_found = 0; 277 char *path = getenv("PATH_INFO") ? getenv("PATH_INFO") : "/"; 278 for (r = routes; r < routes + LEN(routes); r++) { 279 regex_t preg; 280 if (regcomp(&preg, r->path, REG_EXTENDED | REG_NOSUB) != 0) 281 { 282 err_exit("regcomp"); 283 } 284 path_found = regexec(&preg, path, 0, NULL, 0) != REG_NOMATCH; 285 regfree(&preg); 286 if (path_found && getenv("REQUEST_METHOD")) { 287 if (strcmp(getenv("REQUEST_METHOD"), r->method) == 0) { 288 open_database(); 289 /* TODO: pass args */ 290 r->func(); 291 close_database(); 292 return EXIT_SUCCESS; 293 } 294 } 295 } 296 297 !path_found ? header_status(404) : header_status(405); 298 header_end(); 299 return EXIT_SUCCESS; 300 }