/* * 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 #include #include "subtext.h" #include "ansi.h" #include "folder.h" #include "nomodem.h" #include "sha1.h" #include "user.h" #include "zmodem.h" #define FILES_PER_PAGE 20 #define FILE_VIEW_RETURN_DONE -1 #define FILE_VIEW_RETURN_LIST -2 #define FILE_VIEW_RETURN_FIND -3 const struct struct_field folder_fields[] = { { "Folder ID", CONFIG_TYPE_LONG, offsetof(struct folder, id), 1, LONG_MAX }, { "Name", CONFIG_TYPE_STRING, offsetof(struct folder, name), 1, member_size(struct folder, name) }, { "Disk Path", CONFIG_TYPE_STRING, offsetof(struct folder, path), 1, member_size(struct folder, path) }, { "Description", CONFIG_TYPE_STRING, offsetof(struct folder, description), 0, member_size(struct folder, description) }, { "Restricted Posting", CONFIG_TYPE_BOOLEAN, offsetof(struct folder, restricted_posting), 0, 0 }, { "Restricted Viewing", CONFIG_TYPE_BOOLEAN, offsetof(struct folder, restricted_viewing), 0, 0 }, }; const size_t nfolder_fields = nitems(folder_fields); const struct bile_object_field folder_object_fields[] = { { offsetof(struct folder, id), member_size(struct folder, id), -1 }, { offsetof(struct folder, name), member_size(struct folder, name), -1 }, { offsetof(struct folder, description), member_size(struct folder, description), -1 }, { offsetof(struct folder, restricted_posting), member_size(struct folder, restricted_posting), -1 }, { offsetof(struct folder, restricted_viewing), member_size(struct folder, restricted_viewing), -1 }, { offsetof(struct folder, last_upload_at), member_size(struct folder, last_upload_at), -1 }, { offsetof(struct folder, file_count), member_size(struct folder, file_count), -1 }, { offsetof(struct folder, path), member_size(struct folder, path), -1 }, }; const size_t nfolder_object_fields = nitems(folder_object_fields); const struct bile_object_field folder_file_object_fields[] = { { offsetof(struct folder_file, id), member_size(struct folder_file, id), -1 }, { offsetof(struct folder_file, time), member_size(struct folder_file, time), -1 }, { offsetof(struct folder_file, uploader_user_id), member_size(struct folder_file, uploader_user_id), -1 }, { offsetof(struct folder_file, filename), member_size(struct folder_file, filename), -1 }, { offsetof(struct folder_file, description), member_size(struct folder_file, description), -1 }, { offsetof(struct folder_file, size), member_size(struct folder_file, size), -1 }, { offsetof(struct folder_file, sha1_checksum), member_size(struct folder_file, sha1_checksum), -1 }, { offsetof(struct folder_file, notes_size), member_size(struct folder_file, notes_size), -1 }, { offsetof(struct folder_file, notes), -1, offsetof(struct folder_file, notes_size) }, }; const size_t nfolder_file_object_fields = nitems(folder_file_object_fields); unsigned long folder_upload(struct session *s, struct folder *folder, char *initial_filename, char *initial_description); void folder_list_files(struct session *s, struct folder *folder, size_t nfile_ids, unsigned long *file_ids, short files_per_page, size_t page, size_t pages); void folder_show(struct session *s, struct folder *folder); short folder_file_view(struct session *s, struct folder *folder, unsigned long id); void folder_delete_file(struct session *s, struct folder *folder, struct folder_file *file); size_t folder_find_file_ids(struct folder *folder, size_t *nfile_ids, unsigned long **file_ids); bool folder_file_save(struct folder *folder, struct folder_file *file, char *temp_path); bool folder_file_valid_filename(struct session *session, struct folder *folder, struct folder_file *file, char **error); bool folder_edit_file(struct session *s, struct folder *folder, struct folder_file *file, char *file_path); bool folder_file_checksum(struct session *s, struct folder_file *file, char *file_path); void folder_list(struct session *s) { static const struct session_menu_option opts[] = { { '#', "#", "View file folder [#]" }, { 'l', "Ll", "List folders" }, { 'q', "QqXx", "Return to main menu" }, { '?', "?", "List menu options" }, }; static const char prompt_help[] = "#:View Folder L:List Q:Return ?:Help"; short n, fn; char c; 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}}File 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, "%2d %- 13.13s %s\r\n", n + 1, db->folders[n].name, db->folders[n].description); } session_flush(s); show_list = false; } c = session_menu(s, "File Folders", "Files", (char *)prompt_help, opts, nitems(opts), show_help, "Folder #", &fn); show_help = false; handle_opt: switch (c) { case 'l': show_list = true; break; case '#': check_fn: if (fn < 1 || fn > db->nfolders) { session_printf(s, "Invalid folder\r\n"); session_flush(s); break; } folder_show(s, &db->folders[fn - 1]); break; case '?': show_help = true; break; default: done = true; break; } } } void folder_show(struct session *s, struct folder *folder) { static const struct session_menu_option opts[] = { { '#', "#", "View file [#]" }, { '<', "<", "Newer page of files" }, { 'l', "Ll", "List files" }, { '>', ">", "Older page of files" }, { 'u', "Uu", "Upload new file" }, { 'q', "QqXx", "Return to main menu" }, { '?', "?", "List menu options" }, }; static const char prompt_help[] = "#:View <:Newer >:Older L:List U:Upload Q:Return ?:Help"; char prompt[6 + member_size(struct folder, name) + 8]; size_t page, pages, nfile_ids; unsigned long *file_ids = NULL; short fpp, fn; char c; bool done, show_list, show_help, find_files; page = 0; show_list = true; find_files = true; show_help = false; done = false; snprintf(prompt, sizeof(prompt), "Files:%s", folder->name); while (!done && !s->ending) { if (find_files) { folder_find_file_ids(folder, &nfile_ids, &file_ids); fpp = FILES_PER_PAGE; if (s->terminal_lines < fpp + 3) fpp = BOUND(fpp, 5, s->terminal_lines - 3); /* ceil(nfile_ids / fpp) */ pages = (nfile_ids + fpp - 1) / fpp; if (page >= pages) page = pages - 1; find_files = false; } if (show_list) { folder_list_files(s, folder, nfile_ids, file_ids, fpp, page + 1, pages); show_list = false; } c = session_menu(s, folder->description, prompt, (char *)prompt_help, opts, nitems(opts), show_help, "File #", &fn); show_help = false; handle_opt: switch (c) { case 'l': show_list = true; break; case 'u': if (folder_upload(s, folder, NULL, NULL)) find_files = true; break; case '>': case '<': if (c == '>' && page == pages - 1) { session_printf(s, "You are at the last page of files\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--; show_list = true; break; case '#': if (fn < 1 || fn > nfile_ids) { session_printf(s, "Invalid file\r\n"); session_flush(s); break; } c = folder_file_view(s, folder, file_ids[fn - 1]); if (c == FILE_VIEW_RETURN_FIND) find_files = true; break; case '?': show_help = true; break; default: done = true; break; } } if (file_ids != NULL) xfree(&file_ids); } void folder_list_files(struct session *s, struct folder *folder, size_t nfile_ids, unsigned long *file_ids, short files_per_page, unsigned long page, unsigned long pages) { char time[24]; size_t n, off, size; struct folder_file file; short ret; char *data; session_printf(s, "{{B}}%s: %s (Page %ld of %ld){{/B}}\r\n", folder->name, folder->description, page, pages); session_printf(s, "%s # Date File Description%s\r\n", ansi(s, ANSI_BOLD, ANSI_END), ansi(s, ANSI_RESET, ANSI_END)); session_flush(s); if (nfile_ids == 0) { session_printf(s, "No files here yet.\r\n"); session_flush(s); return; } off = files_per_page * (page - 1); for (n = 0; n < files_per_page; n++) { if (off + n >= nfile_ids) break; size = bile_read_alloc(folder->bile, FOLDER_FILE_RTYPE, file_ids[off + n], &data); ret = bile_unmarshall_object(folder->bile, folder_file_object_fields, nfolder_file_object_fields, data, size, &file, sizeof(file), false); xfree(&data); if (ret == BILE_ERR_NO_MEMORY) return; strftime(time, sizeof(time), "%Y-%m-%d", localtime(&file.time)); session_printf(s, "%2ld %s {{#}}%- 17.17s %- 40s\r\n", n + 1, time, file.filename, file.description); } session_flush(s); } unsigned long folder_upload(struct session *s, struct folder *folder, char *initial_filename, char *initial_description) { struct folder_file file = { 0 }; struct stat sb; struct zmodem_session *zs; struct nomodem_session *ns; char *upload_path = NULL, *file_name; size_t file_size; short error; if (!s->user) { session_printf(s, "Uploading is not available to guests.\r\n" "Please create an account first.\r\n"); session_flush(s); return 0; } if (initial_filename) strlcpy(file.filename, initial_filename, sizeof(file.filename)); file.uploader_user_id = s->user->id; upload_path = xmalloc(FILENAME_MAX); if (upload_path == NULL) return 0; session_printf(s, "{{B}}Upload New File{{/B}}\r\n"); if (s->can_nomodem) { session_printf(s, "To begin your upload, press Enter and choose a file (only one file\r\n"); session_printf(s, "can be uploaded at a time).\r\n"); } else { session_printf(s, "To begin your ZMODEM upload, press Enter and choose a file through\r\n"); session_printf(s, "your terminal program (only one file can be uploaded at a time).\r\n"); } session_printf(s, "\r\n" "You'll then be given the option to change the filename and enter a\r\n" "description for the file.\r\n" "\r\n" "To cancel your upload, press ^C a few times.\r\n\r\n"); session_flush(s); session_pause_return(s, 0, "when you are ready..."); snprintf(upload_path, FILENAME_MAX, "%s:upload-%08lx%08lx", folder->path, xorshift32(), xorshift32()); if (s->can_nomodem) { ns = nomodem_receive(s, upload_path); if (ns == NULL) { xfree(&upload_path); return 0; } } else { zs = ZCreateReceiver(s, upload_path); if (zs == NULL) { xfree(&upload_path); return 0; } zs->DoIACEscape = s->is_telnet; ZInit(zs); s->direct_output = true; } session_flush(s); session_logf(s, "[%s] Receiving uploaded file to %s", folder->name, upload_path); while (!s->ending) { if (s->can_nomodem) { if (nomodem_timed_out(ns)) goto timed_out; if (!nomodem_continue(ns)) break; } else { if (ZHaveTimedOut(zs)) { ZTimeOutProc(zs); goto timed_out; } if (!ZParse(zs)) break; } if (s->obuflen) session_flush(s); continue; timed_out: session_logf(s, "[%s] Transfer timed out, canceling", folder->name); break; } /* flush the pipes */ s->obuflen = 0; uthread_msleep(1000); session_clear_input(s); s->direct_output = false; if (s->can_nomodem) { file_size = ns->file_size; if (ns->file) { fclose(ns->file); ns->file = NULL; } } else { if (zs->file) { fclose(zs->file); zs->file = NULL; } file_size = zs->file_size; } session_printf(s, "\r\n\r\n"); if (stat(upload_path, &sb) != 0 || sb.st_size == 0) { session_logf(s, "[%s] Failed receiving upload, no temp file", folder->name); session_printf(s, "Failed receiving file, aborting.\r\n"); session_pause_return(s, CONTROL_C, "to continue..."); if (s->can_nomodem) { nomodem_finish(&ns); } else { ZDestroy(zs); zs = NULL; } xfree(&upload_path); return 0; } if (sb.st_size != file_size) { session_logf(s, "[%s] Received uploaded file of size %ld but " "supposed to be %ld, canceling", folder->name, sb.st_size, file_size); session_printf(s, "Uploaded file expected to be {{B}}%ld{{/B}} byte%s, but " "received\r\n" "{{B}}%ld{{/B}} byte%s. Deleting file and canceling upload.\r\n", file_size, file_size == 1 ? "" : "s", sb.st_size, sb.st_size == 1 ? "" : "s"); session_pause_return(s, CONTROL_C, "to continue..."); if (s->can_nomodem) { nomodem_finish(&ns); } else { ZDestroy(zs); zs = NULL; } goto file_upload_cancel; } if (s->can_nomodem) file_name = ns->file_name; else file_name = zs->file_name; strlcpy(file.filename, file_name, sizeof(file.filename)); file.size = sb.st_size; session_printf(s, "Successfully received file {{B}}%s{{/B}} of size " "{{B}}%ld{{/B}} byte%s.\r\n", file.filename, file.size, file.size == 1 ? "" : "s"); session_pause_return(s, '\r', "to continue..."); session_logf(s, "[%s] Received uploaded file %s of size %ld", folder->name, file.filename, file.size); if (s->can_nomodem) { nomodem_finish(&ns); } else { ZDestroy(zs); zs = NULL; } session_printf(s, "Calculating SHA1 checksum of file... "); session_flush(s); if (!folder_file_checksum(s, &file, upload_path)) { session_printf(s, "failed reading file!\r\n"); session_flush(s); goto file_upload_cancel; } session_output(s, "\r\n\r\n", 4); session_flush(s); if (folder_edit_file(s, folder, &file, upload_path)) goto file_upload_done; else { session_printf(s, "\r\nFailed saving file!\r\n"); session_flush(s); } file_upload_cancel: session_printf(s, "\r\n"); session_flush(s); if (upload_path[0] != '\0') { CtoPstr(upload_path); error = FSDelete(upload_path, 0); PtoCstr(upload_path); if (error) { session_logf(s, "[%s] Failed deleting temporary uploaded " "file %s: %d", folder->name, upload_path, error); } else { session_logf(s, "[%s] Canceled upload, deleted temp file %s", folder->name, upload_path); } } file_upload_done: if (file.notes) xfree(&file.notes); if (upload_path != NULL) xfree(&upload_path); return file.id; } short folder_file_view(struct session *s, struct folder *folder, unsigned long id) { static const struct session_menu_option opts[] = { { 'd', "Dd", "Download this file" }, { 'r', "Rr", "Remove this file" }, { 'e', "Ee", "Edit this file" }, { 'q', "QqXx", "Return to folder" }, { '?', "?", "List these options" }, }; static const char prompt_help[] = "D:Download R:Delete E:Edit Q:Return ?:Help"; char time[32]; char prompt[6 + member_size(struct folder, name) + 1 + member_size(struct folder_file, filename)]; char *path = NULL, *data, c; struct folder_file file; struct username_cache *uploader; struct zmodem_session *zs; struct nomodem_session *ns; struct session_menu_option *dopts = NULL; FILE *fp; size_t size; short cc, n, bret, ret = FILE_VIEW_RETURN_FIND; bool done = false, show_help = false; size = bile_read_alloc(folder->bile, FOLDER_FILE_RTYPE, id, &data); if (size == 0) panic("failed fetching message %ld: %d", id, bile_error(folder->bile)); bret = bile_unmarshall_object(folder->bile, folder_file_object_fields, nfolder_file_object_fields, data, size, &file, sizeof(file), true); xfree(&data); if (bret == BILE_ERR_NO_MEMORY) return ret; dopts = xmalloc(sizeof(opts)); if (dopts == NULL) return ret; memcpy(dopts, opts, sizeof(opts)); if (!(s->user && (s->user->is_sysop || s->user->id == file.uploader_user_id))) { /* disable deleting and editing */ for (n = 0; n < nitems(opts); n++) { if (dopts[n].ret == 'r' || dopts[n].ret == 'e') dopts[n].key[0] = '\0'; } } uploader = user_username(file.uploader_user_id); strftime(time, sizeof(time), "%Y-%m-%d %H:%M:%S", localtime(&file.time)); session_printf(s, "{{B}}Folder:{{/B}}{{#}} %s\r\n", folder->name); session_printf(s, "{{B}}File Name:{{/B}}{{#}} %s\r\n", file.filename); session_printf(s, "{{B}}Description:{{/B}}{{#}} %s\r\n", file.description); session_printf(s, "{{B}}File Size:{{/B}} %lu\r\n", file.size); session_printf(s, "{{B}}File SHA1:{{/B}} %s\r\n", file.sha1_checksum); session_printf(s, "{{B}}Uploaded:{{/B}} %s %s\r\n", time, db->config.timezone); session_printf(s, "{{B}}Uploaded By:{{/B}} %s\r\n", uploader ? uploader->username : "(unknown)"); session_flush(s); if (file.notes_size > 0) { session_printf(s, "{{B}}Notes:{{/B}}\r\n"); session_output(s, file.notes, file.notes_size); session_printf(s, "\r\n"); } session_printf(s, "\r\n[ Not viewing file contents, press 'd' to download. ]\r\n"); session_flush(s); snprintf(prompt, sizeof(prompt), "Files:%s:%s", folder->name, file.filename); while (!done && !s->ending) { c = session_menu(s, file.filename, prompt, (char *)prompt_help, dopts, nitems(opts), show_help, NULL, NULL); show_help = false; switch (c) { case 'd': path = xmalloc(FILENAME_MAX); if (path == NULL) { done = true; break; } snprintf(path, FILENAME_MAX, "%s:%s", folder->path, file.filename); fp = fopen(path, "rb"); if (!fp) { session_logf(s, "[%s] Failed opening file %s", folder->name, path); session_printf(s, "{{B}}Error:{{/B}} Failed opening file\r\n"); xfree(&path); done = true; break; } if (s->can_nomodem) { ns = nomodem_send(s, fp, file.filename); if (ns == NULL) { xfree(&path); done = true; break; } } else { zs = ZCreateSender(s, fp, file.filename); if (zs == NULL) { xfree(&path); done = true; break; } zs->DoIACEscape = s->is_telnet; ZInit(zs); s->direct_output = true; } session_logf(s, "[%s] Downloading file %s", folder->name, file.filename); for (;;) { if (s->can_nomodem) { if (nomodem_timed_out(ns)) goto timed_out; if (!nomodem_continue(ns)) break; } else { if (ZHaveTimedOut(zs)) { ZTimeOutProc(zs); goto timed_out; } if (!ZParse(zs)) break; } if (s->obuflen) session_flush(s); continue; timed_out: session_logf(s, "[%s] Transfer timed out, canceling", folder->name); break; } /* flush the pipes */ s->obuflen = 0; uthread_msleep(1000); session_clear_input(s); if (s->can_nomodem) { nomodem_finish(&ns); } else { s->direct_output = false; if (zs->file) { fclose(zs->file); zs->file = NULL; } ZDestroy(zs); zs = NULL; } xfree(&path); session_printf(s, "\r\n"); session_flush(s); session_pause_return(s, CONTROL_C, "to continue..."); break; case 'e': if (!(s->user && (s->user->is_sysop || s->user->id == file.uploader_user_id))) { session_printf(s, "Invalid option\r\n"); session_flush(s); break; } path = xmalloc(FILENAME_MAX); if (path == NULL) { done = true; break; } snprintf(path, FILENAME_MAX, "%s:%s", folder->path, file.filename); folder_edit_file(s, folder, &file, path); xfree(&path); break; case 'r': if (!(s->user && (s->user->is_sysop || s->user->id == file.uploader_user_id))) { session_printf(s, "Invalid option\r\n"); session_flush(s); break; } session_printf(s, "Are you sure you want to permanently " "delete this file? [y/N] "); session_flush(s); cc = session_input_char(s); if (cc == 'y' || c == 'Y') { session_printf(s, "%c\r\n", cc); session_flush(s); folder_delete_file(s, folder, &file); session_printf(s, "\r\n{{B}}File deleted!{{/B}}\r\n"); session_flush(s); ret = FILE_VIEW_RETURN_FIND; done = true; } else { session_printf(s, "\r\nFile not deleted.\r\n"); session_flush(s); } break; case 'q': done = true; break; case '?': show_help = true; break; } } if (file.notes != NULL) xfree(&file.notes); xfree(&dopts); return ret; } size_t folder_find_file_ids(struct folder *folder, size_t *nfile_ids, unsigned long **file_ids) { struct bile_object *o; struct folder_file file; struct file_name_map { unsigned long id; char filename[10]; /* only sorted to 10 character places */ } *name_map = NULL, tmp_map; size_t n; short i, j, ret; char *data; *nfile_ids = bile_count_by_type(folder->bile, FOLDER_FILE_RTYPE); if (*nfile_ids == 0) return 0; name_map = xcalloc(*nfile_ids, sizeof(struct file_name_map)); if (name_map == NULL) return 0; for (n = 0; (o = bile_get_nth_of_type(folder->bile, n, FOLDER_FILE_RTYPE)); n++) { if (n >= *nfile_ids) break; if (bile_read_alloc(folder->bile, FOLDER_FILE_RTYPE, o->id, &data) == 0) break; ret = bile_unmarshall_object(folder->bile, folder_file_object_fields, nfolder_file_object_fields, data, o->size, &file, sizeof(file), false); xfree(&o); xfree(&data); if (ret == 0 && bile_error(folder->bile) == BILE_ERR_NO_MEMORY) { xfree(&name_map); return 0; } name_map[n].id = file.id; strlcpy(name_map[n].filename, file.filename, sizeof(name_map[n].filename)); } /* sort by filename */ for (i = 1; i < *nfile_ids; i++) { for (j = i; j > 0; j--) { if (strcasecmp(name_map[j].filename, name_map[j - 1].filename) > 0) break; tmp_map = name_map[j]; name_map[j] = name_map[j - 1]; name_map[j - 1] = tmp_map; } } *file_ids = xcalloc(sizeof(long), *nfile_ids); if (*file_ids == NULL) { xfree(&name_map); return 0; } for (i = 0; i < *nfile_ids; i++) (*file_ids)[i] = name_map[i].id; xfree(&name_map); done: return *nfile_ids; } bool folder_file_save(struct folder *folder, struct folder_file *file, char *temp_path) { char *new_name = NULL; short ret; char *data; size_t size; bool need_move = false; if (!file->id) { need_move = true; file->id = bile_next_id(folder->bile, FOLDER_FILE_RTYPE); } if (!file->time) file->time = Time; ret = bile_marshall_object(folder->bile, folder_file_object_fields, nfolder_file_object_fields, file, &data, &size); if (ret != 0 || size == 0) { warn("failed to marshall new file object"); file->id = 0; return false; } if (bile_write(folder->bile, FOLDER_FILE_RTYPE, file->id, data, size) != size) { warn("bile_write of new file failed! %d", bile_error(folder->bile)); file->id = 0; xfree(&data); return false; } xfree(&data); bile_flush(folder->bile, true); if (need_move) { new_name = xmalloc(FILENAME_MAX); if (new_name == NULL) return false; snprintf(new_name, FILENAME_MAX, "%s:%s", folder->path, file->filename); CtoPstr(temp_path); CtoPstr(new_name); ret = Rename(temp_path, 0, new_name); PtoCstr(temp_path); PtoCstr(new_name); if (ret != 0) { warn("FSRename(%s, 0, %s) failed: %d", temp_path, new_name, ret); bile_delete(folder->bile, FOLDER_FILE_RTYPE, file->id, BILE_DELETE_FLAG_ZERO | BILE_DELETE_FLAG_PURGE); file->id = 0; xfree(&new_name); return false; } xfree(&new_name); } return true; } void folder_delete_file(struct session *s, struct folder *folder, struct folder_file *file) { char *path = NULL; short ret; bile_delete(folder->bile, FOLDER_FILE_RTYPE, file->id, BILE_DELETE_FLAG_ZERO | BILE_DELETE_FLAG_PURGE); bile_flush(folder->bile, true); path = xmalloc(FILENAME_MAX); if (path == NULL) return; snprintf(path, FILENAME_MAX, "%s:%s", folder->path, file->filename); CtoPstr(path); ret = FSDelete(path, 0); PtoCstr(path); if (ret != 0) session_logf(s, "[%s] Failed deleting %s: %d", folder->name, path, ret); xfree(&path); if (file->notes != NULL) xfree(&file->notes); session_logf(s, "[%s] Deleted file %s (%ld)", folder->name, file->filename, file->id); } bool folder_file_valid_filename(struct session *session, struct folder *folder, struct folder_file *file, char **error) { struct bile_object *o; struct folder_file tfile; size_t len, n; short ret; char *data; char c; if (file->filename[0] == '\0') { *error = xstrdup("filename cannot be empty"); return false; } len = strlen(file->filename); if (len > FOLDER_FILE_FILENAME_LENGTH) { *error = xmalloc(61); if (*error) snprintf(*error, 60, "filename cannot be more than %d " "characters", FOLDER_FILE_FILENAME_LENGTH); return false; } for (n = 0; n < len; n++) { c = file->filename[n]; if (n == 0 && c == '.') { *error = xstrdup("filename cannot start with a dot"); return false; } if (n == len - 1 && c == ' ') { *error = xstrdup("filename cannot end with a space"); return false; } if (!((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_' || c == '-' || c == '+' || c == '*' || c == ' ' || c == '.' || c == ',')) { *error = xmalloc(61); if (*error) snprintf(*error, 60, "filename cannot contain '%c' " "character", c); return false; } } for (n = 0; (o = bile_get_nth_of_type(folder->bile, n, FOLDER_FILE_RTYPE)); n++) { if (o->id == file->id) { xfree(&o); continue; } ret = bile_read_alloc(folder->bile, FOLDER_FILE_RTYPE, o->id, &data); if (ret == 0) { xfree(&o); return false; } ret = bile_unmarshall_object(folder->bile, folder_file_object_fields, nfolder_file_object_fields, data, o->size, &tfile, sizeof(tfile), false); xfree(&data); xfree(&o); if (ret == BILE_ERR_NO_MEMORY) return false; if (strcasecmp(file->filename, tfile.filename) == 0) { *error = xstrdup("filename is already taken"); return false; } } if (bile_error(folder->bile) == BILE_ERR_NO_MEMORY) return false; return true; } bool folder_edit_file(struct session *s, struct folder *folder, struct folder_file *file, char *file_path) { struct username_cache *uploader; char *tmp, *errorstr; unsigned short c; uploader = user_username(file->uploader_user_id); session_printf(s, "{{B}}Uploaded By:{{/B}} %s\r\n", uploader ? uploader->username : "(unknown)"); session_printf(s, "{{B}}Folder:{{/B}} %s\r\n", folder->name); session_printf(s, "{{B}}SHA1 Checksum:{{/B}} %s\r\n", file->sha1_checksum); session_printf(s, "{{B}}File Size:{{/B}} %ld\r\n", file->size); session_flush(s); file_annotate: for (;;) { session_printf(s, "{{B}}File Name:{{/B}} "); session_flush(s); if (file->id) { /* TODO: allow renaming and do FSRename */ session_printf(s, "{{#}}%s\r\n", file->filename); } else { tmp = session_field_input(s, FOLDER_FILE_FILENAME_LENGTH, FOLDER_FILE_FILENAME_LENGTH - 1, file->filename, false, 0); if (tmp == NULL) return false; strlcpy(file->filename, tmp, sizeof(file->filename)); xfree(&tmp); session_output(s, "\r\n", 2); session_flush(s); rtrim(file->filename, "\r\n\t "); if (!folder_file_valid_filename(s, folder, file, &errorstr)) { session_printf(s, "{{B}}Error:{{/B}} %s\r\n", errorstr); xfree(&errorstr); continue; } } break; } for (;;) { session_printf(s, "{{B}}Short Description:{{/B}} "); session_flush(s); tmp = session_field_input(s, FOLDER_FILE_DESCR_LENGTH, FOLDER_FILE_DESCR_LENGTH - 1, file->description, false, 0); if (tmp == NULL) return false; strlcpy(file->description, tmp, sizeof(file->description)); xfree(&tmp); session_output(s, "\r\n", 2); session_flush(s); rtrim(file->description, "\r\n\t "); if (file->description[0] == '\0') { session_printf(s, "{{B}}Error:{{/B}} File " "description cannot be blank (^C to cancel)\r\n"); session_flush(s); continue; } break; } for (;;) { session_printf(s, "{{B}}Notes (Optional, ^D when finished):{{/B}}\r\n"); session_flush(s); tmp = session_field_input(s, 2048, s->terminal_columns - 1, file->notes, true, 0); if (file->notes != NULL) xfree(&file->notes); file->notes = tmp; session_output(s, "\r\n", 2); session_flush(s); if (file->notes == NULL) { file->notes_size = 0; break; } rtrim(file->notes, "\r\n\t "); if (file->notes[0] == '\0') { xfree(&file->notes); file->notes = NULL; file->notes_size = 0; break; } file->notes_size = strlen(file->notes) + 1; break; } for (;;) { session_printf(s, "\r\n{{B}}(S){{/B}}ave, " "{{B}}(E){{/B}}dit again, "); if (file->id && s->user->is_sysop) session_printf(s, "Re-c{{B}}(h){{/B}}ecksum, "); session_printf(s, "or {{B}}(C){{/B}}ancel? "); session_flush(s); c = session_input_char(s); if (c == 0 || s->ending) return false; switch (c) { case 'h': case 'H': if (s->user->is_sysop) { session_printf(s, "%c\r\n", c); session_flush(s); session_printf(s, "Calculating SHA1 checksum... "); session_flush(s); if (!folder_file_checksum(s, file, file_path)) { session_printf(s, "failed reading %s!\r\n", file_path); session_flush(s); return false; } session_printf(s, "\r\nNew checksum: %s\r\n", file->sha1_checksum); session_flush(s); } break; case 's': case 'S': case 'y': session_printf(s, "%c\r\n", c); session_flush(s); /* FALLTHROUGH */ case '\n': case '\r': /* save */ session_printf(s, "Saving file... "); session_flush(s); if (folder_file_save(folder, file, file_path)) { session_logf(s, "[%s] Saved file %s", folder->name, file->filename); session_printf(s, "done\r\n"); session_flush(s); return true; } session_printf(s, "failed!\r\n"); session_flush(s); return false; case 'e': case 'E': session_printf(s, "%c\r\n", c); session_flush(s); goto file_annotate; case 'c': case 'C': session_printf(s, "%c\r\n", c); session_flush(s); return false; case CONTROL_C: return false; } } return false; } bool folder_file_checksum(struct session *s, struct folder_file *file, char *file_path) { SHA1_CTX sha1; FILE *fp; char *data; char fmt[12]; size_t rsize, size, dg, olen, tsize; data = xmalloc(512); if (data == NULL) return false; SHA1Init(&sha1); fp = fopen(file_path, "rb"); if (fp == NULL) { xfree(&data); return false; } tsize = file->size; dg = 1; while (tsize >= 10) { dg++; tsize /= 10; } /* 123456 -> "% 6lu/%lu" */ snprintf(fmt, sizeof(fmt), "%% %lulu/%%lu", dg); rsize = 0; olen = session_printf(s, fmt, rsize, file->size); session_flush(s); while (!feof(fp)) { size = fread(data, 1, 512, fp); rsize += size; SHA1Update(&sha1, (const u_int8_t *)data, size); if (rsize % 2048 == 0) uthread_yield(); if (rsize % 10240 == 0 || rsize == file->size) { session_printf(s, "%s", ansi(s, ANSI_BACK_N, (short)olen, ANSI_END)); session_printf(s, fmt, rsize, file->size); session_flush(s); } } fclose(fp); xfree(&data); SHA1End(&sha1, (char *)&file->sha1_checksum); if (rsize != file->size) return false; return true; }