/* * Copyright (c) 2022 joshua stein * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ #include #include #include "subtext.h" #include "ansi.h" #include "binkp.h" #include "board.h" #include "logger.h" #include "serial_local.h" #include "session.h" #include "sysop.h" #include "telnet.h" #define USERS_PER_PAGE 20 void sysop_edit_boards(struct session *s); void sysop_edit_folders(struct session *s); void sysop_edit_motd(struct session *s); void sysop_edit_users(struct session *s); size_t sysop_find_user_ids(size_t nall_user_ids, unsigned long *all_user_ids, unsigned long **user_ids, size_t offset, size_t limit); void sysop_edit_user(struct session *s, unsigned long id); void sysop_menu(struct session *s) { static const struct session_menu_option opts[] = { { 'm', "Mm", "Edit Message of the Day" }, { 'i', "Ii", "Trigger FTN Binkp poll" }, { 'h', "Hh", "Hang up modem" }, { 'b', "Bb", "Manage boards" }, { 'f', "Ff", "Manage file folders" }, { 'u', "Uu", "Manage users" }, { 's', "Ss", "Change BBS settings" }, { 'v', "Vv", "Reload views" }, { 'r', "Rr", "Reboot system" }, { 'q', "QqXx", "Return to main menu" }, { '?', "?", "List menu options" }, }; static const char prompt_help[] = "M:MOTD I:Binkp H:Hang Up B:Boards F:Folders U:Users S:Settings V:Views"; char c; bool show_help = true; bool done = false; if (!s->user || !s->user->is_sysop) return; session_logf(s, "Entered sysop menu"); while (!done && !s->ending) { c = session_menu(s, "Sysop Menu", "Sysop", (char *)prompt_help, opts, nitems(opts), show_help, NULL, NULL); show_help = false; switch (c) { case 'b': sysop_edit_boards(s); break; case 'f': sysop_edit_folders(s); break; case 'h': session_printf(s, "Hanging up modem...\r\n"); session_flush(s); serial_hangup(); serial_init(); break; case 'i': session_printf(s, "Requesting binkp poll...\r\n"); session_flush(s); binkp_next_poll = 0; binkp_last_poll_error = false; uthread_wakeup(periodic_job_thread); break; case 'm': sysop_edit_motd(s); break; case 'r': { IOParam pb; long m; short sc; session_printf(s, "Are you sure you want to reboot the system? [y/N] "); session_flush(s); sc = session_input_char(s); if (sc != 'Y' && sc != 'y') { session_output(s, "\r\n", 2); session_flush(s); break; } session_printf(s, "%c\r\n", sc == 'Y' ? 'Y' : 'y'); session_flush(s); _exiting(0); /* call atexit list, from atexit.c */ /* flush the disk and give it a couple seconds */ memset(&pb, 0, sizeof(pb)); PBFlushVol(&pb, false); Delay(120, &m); ShutDwnStart(); /* we should never get here */ ExitToShell(); break; } case 's': sysop_edit_settings(s); break; case 'u': sysop_edit_users(s); break; case 'v': session_printf(s, "Reloading views...\r\n"); session_flush(s); db_cache_views(db); break; case '?': show_help = true; break; default: done = true; break; } } } void sysop_edit_settings(struct session *s) { struct config *new_config; const struct struct_field *sf; char *new_config_data, *cur_config_data; short ret, n, nsval, osval; unsigned long nlval, olval; unsigned short reinits = 0; if (!s->user || !s->user->is_sysop) return; ret = struct_editor(s, config_fields, nconfig_fields, NULL, 0, &db->config, sizeof(struct config), (void *)&new_config, "BBS Settings", "Sysop:Settings"); if (ret != 0 || new_config == NULL) { session_printf(s, "No changes made\r\n"); return; } for (n = 0; n < nconfig_fields; n++) { sf = &config_fields[n]; cur_config_data = (char *)&db->config + sf->off; new_config_data = (char *)new_config + sf->off; switch (sf->type) { case CONFIG_TYPE_STRING: case CONFIG_TYPE_PASSWORD: if (memcmp(cur_config_data, new_config_data, sf->max) == 0) goto next_field; if (sf->type == CONFIG_TYPE_PASSWORD) session_logf(s, "Changed BBS setting \"%s\" (password)", sf->name); else session_logf(s, "Changed BBS setting \"%s\" from \"%s\" " "to \"%s\"", sf->name, cur_config_data, new_config_data); memcpy(cur_config_data, new_config_data, sf->max); break; case CONFIG_TYPE_SHORT: osval = CHARS_TO_SHORT(cur_config_data[0], cur_config_data[1]); nsval = CHARS_TO_SHORT(new_config_data[0], new_config_data[1]); if (nsval == osval) goto next_field; session_logf(s, "Changed BBS setting \"%s\" from %d to %d", sf->name, osval, nsval); memcpy(cur_config_data, new_config_data, sizeof(short)); break; case CONFIG_TYPE_LONG: olval = CHARS_TO_LONG(cur_config_data[0], cur_config_data[1], cur_config_data[2], cur_config_data[3]); nlval = CHARS_TO_LONG(new_config_data[0], new_config_data[1], new_config_data[2], new_config_data[3]); if (olval == nlval) goto next_field; session_logf(s, "Changed BBS setting \"%s\" from %lu to %lu", sf->name, olval, nlval); memcpy(cur_config_data, new_config_data, sizeof(long)); break; case CONFIG_TYPE_BOOLEAN: if (cur_config_data[0] == new_config_data[0]) goto next_field; session_logf(s, "Changed BBS setting \"%s\" from %s to %s", sf->name, cur_config_data[0] ? "true" : "false", new_config_data[0] ? "true" : "false"); memcpy(cur_config_data, new_config_data, 1); break; case CONFIG_TYPE_IP: { char cur_ip_str[16], new_ip_str[16]; olval = CHARS_TO_LONG(cur_config_data[0], cur_config_data[1], cur_config_data[2], cur_config_data[3]); nlval = CHARS_TO_LONG(new_config_data[0], new_config_data[1], new_config_data[2], new_config_data[3]); if (olval == nlval) goto next_field; if (olval == 0) snprintf(cur_ip_str, sizeof(cur_ip_str), "(None)"); else long2ip(olval, cur_ip_str); if (nlval == 0) snprintf(new_ip_str, sizeof(new_ip_str), "(None)"); else long2ip(nlval, new_ip_str); session_logf(s, "Changed BBS setting \"%s\" from %s to %s", sf->name, cur_ip_str, new_ip_str); memcpy(cur_config_data, new_config_data, sizeof(long)); break; } } reinits |= (1 << sf->reinit); next_field: continue; } db_config_save(db); bile_flush(db->bile, true); if (reinits & (1 << CONFIG_REQUIRES_SERIAL_REINIT)) serial_reinit(); if (reinits & (1 << CONFIG_REQUIRES_TELNET_REINIT)) telnet_reinit(); if (reinits & (1 << CONFIG_REQUIRES_BINKP_REINIT)) binkp_reinit(); if (reinits & (1 << CONFIG_REQUIRES_IPDB_REINIT)) { if (db->ipdb) ipdb_close(&db->ipdb); if (db->config.ipdb_path[0]) db->ipdb = ipdb_open(db->config.ipdb_path); } if (reinits & (1 << CONFIG_REQUIRES_SYSLOG_REINIT)) syslog_reinit(); xfree(&new_config); session_printf(s, "Successfully saved changes to BBS Settings\r\n"); } void sysop_edit_boards(struct session *s) { static const struct session_menu_option opts[] = { { '#', "", "Enter board to edit" }, { 'l', "Ll", "List boards" }, { 'n', "Nn", "Create new board" }, { 'q', "QqXx", "Return to sysop menu" }, { '?', "?", "List menu options" }, }; static const struct session_menu_option edit_opts[] = { { 'd', "Dd", "Delete board" }, { 'i', "Ii", "Re-index posts" }, }; static const char prompt_help[] = "#:Edit Board L:List N:New Q:Return ?:Help"; char prompt[30]; struct board *board, *new_board; struct bile *new_board_bile; size_t n, size; short bn, ret, sc; char c, *data = NULL; bool done, show_list, show_help; show_list = true; show_help = false; done = false; while (!done && !s->ending) { if (show_list) { session_printf(s, "{{B}}Boards{{/B}}\r\n"); session_printf(s, "%s# Name Description%s\r\n", ansi(s, ANSI_BOLD, ANSI_END), ansi(s, ANSI_RESET, ANSI_END)); session_flush(s); for (n = 0; n < db->nboards; n++) { session_printf(s, "%2ld %- 10.10s %s\r\n", n + 1, db->boards[n].name, db->boards[n].description); } session_flush(s); show_list = false; } c = session_menu(s, "Board Editor", "Sysop:Boards", (char *)prompt_help, opts, nitems(opts), show_help, "Board #", &bn); show_help = false; switch (c) { case 'l': show_list = true; break; case 'n': board = xmalloczero(sizeof(struct board)); if (board == NULL) continue; board->id = bile_next_id(db->bile, DB_BOARD_RTYPE); board->restricted_posting = false; board->restricted_viewing = false; ret = struct_editor(s, board_fields, nboard_fields, NULL, 0, board, sizeof(struct board), (void *)&new_board, "New Board", "Sysop:Boards:New"); if (ret != 0) { xfree(&board); continue; } new_board_bile = db_board_create(db, new_board, false); if (new_board_bile == NULL) { xfree(&board); continue; } bile_close(new_board_bile); bile_flush(db->bile, true); db_cache_boards(db); session_logf(s, "Created new board %ld: %s", new_board->id, new_board->name); xfree(&board); xfree(&new_board); break; case '#': if (bn < 1 || bn > db->nboards) { session_printf(s, "Invalid board ID\r\n"); session_flush(s); break; } board = &db->boards[bn - 1]; snprintf(prompt, sizeof(prompt), "Sysop:Boards:%ld", board->id); edit_board: ret = struct_editor(s, board_fields, nboard_fields, edit_opts, nitems(edit_opts), board, sizeof(struct board), (void *)&new_board, "Edit Board", prompt); switch (ret) { case 'i': board_index_sorted_post_ids(board, NULL); goto edit_board; case 'd': session_printf(s, "Really delete board %s? [y/N] ", board->name); session_flush(s); sc = session_input_char(s); if (s->ending) return; if (sc != 'Y' && sc != 'y') { session_output(s, "\r\n", 2); session_flush(s); break; } session_printf(s, "%c\r\n", sc == 'Y' ? 'Y' : 'y'); session_flush(s); session_logf(s, "Deleting board %ld (%s)!", board->id, board->name); db_board_delete(db, board); bile_flush(db->bile, true); db_cache_boards(db); break; case 0: ret = bile_marshall_object(db->bile, board_object_fields, nboard_object_fields, new_board, &data, &size); if (ret != 0 || size == 0) panic("board: failed to marshall object"); if (bile_write(db->bile, DB_BOARD_RTYPE, new_board->id, data, size) != size) panic("save of board failed: %d", bile_error(db->bile)); xfree(&data); bile_flush(db->bile, true); session_logf(s, "Saved changes to board %ld: %s", new_board->id, new_board->name); xfree(&new_board); db_cache_boards(db); break; } break; case '?': show_help = true; break; default: done = true; break; } } } void sysop_edit_folders(struct session *s) { static const struct session_menu_option opts[] = { { '#', "", "Enter folder to edit" }, { 'l', "Ll", "List folders" }, { 'n', "Nn", "Create new folder" }, { 'q', "QqXx", "Return to sysop menu" }, { '?', "?", "List menu options" }, }; static const struct session_menu_option edit_opts[] = { { 'd', "Dd", "Delete folder" }, }; static const char prompt_help[] = "#:Edit Folder L:List N:New Q:Return ?:Help"; char prompt[30]; struct folder *folder, *new_folder; struct bile *new_folder_bile; size_t n, size; short ret, fn, sc; char c, *data = NULL; bool done, show_list, show_help; show_list = true; show_help = false; done = false; while (!done && !s->ending) { if (show_list) { session_printf(s, "{{B}}Folders{{/B}}\r\n"); session_printf(s, "%s# Name Description%s\r\n", ansi(s, ANSI_BOLD, ANSI_END), ansi(s, ANSI_RESET, ANSI_END)); session_flush(s); for (n = 0; n < db->nfolders; n++) { session_printf(s, "%2ld %- 15.15s %s\r\n", n + 1, db->folders[n].name, db->folders[n].description); } session_flush(s); show_list = false; } c = session_menu(s, "Folder Editor", "Sysop:Folders", (char *)prompt_help, opts, nitems(opts), show_help, "Folder #", &fn); show_help = false; switch (c) { case 'l': show_list = true; break; case 'n': folder = xmalloczero(sizeof(struct folder)); if (folder == NULL) continue; folder->id = bile_next_id(db->bile, DB_FOLDER_RTYPE); ret = struct_editor(s, folder_fields, nfolder_fields, NULL, 0, folder, sizeof(struct folder), (void *)&new_folder, "New Folder", "Sysop:Folders:New"); if (ret != 0) { xfree(&folder); continue; } new_folder_bile = db_folder_create(db, new_folder, false); if (new_folder_bile == NULL) { xfree(&folder); xfree(&new_folder); continue; } bile_close(new_folder_bile); bile_flush(db->bile, true); db_cache_folders(db); session_logf(s, "Created new folder %ld: %s", new_folder->id, new_folder->name); xfree(&folder); xfree(&new_folder); break; case '#': if (fn < 1 || fn > db->nfolders) { session_printf(s, "Invalid folder ID\r\n"); session_flush(s); break; } folder = &db->folders[fn - 1]; snprintf(prompt, sizeof(prompt), "Sysop:Folders:%ld", folder->id); edit_folder: ret = struct_editor(s, folder_fields, nfolder_fields, edit_opts, nitems(edit_opts), folder, sizeof(struct folder), (void *)&new_folder, "Edit Folder", prompt); switch (ret) { case 'd': session_printf(s, "Really delete folder %s? [y/N] ", folder->name); session_flush(s); sc = session_input_char(s); if (s->ending) return; if (sc != 'Y' && sc != 'y') { session_output(s, "\r\n", 2); session_flush(s); break; } session_printf(s, "%c\r\n", sc == 'Y' ? 'Y' : 'y'); session_flush(s); session_logf(s, "Deleting folder %ld (%s)!", folder->id, folder->name); db_folder_delete(db, folder); bile_flush(db->bile, true); db_cache_folders(db); break; case 0: ret = bile_marshall_object(db->bile, folder_object_fields, nfolder_object_fields, new_folder, &data, &size); if (ret != 0 || size == 0) panic("folder: failed to marshall object"); if (bile_write(db->bile, DB_FOLDER_RTYPE, new_folder->id, data, size) != size) panic("save of folder failed: %d", bile_error(db->bile)); bile_flush(db->bile, true); xfree(&data); session_logf(s, "Saved changes to folder %ld: %s", new_folder->id, new_folder->name); xfree(&new_folder); db_cache_folders(db); break; } break; case '?': show_help = true; break; default: done = true; break; } } } void sysop_edit_motd(struct session *s) { static const struct session_menu_option opts[] = { { 'n', "Nn", "Create new MOTD" }, { 's', "Ss", "Show latest MOTD" }, { 'q', "QqXx", "Return to sysop menu" }, { '?', "?", "List menu options" }, }; static const char prompt_help[] = "N:New S:Show Q:Return ?:Help"; char c; bool show_help = true; bool done = false; while (!done && !s->ending) { c = session_menu(s, "MOTD Editor", "Sysop:MOTD", (char *)prompt_help, opts, nitems(opts), show_help, NULL, NULL); show_help = false; switch (c) { case 's': { session_print_motd(s, true); break; } case 'n': { char *motd = NULL, *tmp = NULL; struct bile_object *old_motd; unsigned long new_id; motd_write: session_printf(s, "MOTD Text:\r\n"); session_flush(s); tmp = session_field_input(s, 2048, s->terminal_columns - 1, motd, true, 0); session_output(s, "\r\n", 2); session_flush(s); rtrim(tmp, "\r\n\t "); if (motd) xfree(&motd); motd = tmp; session_printf(s, "\r\n{{B}}(S){{/B}}ave MOTD, " "{{B}}(E){{/B}}dit again, or {{B}}(C){{/B}}ancel? "); session_flush(s); c = session_input_char(s); if (c == 0 && s->obuflen > 0) { s->node_funcs->output(s); uthread_yield(); xfree(&motd); break; } session_printf(s, "%c\r\n", c); session_flush(s); switch (c) { case 's': case 'S': case 'y': case '\n': case '\r': /* save */ session_printf(s, "Saving MOTD..."); session_flush(s); new_id = bile_next_id(db->bile, DB_MOTD_RTYPE); old_motd = bile_get_nth_of_type(db->bile, 0, DB_MOTD_RTYPE); if (old_motd) { bile_delete(db->bile, DB_MOTD_RTYPE, old_motd->id, BILE_DELETE_FLAG_ZERO | BILE_DELETE_FLAG_PURGE); xfree(&old_motd); } bile_write(db->bile, DB_MOTD_RTYPE, new_id, motd, strlen(motd) + 1); bile_flush(db->bile, true); session_printf(s, " saved!\r\n"); session_flush(s); break; case 'e': case 'E': goto motd_write; break; case 'c': case 'C': case CONTROL_C: xfree(&motd); break; } break; } case '?': show_help = true; break; default: done = true; break; } } } void sysop_edit_users(struct session *s) { static const struct session_menu_option opts[] = { { '#', "#", "Edit user id [#]" }, { 'u', "Uu", "Edit user by username" }, { '<', "<", "Previous page of users" }, { 'l', "Ll", "List users" }, { '>', ">", "Next page of users" }, { 'q', "QqXx", "Return to sysop menu" }, { '?', "?", "List menu options" }, }; static const char prompt_help[] = "#:Edit User <:Prev >:Next L:List Q:Return ?:Help"; unsigned long *all_user_ids = NULL, *user_ids = NULL; struct user user, *fuser; size_t pages, page, n, size, nall_user_ids, nuser_ids; short suser_id, upp; char c, time[30], *username; bool show_help = false; bool show_list = true; bool done = false; bool find_user_ids = true; nall_user_ids = bile_sorted_ids_by_type(db->bile, DB_USER_RTYPE, &all_user_ids); page = 0; while (!done && !s->ending) { if (find_user_ids) { upp = USERS_PER_PAGE; if (s->terminal_lines < upp + 3) upp = BOUND(upp, 5, s->terminal_lines - 3); nuser_ids = sysop_find_user_ids(nall_user_ids, all_user_ids, &user_ids, page * upp, upp); /* ceil(nall_user_ids / upp) */ pages = (nall_user_ids + upp - 1) / upp; if (page >= pages) page = pages - 1; find_user_ids = false; } if (show_list) { session_printf(s, "{{B}}Users (Page %ld of %ld){{/B}}\r\n", page + 1, pages); session_printf(s, "%s # Username Active Sysop Last Login%s\r\n", ansi(s, ANSI_BOLD, ANSI_END), ansi(s, ANSI_RESET, ANSI_END)); session_flush(s); for (n = 0; n < nuser_ids; n++) { size = bile_read(db->bile, DB_USER_RTYPE, user_ids[n], (char *)&user, sizeof(struct user)); if (size == 0) continue; if (user.last_seen_at) strftime(time, sizeof(time), "%Y-%m-%d %H:%M", localtime(&user.last_seen_at)); else snprintf(time, sizeof(time), "Never"); session_printf(s, "%3ld %-14s %c %c %s\r\n", user.id, user.username, user.is_enabled ? 'Y' : '-', user.is_sysop ? 'Y' : '-', time); } session_flush(s); show_list = false; } c = session_menu(s, "Edit Users", "Sysop:Users", (char *)prompt_help, opts, nitems(opts), show_help, "User ID", &suser_id); show_help = false; handle_opt: switch (c) { case 'l': show_list = true; break; case '>': case '<': if (c == '>' && page == pages - 1) { session_printf(s, "You are at the last page of posts\r\n"); session_flush(s); break; } if (c == '<' && page == 0) { session_printf(s, "You are already at the first page\r\n"); session_flush(s); break; } if (c == '>') page++; else page--; find_user_ids = true; show_list = true; break; case '#': sysop_edit_user(s, suser_id); break; case 'u': session_printf(s, "Username: "); session_flush(s); username = session_field_input(s, 30, 30, NULL, false, 0); session_output(s, "\r\n", 2); session_flush(s); if (username == NULL) break; if (username[0] == '\0') { xfree(&username); break; } fuser = user_find_by_username(username); xfree(&username); if (!fuser) { session_printf(s, "Error: No such user.\r\n"); session_flush(s); break; } sysop_edit_user(s, fuser->id); xfree(&fuser); break; case '?': show_help = true; break; default: done = true; break; } } if (all_user_ids != NULL) xfree(&all_user_ids); } size_t sysop_find_user_ids(size_t nall_user_ids, unsigned long *all_user_ids, unsigned long **user_ids, size_t offset, size_t limit) { size_t nuser_ids, n; if (nall_user_ids < offset) return 0; nuser_ids = nall_user_ids - offset; if (nuser_ids > limit) nuser_ids = limit; *user_ids = xcalloc(sizeof(unsigned long), nuser_ids); if (*user_ids == NULL) return 0; for (n = 0; n < nuser_ids; n++) { (*user_ids)[n] = all_user_ids[offset + n]; } return nuser_ids; } void sysop_edit_user(struct session *s, unsigned long id) { static const struct session_menu_option opts[] = { { 'p', "Pp", "Change password" }, { 'e', "Ee", "Enable/disable account" }, { 'd', "Dd", "Delete account" }, { 's', "Ss", "Toggle sysop flag" }, { 'q', "QqXx", "Return to users menu" }, { '?', "?", "List menu options" }, }; static const char prompt_help[] = "P:Password E:Enable/Disable D:Delete S:Sysop Q:Return ?:Help"; char title[50]; char prompt[25]; struct user user; size_t size; bool done = false; bool show_help = true; char c; short cc; size = bile_read(db->bile, DB_USER_RTYPE, id, (char *)&user, sizeof(struct user)); if (!size) { session_printf(s, "Error: Failed to find user %ld\r\n", id); session_flush(s); return; } snprintf(title, sizeof(title), "Edit User %s", user.username); snprintf(prompt, sizeof(prompt), "Sysop:Users:%ld", user.id); while (!done && !s->ending) { c = session_menu(s, title, prompt, (char *)prompt_help, opts, nitems(opts), show_help, NULL, NULL); show_help = false; handle_opt: switch (c) { case 'd': session_printf(s, "Are you sure you want to permanently " "delete {{B}}%s{{/B}}? [y/N] ", user.username); session_flush(s); cc = session_input_char(s); if (cc == 'y' || c == 'Y') { session_printf(s, "%c\r\n", cc); session_flush(s); user_delete(&user); session_printf(s, "\r\n{{B}}User deleted!{{/B}}\r\n"); done = true; } else { session_printf(s, "\r\nUser not deleted.\r\n"); session_flush(s); } break; case 'e': user.is_enabled = !user.is_enabled; user_save(&user); session_printf(s, "User %s is now %sabled.\r\n", user.username, user.is_enabled ? "en" : "dis"); session_flush(s); break; case 'p': user_change_password(s, &user); break; case 's': if (strcmp(s->user->username, user.username) == 0) { session_printf(s, "You cannot remove your own sysop " "flag.\r\n"); session_flush(s); break; } user.is_sysop = !user.is_sysop; user_save(&user); session_printf(s, "User %s is now %sa sysop.\r\n", user.username, user.is_sysop ? "" : "no longer "); session_flush(s); break; case '?': show_help = true; break; default: done = true; break; } } }