static int common_pager_exit_handler(int r, const char *fpath) { // post processing switch(r) { case RET_DOSYSOPEDIT: r = FULLUPDATE; if (!check_sysop_edit_perm(fpath)) break; log_filef("log/security", LOG_CREAT, "%u %s %d %s admin edit file=%s\n", (int)now, Cdate(&now), getpid(), cuser.userid, fpath); veditfile(fpath); break; case RET_COPY2TMP: r = FULLUPDATE; if (HasUserPerm(PERM_BASIC)) { char buf[PATHLEN]; getdata(b_lines - 1, 0, "把這篇文章收入到暫存檔?[y/N] ", buf, 4, LCECHO); if (buf[0] != 'y') break; setuserfile(buf, ask_tmpbuf(b_lines - 1)); Copy(fpath, buf); } break; case RET_SELECTBRD: r = FULLUPDATE; if (currstat == READING) r = Select(); break; case RET_DOCHESSREPLAY: r = FULLUPDATE; if (HasUserPerm(PERM_BASIC)) ChessReplayGame(fpath); break; #if defined(USE_BBSLUA) && !defined(DISABLE_BBSLUA_IN_PAGER) case RET_DOBBSLUA: r = FULLUPDATE; // check permission again if (HasUserPerm(PERM_BASIC)) bbslua(fpath); break; #endif } return r; }
/* 使用者管理 */ int m_user(void) { userec_t xuser; int id; char genbuf[200]; vs_hdr("使用者設定"); usercomplete(msg_uid, genbuf); if (*genbuf) { move(2, 0); if ((id = getuser(genbuf, &xuser))) { user_display(&xuser, 1); if( HasUserPerm(PERM_ACCOUNTS) ) uinfo_query(xuser.userid, 1, id); else pressanykey(); } else { outs(err_uid); clrtoeol(); pressanykey(); } } return 0; }
static int check_sysop_edit_perm(const char *fpath) { if (!HasUserPerm(PERM_SYSOP) || strcmp(fpath, "etc/ve.hlp") == 0) return 0; if (fpath && *fpath) { if (strstr(fpath, BBSHOME) == fpath) fpath += (strlen(BBSHOME) + 1); // allow only files in board, man or home. if (!(strstr(fpath, "boards/") == fpath || strstr(fpath, "home/") == fpath || strstr(fpath, "man/") == fpath)) return 0; } #ifdef BN_SECURITY if (strcmp(currboard, BN_SECURITY) == 0) return 0; #endif // BN_SECURITY return 1; }
int Announce(void) { setutmpmode(ANNOUNCE); a_menu(BBSNAME "佈告欄", "man", ((HasUserPerm(PERM_SYSOP) ) ? SYSOP : NOBODY), 0, NULL, NULL); return 0; }
static int check_sysop_edit_perm(const char *fpath) { if (!HasUserPerm(PERM_SYSOP) || strcmp(fpath, "etc/ve.hlp") == 0) return 0; #ifdef BN_SECURITY if (strcmp(currboard, BN_SECURITY) == 0) return 0; #endif // BN_SECURITY return 1; }
static void a_showname(const menu_t * pm) { char buf[PATHLEN]; int len; int i; int sym; move(b_lines - 1, 0); snprintf(buf, sizeof(buf), "%s/%s", pm->path, pm->header[pm->now - pm->page].filename); if (dashl(buf)) { prints("此 symbolic link 名稱為 %s\n", pm->header[pm->now - pm->page].filename); if ((len = readlink(buf, buf, PATHLEN - 1)) >= 0) { buf[len] = '\0'; for (i = 0; BBSHOME[i] && buf[i] == BBSHOME[i]; i++); if (!BBSHOME[i] && buf[i] == '/') { if (HasUserPerm(PERM_BBSADM)) sym = 1; else { sym = 0; for (i++; BBSHOME "/man"[i] && buf[i] == BBSHOME "/man"[i]; i++); if (!BBSHOME "/man"[i] && buf[i] == '/') sym = 1; } if (sym) { vmsgf("此 symbolic link 指向 %s", &buf[i + 1]); } } } } else if (dashf(buf)) prints("此文章名稱為 %s", pm->header[pm->now - pm->page].filename); else if (dashd(buf)) prints("此目錄名稱為 %s", pm->header[pm->now - pm->page].filename); else outs("此項目已損毀, 建議將其刪除!"); pressanykey(); }
static void a_delete(menu_t * pm, const char *backup_dir) { char fpath[PATHLEN], buf[PATHLEN], cmd[PATHLEN]; char ans[4]; fileheader_t backup, *fhdr = &(pm->header[pm->now - pm->page]); const char *msg_errsync = "刪除檔案失敗,請退回上層目錄後再重試一次", *msg_errsync2 = "檔案可能已被它人刪除,請退回上層目錄再重進確認", *msg_errbackup = "檔案已刪除但無法備份。請至 " BN_BUGREPORT "報告您試圖刪除檔案的位置。"; snprintf(fpath, sizeof(fpath), "%s/%s", pm->path, fhdr->filename); setadir(buf, pm->path); if (fhdr->filename[0] == 'H' && fhdr->filename[1] == '.') { getdata(b_lines - 1, 1, "您確定要刪除此精華區連線嗎(Y/N)?[N] ", ans, sizeof(ans), LCECHO); if (ans[0] != 'y') return; if (delete_fileheader(buf, fhdr, pm->now + 1) == -1) { vmsg(msg_errsync); return; } } else if (dashl(fpath)) { getdata(b_lines - 1, 1, "您確定要刪除此 symbolic link 嗎(Y/N)?[N] ", ans, sizeof(ans), LCECHO); if (ans[0] != 'y') return; if (delete_fileheader(buf, fhdr, pm->now + 1) == -1) { vmsg(msg_errsync); return; } unlink(fpath); } else if (dashf(fpath)) { getdata(b_lines - 1, 1, "您確定要刪除此檔案嗎(Y/N)?[N] ", ans, sizeof(ans), LCECHO); if (ans[0] != 'y') return; if (delete_fileheader(buf, fhdr, pm->now + 1) == -1) { vmsg(msg_errsync); return; } switch(delete_file_content(buf, fhdr, backup_dir, NULL, 0)) { case DELETE_FILE_CONTENT_BACKUP_FAILED: vmsg(msg_errbackup); break; case DELETE_FILE_CONTENT_FAILED: vmsg(msg_errsync2); break; default: #ifndef USE_TIME_CAPSULE // When not using time capsule, .DIR content may be changed in // board (BN_DELETE/BN_JUNK) and need to be changed. However // since that's going to be deprecated in future, let's have a // simple workaround here. if (backup_dir) { const char *bn = NULL; if (strstr(backup_dir, "/" BN_JUNK "/")) bn = BN_JUNK; else if (strstr(backup_dir, "/" BN_DELETED "/")) bn = BN_DELETED; if (bn) setbtotal(getbnum(bn)); } #endif break; } } else if (dashd(fpath)) { // TODO(hungte) We should create a top level folder and move everything // inside. // XXX we also check PERM_MAILLIMIT here because RMAIL // may be not trusted... const char *save_bn = ( HasUserPerm(PERM_MAILLIMIT) && (currstat & RMAIL) ) ? BN_JUNK : BN_DELETED; getdata(b_lines - 1, 1, "您確定要刪除整個目錄嗎(Y/N)?[N] ", ans, sizeof(ans), LCECHO); if (ans[0] != 'y') return; if (delete_fileheader(buf, fhdr, pm->now + 1) == -1) { vmsg(msg_errsync); return; } setapath(buf, save_bn); // XXX because this directory will hold folders from entire site, // let's allow it to use a large set of file names. if (stampadir(buf, &backup, 1) != 0) { vmsg("抱歉,系統目前無法刪除資料,請通知站務人員"); return; } snprintf(cmd, sizeof(cmd), "rm -rf %s;/bin/mv -f %s %s", buf, fpath, buf); system(cmd); strlcpy(backup.owner, cuser.userid, sizeof(backup.owner)); strcpy(backup.title, "◆"); strlcpy(backup.title + 2, fhdr->title + 2, sizeof(backup.title) - 3); // restrict access if source is hidden if ((fhdr->filemode & FILE_BM) || (fhdr->filemode & FILE_HIDE)) backup.filemode |= FILE_BM; /* merge setapath(buf, save_bn); setadir(buf, buf); */ snprintf(buf, sizeof(buf), "man/boards/%c/%s/" FN_DIR, *save_bn, save_bn); append_record(buf, &backup, sizeof(backup)); } else { /* Ptt 損毀的項目 */ getdata(b_lines - 1, 1, "您確定要刪除此損毀的項目嗎(Y/N)?[N] ", ans, sizeof(ans), LCECHO); if (ans[0] != 'y') return; if (delete_fileheader(buf, fhdr, pm->now + 1) == -1) return; } pm->num--; }
int a_menu_rec(const char *maintitle, const char *path, int lastlevel, int lastbid, char *trans_buffer, a_menu_session_t *sess, const int *preselect, // we don't change root's value (but may change root pointer) // we may change parent's value (but never change parent pointer) const menu_t *root, menu_t* const parent) { menu_t me = {0}; char fname[PATHLEN]; int ch, returnvalue = FULLUPDATE; assert(sess); // prevent deep resursive directories if (strlen(path) + FNLEN >= PATHLEN) { // it is not save to enter such directory. return returnvalue; } if(trans_buffer) trans_buffer[0] = '\0'; if (parent) { parent->next = &me; } else { assert(root == NULL); root = &me; } me.header_size = p_lines; me.header = (fileheader_t *) calloc(me.header_size, FHSZ); me.path = path; strlcpy(me.mtitle, maintitle, sizeof(me.mtitle)); setadir(fname, me.path); me.num = get_num_records(fname, FHSZ); me.bid = lastbid; /* 精華區-tree 中部份結構屬於 cuser ==> BM */ if (!(me.level = lastlevel)) { char *ptr; // warning: this is only valid for me.level. // is_uBM should not do anything except returning test result: // for ex, setting user BM permission automatically. // such extra behavior will result in any sub-op to have PERM_BM // ability, which leads to entering BM board without authority. // Thanks to mtdas@ptt for reporting this exploit. if (HasBasicUserPerm(PERM_LOGINOK) && !HasUserPerm(PERM_NOCITIZEN) && (ptr = strrchr(me.mtitle, '['))) me.level = is_uBM(ptr + 1, cuser.userid); } me.page = A_INVALID_PAGE; if (preselect && !*preselect) preselect = NULL; me.now = preselect ? (*preselect -1) : 0; for (;;) { if (me.now >= me.num) me.now = me.num - 1; if (me.now < 0) me.now = 0; if (me.now < me.page || me.now >= me.page + me.header_size) { me.page = me.now - ((me.page == 10000 && me.now > p_lines / 2) ? (p_lines / 2) : (me.now % p_lines)); if (!a_showmenu(&me)) { // some directories are invalid, restart! sess->bReturnToRoot = 1; break; } } if (preselect && *preselect && preselect[1]) { // if this is not the last preselect entry, enter it ch = KEY_ENTER; } else { ch = cursor_key(2 + me.now - me.page, 0); } if (ch == 'q' || ch == 'Q' || ch == KEY_LEFT) break; // TODO maybe we should let 1-9=simple search and z=tree-search // TODO or let 'z' prefix means 'back to root' if ((ch >= '1' && ch <= '9') || (ch == 'z' || ch == 'Z')) { int n = a_multi_search_num(ch, sess); me.page = A_INVALID_PAGE; if (n > 0) { // simple (single) selection me.now = n-1; me.page = 10000; // I don't know what's the magic value 10000... } else if (n == 0 && sess->z_indexes[0] == 0) { // empty/invalid input } else { // n == 0 with multiple selects preselect = sess->z_indexes; if (*preselect < 0) { // return to root first? if (parent) { sess->bReturnToRoot = 1; return DONOTHING; } // already in root preselect ++; } // handle first preselect (maybe zero due to previous 'already in root') if (*preselect > 0) me.now = *preselect - 1; else preselect = NULL; } continue; } switch (ch) { case KEY_UP: case 'k': if (--me.now < 0) me.now = me.num - 1; break; case KEY_DOWN: case 'j': if (++me.now >= me.num) me.now = 0; break; case KEY_PGUP: case Ctrl('B'): if (me.now >= p_lines) me.now -= p_lines; else if (me.now > 0) me.now = 0; else me.now = me.num - 1; break; case ' ': case KEY_PGDN: case Ctrl('F'): if (me.now < me.num - p_lines) me.now += p_lines; else if (me.now < me.num - 1) me.now = me.num - 1; else me.now = 0; break; case KEY_HOME: case '0': me.now = 0; break; case KEY_END: case '$': me.now = me.num - 1; break; case '?': case '/': if(me.num) { me.now = a_searchtitle(&me, ch == '?'); me.page = A_INVALID_PAGE; } break; case 'h': a_showhelp(me.level); me.page = A_INVALID_PAGE; break; case Ctrl('W'): a_where_am_i(root, me.now, me.header[me.now - me.page].title); vmsg(NULL); me.page = A_INVALID_PAGE; break; case 'e': case 'E': snprintf(fname, sizeof(fname), "%s/%s", path, me.header[me.now - me.page].filename); if (dashf(fname) && me.level >= MANAGER) { int edflags = 0; *quote_file = 0; # ifdef BN_BBSMOVIE if (me.bid && strcmp(getbcache(me.bid)->brdname, BN_BBSMOVIE) == 0) { edflags |= EDITFLAG_UPLOAD; edflags |= EDITFLAG_ALLOWLARGE; } # endif // BN_BBSMOVIE if (vedit2(fname, NA, NULL, edflags) != -1) { char fpath[PATHLEN]; fileheader_t fhdr; strlcpy(fpath, path, sizeof(fpath)); stampfile(fpath, &fhdr); unlink(fpath); strlcpy(fhdr.filename, me.header[me.now - me.page].filename, sizeof(fhdr.filename)); strlcpy(me.header[me.now - me.page].owner, cuser.userid, sizeof(me.header[me.now - me.page].owner)); setadir(fpath, path); substitute_record(fpath, me.header + me.now - me.page, sizeof(fhdr), me.now + 1); } me.page = A_INVALID_PAGE; } break; case 't': case 'c': if (me.now < me.num) { if (!isvisible_man(&me)) break; snprintf(fname, sizeof(fname), "%s/%s", path, me.header[me.now - me.page].filename); /* XXX: dirty fix 應該要改成如果發現該目錄裡面有隱形目錄的話才拒絕. 不過這樣的話須要整個搜一遍, 而且目前判斷該資料是目錄 還是檔案竟然是用 fstat(2) 而不是直接存在 .DIR 內 |||b 須等該資料寫入 .DIR 內再 implement才有效率. */ if( !me.level && !HasUserPerm(PERM_SYSOP) && (me.bid==0 || !is_BM_cache(me.bid)) && dashd(fname) ) vmsg("只有板主才可以拷貝目錄唷!"); else a_copyitem(fname, me.header[me.now - me.page].title, 0, 1); me.page = A_INVALID_PAGE; /* move down */ if (++me.now >= me.num) me.now = 0; break; } case KEY_ENTER: case KEY_RIGHT: case 'r': if (me.now >= me.num || me.now < 0) { preselect = NULL; continue; } else { fileheader_t *fhdr = &me.header[me.now - me.page]; const int *newselect = preselect ? preselect+1 : NULL; preselect = NULL; if (!isvisible_man(&me)) break; #ifdef DEBUG vmsgf("%s/%s", &path[11], fhdr->filename);; #endif snprintf(fname, sizeof(fname), "%s/%s", path, fhdr->filename); if (dashf(fname)) { int more_result; while ((more_result = more(fname, YEA))) { /* Ptt 範本精靈 plugin */ if (trans_buffer && (currstat == EDITEXP || currstat == OSONG)) { char ans[4]; move(22, 0); clrtoeol(); getdata(22, 1, currstat == EDITEXP ? "要把範例加入到文章內嗎?[y/N]" : "確定要選這篇嗎?[y/N]", ans, sizeof(ans), LCECHO); if (ans[0] == 'y') { strlcpy(trans_buffer, fname, PATHLEN); sess->bReturnToRoot = 1; if (currstat == OSONG) { log_filef(FN_USSONG, LOG_CREAT, "%s\n", fhdr->title); } free(me.header); return FULLUPDATE; } } if (more_result == READ_PREV) { if (--me.now < 0) { me.now = 0; break; } } else if (more_result == READ_NEXT) { if (++me.now >= me.num) { me.now = me.num - 1; break; } /* we only load me.header_size pages */ if (me.now - me.page >= me.header_size) break; } else break; if (!isvisible_man(&me)) break; snprintf(fname, sizeof(fname), "%s/%s", path, me.header[me.now - me.page].filename); if (!dashf(fname)) break; } } else if (dashd(fname)) { returnvalue = a_menu_rec(me.header[me.now - me.page].title, fname, me.level, me.bid, trans_buffer, sess, newselect, root, &me); if (returnvalue == DONOTHING) { // DONOTHING will only be caused by previous a_multi_search_num + preselect. assert(sess->bReturnToRoot); if (!parent) { // we've reached root menu! assert(sess->z_indexes[0] == -1); sess->bReturnToRoot = 0; returnvalue = FULLUPDATE; preselect = sess->z_indexes+1; // skip first 'return to root' if (*preselect > 0) me.now = *preselect-1; } } else { returnvalue = FULLUPDATE; } me.next = NULL; /* Ptt 強力跳出recursive */ if (sess->bReturnToRoot) { free(me.header); return returnvalue; } } me.page = A_INVALID_PAGE; } break; case 'F': case 'U': if (me.now < me.num) { fileheader_t *fhdr = &me.header[me.now - me.page]; if (!isvisible_man(&me)) break; snprintf(fname, sizeof(fname), "%s/%s", path, fhdr->filename); if (HasBasicUserPerm(PERM_LOGINOK) && dashf(fname)) { a_forward(path, fhdr, ch /* == 'U' */ ); /* By CharlieL */ } else vmsg("無法轉寄此項目"); me.page = A_INVALID_PAGE; } break; } if (me.level >= MANAGER) { switch (ch) { case 'n': a_newitem(&me, ADDITEM); me.page = A_INVALID_PAGE; break; case 'g': a_newitem(&me, ADDGROUP); me.page = A_INVALID_PAGE; break; case 'p': a_pasteitem(&me, 1); me.page = A_INVALID_PAGE; break; case 'f': a_editsign(&me); me.page = A_INVALID_PAGE; break; case Ctrl('P'): a_pastetagpost(&me, -1); returnvalue = DIRCHANGED; me.page = A_INVALID_PAGE; break; case Ctrl('A'): a_pastetagpost(&me, 1); returnvalue = DIRCHANGED; me.page = A_INVALID_PAGE; break; case 'a': a_appenditem(&me, 1); me.page = A_INVALID_PAGE; break; } if (me.num) switch (ch) { case 'm': a_moveitem(&me); me.page = A_INVALID_PAGE; break; case 'D': /* Ptt me.page = -1; */ a_delrange(&me, sess->backup_dir); me.page = A_INVALID_PAGE; break; case 'd': a_delete(&me, sess->backup_dir); me.page = A_INVALID_PAGE; break; case 'H': a_hideitem(&me); me.page = A_INVALID_PAGE; break; case 'T': a_newtitle(&me); me.page = A_INVALID_PAGE; break; #ifdef CHESSCOUNTRY case 'L': a_setchesslist(&me); break; #endif } } if (me.level >= SYSOP) { switch (ch) { case 'N': a_showname(&me); me.page = A_INVALID_PAGE; break; } } } free(me.header); return returnvalue; }
static int search_key_user(const char *passwdfile, int mode) { userec_t user; int ch; int unum = 0; FILE *fp1 = fopen(passwdfile, "r"); char friendfile[PATHLEN]="", key[22], *keymatch; int keytype = 0; int isCurrentPwd; assert(fp1); isCurrentPwd = (strcmp(passwdfile, FN_PASSWD) == 0); clear(); if (!mode) { getdata(0, 0, "請輸入id :", key, sizeof(key), DOECHO); } else { // improved search vs_hdr("關鍵字搜尋"); outs("搜尋欄位: [0]全部 1.ID 2.姓名 3.暱稱 4.地址 5.Mail 6.IP 7.職業 8.認證\n"); getdata(2, 0, "要搜尋哪種資料?", key, 2, NUMECHO); if (isascii(key[0]) && isdigit(key[0])) keytype = key[0] - '0'; if (keytype < 0 || keytype > 8) keytype = 0; getdata(3, 0, "請輸入關鍵字: ", key, sizeof(key), DOECHO); } if(!key[0]) { fclose(fp1); return 0; } vs_hdr(key); // <= or < ? I'm not sure... while ((fread(&user, sizeof(user), 1, fp1)) > 0 && unum++ < MAX_USERS) { // skip empty records if (!user.userid[0]) continue; if (!(unum & 0xFF)) { vs_hdr(key); prints("第 [%d] 筆資料\n", unum); refresh(); } // XXX 這裡會取舊資料,要小心 PWD 的 upgrade if (!upgrade_passwd(&user)) continue; keymatch = NULL; if (!mode) { // only verify id if (!strcasecmp(user.userid, key)) keymatch = user.userid; } else { // search by keytype if ((!keytype || keytype == 1) && DBCS_strcasestr(user.userid, key)) keymatch = user.userid; else if ((!keytype || keytype == 2) && DBCS_strcasestr(user.realname, key)) keymatch = user.realname; else if ((!keytype || keytype == 3) && DBCS_strcasestr(user.nickname, key)) keymatch = user.nickname; else if ((!keytype || keytype == 4) && DBCS_strcasestr(user.address, key)) keymatch = user.address; else if ((!keytype || keytype == 5) && strcasestr(user.email, key)) // not DBCS. keymatch = user.email; else if ((!keytype || keytype == 6) && strcasestr(user.lasthost, key)) // not DBCS. keymatch = user.lasthost; else if ((!keytype || keytype == 7) && DBCS_strcasestr(user.career, key)) keymatch = user.career; else if ((!keytype || keytype == 8) && DBCS_strcasestr(user.justify, key)) keymatch = user.justify; } if(keymatch) { vs_hdr(key); prints("第 [%d] 筆資料\n", unum); refresh(); user_display(&user, 1); // user_display does not have linefeed in tail. if (isCurrentPwd && HasUserPerm(PERM_ACCOUNTS)) uinfo_query(user.userid, 1, unum); else outs("\n"); // XXX don't trust 'user' variable after here // because uinfo_query may have changed it. outs(ANSI_COLOR(44) " 空白鍵" \ ANSI_COLOR(37) ":搜尋下一個 " \ ANSI_COLOR(33)" Q" ANSI_COLOR(37)": 離開"); outs(mode ? " A: add to namelist " ANSI_RESET " " : " S: 取用備份資料 " ANSI_RESET " "); while (1) { while ((ch = vkey()) == 0) ; if (ch == 'a' || ch=='A' ) { if(!friendfile[0]) { friend_special(); setfriendfile(friendfile, FRIEND_SPECIAL); } friend_add(user.userid, FRIEND_SPECIAL, keymatch); break; } if (ch == ' ') break; if (ch == 'q' || ch == 'Q') { fclose(fp1); return 0; } if (ch == 's' && !mode) { if (retrieve_backup(&user) >= 0) { fclose(fp1); vmsg("已成功\取用備份資料。"); return 0; } else { vmsg("錯誤: 取用備份資料失敗。"); } } } }
static int common_pager_key_handler(int ch, void *ctx) { switch(ch) { // Special service keys case 'z': if (!HasUserPerm(PERM_BASIC)) break; return RET_DOCHESSREPLAY; #if defined(USE_BBSLUA) && !defined(DISABLE_BBSLUA_IN_PAGER) case 'L': case 'l': if (!HasUserPerm(PERM_BASIC)) break; return RET_DOBBSLUA; #endif // Query information and file touch case 'Q': return RET_DOQUERYINFO; case Ctrl('T'): if (!HasUserPerm(PERM_BASIC)) break; return RET_COPY2TMP; case 'E': // for early check, skip file name (must check again later) if (!check_sysop_edit_perm("")) break; return RET_DOSYSOPEDIT; // Making Response case '%': case 'X': return RET_DORECOMMEND; case 'r': case 'R': return RET_DOREPLY; case 'Y': case 'y': return RET_DOREPLYALL; // Special Navigation case 's': if (!HasUserPerm(PERM_BASIC) || currstat != READING) break; return RET_SELECTBRD; case '#': if (!HasUserPerm(PERM_BASIC) || currstat != READING) break; return RET_SELECTAID; /* ------- SOB THREADED NAVIGATION EXITING KEYS ------- */ // I'm not sure if these keys are all invented by SOB, // but let's honor their names. // Kaede, Raw, Izero, woju - you are all TWBBS heroes // -- by piaip, 2008. case 'A': return AUTHOR_PREV; case 'a': return AUTHOR_NEXT; case 'F': case 'f': return READ_NEXT; case 'B': case 'b': return READ_PREV; /* from Kaede, thread reading */ case ']': case '+': return RELATE_NEXT; case '[': case '-': return RELATE_PREV; case '=': return RELATE_FIRST; } return DONOTHING; }