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