summaryrefslogblamecommitdiffstats
path: root/mbbsd/register.c
blob: d0470fb1310d22434f28f562b02faa86a44da2ca (plain) (tree)
1
2
3
4
5
6
7
8
          
                
 
                                                                      



                                         


                                      




                                                                                                         



                                                                    



                                                                            
      




                                    
 


                                  

                                 
                               
                        
                       
                        


                         
                                 



              
                                       
   
                                            
 
                       

                       
           
                               




                                    

 


                                                                            

                                              







































                                                                          
   
                               
 
                               
                 

                                         
                 
 




                                           



                                                            

                 


                                                            


             

                        






















                                                                            

                           















                                                                         


                                                                


                

                       



                                       



                           


















                                                                      

                         

              




                           
















                                                                            




                                      
          
                                                        


                          
                                                       


                                                                   
                      

                                                             
                                             
                                                          
                                           
                          
 

                                                             
                  
                                                                 






                                                                         
   
                                                                         
 
                                

                                                    
                                                                      
                                                                   
                                                       
 







                                          
     


               









































































                                                                               
                                              








                                                    

                                    




















































                                                                             
 
   
                                  

                               
                                        
                         

                          
                            
 
                
 






                                             
                                                
                                              
                                             

                                                                          



                                              
 
                                                               

                                                                     
                          
 
                                                             

                                                    

                                                                


             
 



                                      
                  
 
                                 
                                          
                        
                                                           

                


                                                    
                               
 







                                                     
                    

               

 
                                                                             
                                             
                                                                             
 
    
                  


                                    
                                 
                                  
 


                                  
                                                                    



                                    
                                                                            




                                              
 
                    
                                         















                                                                  

                             
               


                       
                         
                                                       

                    

                                                
                                        
 
                                 
                                                                         
                                                   
                                                              
                                                                          
         
                                                                                                     
                                                      
                  

                                                                 

              


                  

                                                       
            

                         
                                                       

                    
                                



                                                                               
                                                    

                                                     


                                                                      
                                                  
                                                                         
                                                        
                        



                                                    
                                                             

              
                               
                            
 









































                                                                               
 
























                                                                        



                                                

                
                                    

 




                     





                                                             

















                                                                       


                                                   











                                              
 



                                                                             
    
                    
 

                     
 
                                  





                                                       
                                      



                           
                                                                


                                                       




                                                 
                            
                 
                       
 
                                   

                                                       
                   
                                                                   

                                                       
 
                    

                                                            
                                 
      
     
 
 






































                                                                       

           
                                                                
 
                                                                                     































                                                                          

                                                    






























































                                                                                               

                                                   
                                                                     
         























                                                                              
                                     

                                
                          




                                                     





                                                   

                      
                  





                                                   
                                                     


                         
                  

                                                      
     













                                                                                        
     

                                                                                        
      

                                         
     
 


                                                  
 











                                                                
                                                       















































































                                                                                                 
                                                               


                                                          
                                                          













                                                       
                                                         


                                  
                                                     










































































































                                                                                                               

                           

















                                                           

                                              






                                   
















































































































                                                                            









                                                                           
                        




                                                               




                                                                                             













                                              








                                     



                                                                             
































                                                                            
                                                    
                
                                                     
                
                                               





                                                     




                                                                                           
 

                                                          

                              

                                                                                           


                                   

                                                                        


     
                                                                               












                                                   
                                                    




                                





                                                                         


                              
                                                     


                          

                                                      
                                                


                                                                       

                           
                                      
 
               
 


                                                          

                                                      
                                            






                                                                         

 
                 



































                                                          
                 

















                                                                  
           













                                                                             
















































                                                                             
                                                                            



































































































                                                                            
                 


                          
      













                                                               

                                                        





                                                                 
                                    












                                                    
                 


                          
      



                                             
                                             





                                                                      
                                      












                                        
                 


                                        
      












































































                                                                         



                                      






























                                                                           
    
                                          
















                                    



                                                       


















                                                   
                 
























                                                                
                               

































                                              



                                             


                  
















































                                                                       








                                                       





















































































































































































































































































                                                                                               
                                       































                                                      
                                        



                  






                                                                             























































































                                                                    


                                                                             




                                                                             







































































































































                                                                            
                                    
 






                                                                     
 
















                                                                              
 
                                       
 
                                                
                                                                               















                                                                              
 







                                                                      
 


                                                                     

                       

                                                                               
      
 






                                                                          
 


                                                                                 
 
                                                           
                                                 
 







                                                                      
                                                                   



                               
     
 

                  
 
                        
                                                                    

                  

 


                                                                             

                                      
                             

















































































































                                                                                

                                                      















































































































































































































                                                                                           
                                                          


                                       
                                                                           


                                                                  
                                                                

























                                                          







                                   







                                   



                                                 
                       



                                      












                                                 












                                                 
      
               

                   
                            
                                                                        
                                      
 
                      
                                       

                                  





                                             
     


                                                                   



                                                 

















                                                                    
                       






                  
                   

                                                      
     





                                                              
                       

 

           
/* $Id$ */
#include "bbs.h"

#define FN_REGISTER_LOG  "register.log" // global registration history
#define FN_JUSTIFY   "justify"
#define FN_JUSTIFY_WAIT  "justify.wait"
#define FN_REJECT_NOTIFY "justify.reject"

// Regform1 file name (deprecated)
#define fn_register "register.new"

// New style (Regform2) file names:
#define FN_REGFORM  "regform"   // registration form in user home
#define FN_REGFORM_LOG  "regform.log"   // regform history in user home
#define FN_REQLIST  "reg.wait"  // request list file, in global directory (replacing fn_register)

// #define USE_REGFORM2 // enable if you want to try RegForm2 system
// #define DBG_DISABLE_CHECK    // disable all input checks
// #define DBG_DRYRUN   // Dry-run test (mainly for RegForm2)

////////////////////////////////////////////////////////////////////////////
// Password Hash
////////////////////////////////////////////////////////////////////////////

char *
genpasswd(char *pw)
{
    if (pw[0]) {
    char            saltc[2], c;
    int             i;

    i = 9 * getpid();
    saltc[0] = i & 077;
    saltc[1] = (i >> 6) & 077;

    for (i = 0; i < 2; i++) {
        c = saltc[i] + '.';
        if (c > '9')
        c += 7;
        if (c > 'Z')
        c += 6;
        saltc[i] = c;
    }
    return fcrypt(pw, saltc);
    }
    return "";
}

// NOTE it will clean string in "plain"
int
checkpasswd(const char *passwd, char *plain)
{
    int             ok;
    char           *pw;

    ok = 0;
    pw = fcrypt(plain, passwd);
    if(pw && strcmp(pw, passwd)==0)
    ok = 1;
    memset(plain, 0, strlen(plain));

    return ok;
}

////////////////////////////////////////////////////////////////////////////
// Value Validation
////////////////////////////////////////////////////////////////////////////
static int 
HaveRejectStr(const char *s, const char **rej)
{
    int     i;
    char    *ptr, *rejectstr[] =
    {"幹", "阿", "不", "你媽", "某", "笨", "呆", "..", "xx",
     "你管", "管我", "猜", "天才", "超人", 
     "ㄅ", "ㄆ", "ㄇ", "ㄈ", "ㄉ", "ㄊ", "ㄋ", "ㄌ", "ㄍ", "ㄎ", "ㄏ",
     "ㄐ", "ㄑ", "ㄒ", "ㄓ",/*"ㄔ",*/    "ㄕ", "ㄖ", "ㄗ", "ㄘ", "ㄙ",
     "ㄧ", "ㄨ", "ㄩ", "ㄚ", "ㄛ", "ㄜ", "ㄝ", "ㄞ", "ㄟ", "ㄠ", "ㄡ",
     "ㄢ", "ㄣ", "ㄤ", "ㄥ", "ㄦ", NULL};

    if( rej != NULL )
    for( i = 0 ; rej[i] != NULL ; ++i )
        if( strstr(s, rej[i]) )
        return 1;

    for( i = 0 ; rejectstr[i] != NULL ; ++i )
    if( strstr(s, rejectstr[i]) )
        return 1;

    if( (ptr = strstr(s, "ㄔ")) != NULL ){
    if( ptr != s && strncmp(ptr - 1, "都市", 4) == 0 )
        return 0;
    return 1;
    }
    return 0;
}

static int
removespace(char *s)
{
    int             i, index;

    for (i = 0, index = 0; s[i]; i++) {
    if (s[i] != ' ')
        s[index++] = s[i];
    }
    s[index] = '\0';
    return index;
}

int
bad_user_id(const char *userid)
{
    if(!is_validuserid(userid))
    return 1;

    if (strcasecmp(userid, str_new) == 0)
    return 1;

#ifdef NO_GUEST_ACCOUNT_REG
    if (strcasecmp(userid, STR_GUEST) == 0)
    return 1;
#endif

    /* in2: 原本是用strcasestr,
            不過有些人中間剛剛好出現這個字應該還算合理吧? */
    if( strncasecmp(userid, "fuck", 4) == 0 ||
        strncasecmp(userid, "shit", 4) == 0 )
    return 1;

    /*
     * while((ch = *(++userid))) if(not_alnum(ch)) return 1;
     */
    return 0;
}

static char *
isvalidname(char *rname)
{
#ifdef FOREIGN_REG
    return NULL;
#else
    const char    *rejectstr[] =
    {"肥", "胖", "豬頭", "小白", "小明", "路人", "老王", "老李", "寶貝",
     "先生", "帥哥", "老頭", "小姊", "小姐", "美女", "小妹", "大頭", 
     "公主", "同學", "寶寶", "公子", "大頭", "小小", "小弟", "小妹",
     "妹妹", "嘿", "嗯", "爺爺", "大哥", "無",
     NULL};
    if( removespace(rname) && rname[0] < 0 &&
    strlen(rname) >= 4 &&
    !HaveRejectStr(rname, rejectstr) &&
    strncmp(rname, "小", 2) != 0   && //起頭是「小」
    strncmp(rname, "我是", 4) != 0 && //起頭是「我是」
    !(strlen(rname) == 4 && strncmp(&rname[2], "兒", 2) == 0) &&
    !(strlen(rname) >= 4 && strncmp(&rname[0], &rname[2], 2) == 0))
    return NULL;
    return "您的輸入不正確";
#endif

}

static char *
isvalidcareer(char *career)
{
#ifndef FOREIGN_REG
    const char    *rejectstr[] = {NULL};
    if (!(removespace(career) && career[0] < 0 && strlen(career) >= 6) ||
    strcmp(career, "家裡") == 0 || HaveRejectStr(career, rejectstr) )
    return "您的輸入不正確";
    if (strcmp(&career[strlen(career) - 2], "大") == 0 ||
    strcmp(&career[strlen(career) - 4], "大學") == 0 ||
    strcmp(career, "學生大學") == 0)
    return "麻煩請加學校系所";
    if (strcmp(career, "學生高中") == 0)
    return "麻煩輸入學校名稱";
#else
    if( strlen(career) < 6 )
    return "您的輸入不正確";
#endif
    if (strstr(career, "學") && strstr(career, "系") &&
    (strstr(career, "畢") ==0 && strstr(career, "肄") == 0))
    return "請加上年級";
    return NULL;
}

static char *
isvalidaddr(char *addr)
{
    const char    *rejectstr[] =
    {"地球", "銀河", "火星", NULL};

#ifdef DBG_DISABLE_CHECK
    return NULL;
#endif // DBG_DISABLE_CHECK

    // addr[0] > 0: check if address is starting by Chinese.
    if (!removespace(addr) || strlen(addr) < 15) 
    return "這個地址似乎並不完整";
    if (strstr(addr, "信箱") != NULL || strstr(addr, "郵政") != NULL) 
    return "抱歉我們不接受郵政信箱";
    if ((strstr(addr, "市") == NULL && strstr(addr, "巿") == NULL &&
     strstr(addr, "縣") == NULL && strstr(addr, "室") == NULL) ||
    HaveRejectStr(addr, rejectstr)             ||
    strcmp(&addr[strlen(addr) - 2], "段") == 0 ||
    strcmp(&addr[strlen(addr) - 2], "路") == 0 ||
    strcmp(&addr[strlen(addr) - 2], "巷") == 0 ||
    strcmp(&addr[strlen(addr) - 2], "弄") == 0 ||
    strcmp(&addr[strlen(addr) - 2], "區") == 0 ||
    strcmp(&addr[strlen(addr) - 2], "市") == 0 ||
    strcmp(&addr[strlen(addr) - 2], "街") == 0    )
    return "這個地址似乎並不完整";
    return NULL;
}

static char *
isvalidphone(char *phone)
{
    int     i;

#ifdef DBG_DISABLE_CHECK
    return NULL;
#endif // DBG_DISABLE_CHECK

    for( i = 0 ; phone[i] != 0 ; ++i )
    if( !isdigit((int)phone[i]) )
        return "請不要加分隔符號";
    if (!removespace(phone) || 
    strlen(phone) < 9 || 
    strstr(phone, "00000000") != NULL ||
    strstr(phone, "22222222") != NULL    ) {
    return "這個電話號碼並不正確(請含區碼)" ;
    }
    return NULL;
}


////////////////////////////////////////////////////////////////////////////
// Account Expiring
////////////////////////////////////////////////////////////////////////////

/* -------------------------------- */
/* New policy for allocate new user */
/* (a) is the worst user currently  */
/* (b) is the object to be compared */
/* -------------------------------- */
static int
compute_user_value(const userec_t * urec, time4_t clock)
{
    int             value;

    /* if (urec) has XEMPT permission, don't kick it */
    if ((urec->userid[0] == '\0') || (urec->userlevel & PERM_XEMPT)
    /* || (urec->userlevel & PERM_LOGINOK) */
    || !strcmp(STR_GUEST, urec->userid))
    return 999999;
    value = (clock - urec->lastlogin) / 60; /* minutes */

    /* new user should register in 30 mins */
    // XXX 目前 new acccount 並不會在 utmp 裡放 str_new...
    if (strcmp(urec->userid, str_new) == 0)
    return 30 - value;

#if 0
    if (!urec->numlogins)   /* 未 login 成功者,不保留 */
    return -1;
    if (urec->numlogins <= 3)   /* #login 少於三者,保留 20 天 */
    return 20 * 24 * 60 - value;
#endif
    /* 未完成註冊者,保留 15 天 */
    /* 一般情況,保留 120 天 */
    return (urec->userlevel & PERM_LOGINOK ? 120 : 15) * 24 * 60 - value;
}

int
check_and_expire_account(int uid, const userec_t * urec, int expireRange)
{
    char            genbuf[200];
    int             val;
    if ((val = compute_user_value(urec, now)) < 0) {
    snprintf(genbuf, sizeof(genbuf), "#%d %-12s %15.15s %d %d %d",
         uid, urec->userid, ctime4(&(urec->lastlogin)) + 4,
         urec->numlogins, urec->numposts, val);

    // 若超過 expireRange 則砍人,
    // 不然就 return 0
    if (-val > expireRange)
    {
        log_usies("DATED", genbuf);
        // log_usies("CLEAN", genbuf);
        kill_user(uid, urec->userid);
    } else val = 0;
    }
    return val;
}

////////////////////////////////////////////////////////////////////////////
// Regcode Support
////////////////////////////////////////////////////////////////////////////

#define REGCODE_INITIAL "v6" // always 2 characters

static char *
getregfile(char *buf)
{
    // not in user's home because s/he could zip his/her home
    snprintf(buf, PATHLEN, "jobspool/.regcode.%s", cuser.userid);
    return buf;
}

static char *
makeregcode(char *buf)
{
    char    fpath[PATHLEN];
    int     fd, i;
    // prevent ambigious characters: oOlI
    const char *alphabet = "qwertyuipasdfghjkzxcvbnmoQWERTYUPASDFGHJKLZXCVBNM";

    /* generate a new regcode */
    buf[13] = 0;
    buf[0] = REGCODE_INITIAL[0];
    buf[1] = REGCODE_INITIAL[1];
    for( i = 2 ; i < 13 ; ++i )
    buf[i] = alphabet[random() % strlen(alphabet)];

    getregfile(fpath);
    if( (fd = open(fpath, O_WRONLY | O_CREAT, 0600)) == -1 ){
    perror("open");
    exit(1);
    }
    write(fd, buf, 13);
    close(fd);

    return buf;
}

static char *
getregcode(char *buf)
{
    int     fd;
    char    fpath[PATHLEN];

    getregfile(fpath);
    if( (fd = open(fpath, O_RDONLY)) == -1 ){
    buf[0] = 0;
    return buf;
    }
    read(fd, buf, 13);
    close(fd);
    buf[13] = 0;
    return buf;
}

void
delregcodefile(void)
{
    char    fpath[PATHLEN];
    getregfile(fpath);
    unlink(fpath);
}

////////////////////////////////////////////////////////////////////////////
// Justify Utilities
////////////////////////////////////////////////////////////////////////////

static void
justify_wait(char *userid, char *phone, char *career,
    char *rname, char *addr, char *mobile)
{
    char buf[PATHLEN];
    sethomefile(buf, userid, FN_JUSTIFY_WAIT);
    if (phone[0] != 0) {
    FILE* fn = fopen(buf, "w");
    assert(fn);
    fprintf(fn, "%s\n%s\ndummy\n%s\n%s\n%s\n",
        phone, career, rname, addr, mobile);
    fclose(fn);
    }
}

static void 
email_justify(const userec_t *muser)
{
    char            tmp[IDLEN + 1], buf[256], genbuf[256];
    /* 
     * It is intended to use BBSENAME instead of BBSNAME here.
     * Because recently many poor users with poor mail clients
     * (or evil mail servers) cannot handle/decode Chinese 
     * subjects (BBSNAME) correctly, so we'd like to use 
     * BBSENAME here to prevent subject being messed up.
     * And please keep BBSENAME short or it may be truncated
     * by evil mail servers.
     */
    snprintf(buf, sizeof(buf),
         " " BBSENAME " - [ %s ]", makeregcode(genbuf));

    strlcpy(tmp, cuser.userid, sizeof(tmp));
    // XXX dirty, set userid=SYSOP
    strlcpy(cuser.userid, str_sysop, sizeof(cuser.userid));
#ifdef HAVEMOBILE
    if (strcmp(muser->email, "m") == 0 || strcmp(muser->email, "M") == 0)
        mobile_message(mobile, buf);
    else
#endif
        bsmtp("etc/registermail", buf, muser->email);
    strlcpy(cuser.userid, tmp, sizeof(cuser.userid));
        move(20,0);
        clrtobot();
    outs("我們即將寄出認證信 (您應該會在 10 分鐘內收到)\n"
         "收到後您可以根據認證信標題的認證碼\n"
         "輸入到 (U)ser -> (R)egister 後就可以完成註冊");
    pressanykey();
    return;
}


/* 使用者填寫註冊表格 */
static void
getfield(int line, const char *info, const char *desc, char *buf, int len)
{
    char            prompt[STRLEN];
    char            genbuf[200];

    // clear first
    move(line+1, 0); clrtoeol();
    move(line, 0); clrtoeol();
    prints("  原先設定:%-30.30s (%s)", buf, info);
    snprintf(prompt, sizeof(prompt), "  %s:", desc);
    if (getdata_str(line + 1, 0, prompt, genbuf, len, DOECHO, buf))
    strcpy(buf, genbuf);
    move(line+1, 0); clrtoeol();
    move(line, 0); clrtoeol();
    prints("  %s:%s", desc, buf);
}


int
setupnewuser(const userec_t *user)
{
    char            genbuf[50];
    char           *fn_fresh = ".fresh";
    userec_t        utmp;
    time_t          clock;
    struct stat     st;
    int             fd, uid;

    clock = now;

    // XXX race condition...
    if (dosearchuser(user->userid, NULL))
    {
    vmsg("手腳不夠快,別人已經搶走了!");
    exit(1);
    }

    /* Lazy method : 先找尋已經清除的過期帳號 */
    if ((uid = dosearchuser("", NULL)) == 0) {
    /* 每 1 個小時,清理 user 帳號一次 */
    if ((stat(fn_fresh, &st) == -1) || (st.st_mtime < clock - 3600)) {
        if ((fd = open(fn_fresh, O_RDWR | O_CREAT, 0600)) == -1)
        return -1;
        write(fd, ctime(&clock), 25);
        close(fd);
        log_usies("CLEAN", "dated users");

        fprintf(stdout, "尋找新帳號中, 請稍待片刻...\n\r");

        if ((fd = open(fn_passwd, O_RDWR | O_CREAT, 0600)) == -1)
        return -1;

        /* 不曉得為什麼要從 2 開始... Ptt:因為SYSOP在1 */
        for (uid = 2; uid <= MAX_USERS; uid++) {
        passwd_query(uid, &utmp);
        // tolerate for one year.
        check_and_expire_account(uid, &utmp, 365*12*60);
        }
    }
    }

    /* initialize passwd semaphores */
    if (passwd_init())
    exit(1);

    passwd_lock();

    uid = dosearchuser("", NULL);
    if ((uid <= 0) || (uid > MAX_USERS)) {
    passwd_unlock();
    vmsg("抱歉,使用者帳號已經滿了,無法註冊新的帳號");
    exit(1);
    }

    setuserid(uid, user->userid);
    snprintf(genbuf, sizeof(genbuf), "uid %d", uid);
    log_usies("APPLY", genbuf);

    SHM->money[uid - 1] = user->money;

    if (passwd_update(uid, (userec_t *)user) == -1) {
    passwd_unlock();
    vmsg("客滿了,再見!");
    exit(1);
    }

    passwd_unlock();

    return uid;
}

/////////////////////////////////////////////////////////////////////////////
// New Registration (Phase 1: Create Account)
/////////////////////////////////////////////////////////////////////////////

void
new_register(void)
{
    userec_t        newuser;
    char            passbuf[STRLEN];
    int             try, id, uid;
    char       *errmsg = NULL;

#ifdef HAVE_USERAGREEMENT
    more(HAVE_USERAGREEMENT, YEA);
    while( 1 ){
    getdata(b_lines, 0, "請問您接受這份使用者條款嗎? (yes/no) ",
        passbuf, 4, LCECHO);
    if( passbuf[0] == 'y' )
        break;
    if( passbuf[0] == 'n' ){
        vmsg("抱歉, 您須要接受使用者條款才能註冊帳號享受我們的服務唷!");
        exit(1);
    }
    vmsg("請輸入 y表示接受, n表示不接受");
    }
#endif

    // setup newuser
    memset(&newuser, 0, sizeof(newuser));
    newuser.version = PASSWD_VERSION;
    newuser.userlevel = PERM_DEFAULT;
    newuser.uflag = BRDSORT_FLAG | MOVIE_FLAG;
    newuser.uflag2 = 0;
    newuser.firstlogin = newuser.lastlogin = now;
    newuser.money = 0;
    newuser.pager = PAGER_ON;
    strlcpy(newuser.lasthost, fromhost, sizeof(newuser.lasthost));

#ifdef DBCSAWARE
    if(u_detectDBCSAwareEvilClient())
    newuser.uflag &= ~DBCSAWARE_FLAG;
    else
    newuser.uflag |= DBCSAWARE_FLAG;
#endif

    more("etc/register", NA);
    try = 0;
    while (1) {
        userec_t xuser;
    int minute;

    if (++try >= 6) {
        vmsg("您嘗試錯誤的輸入太多,請下次再來吧");
        exit(1);
    }
    getdata(17, 0, msg_uid, newuser.userid,
        sizeof(newuser.userid), DOECHO);
        strcpy(passbuf, newuser.userid);

    if (bad_user_id(passbuf))
        outs("無法接受這個代號,請使用英文字母,並且不要包含空格\n");
    else if ((id = getuser(passbuf, &xuser)) &&
        // >=: see check_and_expire_account definition
         (minute = check_and_expire_account(id, &xuser, 0)) >= 0) 
    {
        if (minute == 999999) // XXX magic number.  It should be greater than MAX_USERS at least.
        outs("此代號已經有人使用 是不死之身");
        else {
        prints("此代號已經有人使用 還有 %d 天才過期 \n", 
            minute / (60 * 24) + 1);
        }
    } else
        break;
    }

    // XXX 記得最後 create account 前還要再檢查一次 acc

    try = 0;
    while (1) {
    if (++try >= 6) {
        vmsg("您嘗試錯誤的輸入太多,請下次再來吧");
        exit(1);
    }
    move(20, 0); clrtoeol();
    outs(ANSI_COLOR(1;33) 
    "為避免被偷看,您的密碼並不會顯示在畫面上,直接輸入完後按 Enter 鍵即可。\n"
    "另外請注意密碼只有前八個字元有效,超過的將自動忽略。"
    ANSI_RESET);
    if ((getdata(18, 0, "請設定密碼:", passbuf,
             sizeof(passbuf), NOECHO) < 3) ||
        !strcmp(passbuf, newuser.userid)) {
        outs("密碼太簡單,易遭入侵,至少要 4 個字,請重新輸入\n");
        continue;
    }
    strlcpy(newuser.passwd, passbuf, PASSLEN);
    getdata(19, 0, "請檢查密碼:", passbuf, sizeof(passbuf), NOECHO);
    if (strncmp(passbuf, newuser.passwd, PASSLEN)) {
        move(19, 0);
        outs("密碼輸入錯誤, 請重新輸入密碼.\n");
        continue;
    }
    passbuf[8] = '\0';
    strlcpy(newuser.passwd, genpasswd(passbuf), PASSLEN);
    break;
    }
    // set-up more information.
    move(19, 0); clrtobot();

    // warning: because currutmp=NULL, we can simply pass newuser.* to getdata.
    // DON'T DO THIS IF YOUR currutmp != NULL.
    try = 0;
    while (strlen(newuser.nickname) < 2)
    {
    if (++try > 10) {
        vmsg("您嘗試錯誤的輸入太多,請下次再來吧");
        exit(1);
    }
    getdata(19, 0, "綽號暱稱:", newuser.nickname,
        sizeof(newuser.nickname), DOECHO);
    }

    try = 0;
    while (strlen(newuser.realname) < 4)
    {
    if (++try > 10) {
        vmsg("您嘗試錯誤的輸入太多,請下次再來吧");
        exit(1);
    }
    getdata(20, 0, "真實姓名:", newuser.realname,
        sizeof(newuser.realname), DOECHO);

    if ((errmsg = isvalidname(newuser.realname)))
    {
        memset(newuser.realname, 0, sizeof(newuser.realname));
        vmsg(errmsg); 
    }
    }

    try = 0;
    while (strlen(newuser.address) < 8)
    {
    // do not use isvalidaddr to check,
    // because that requires foreign info.
    if (++try > 10) {
        vmsg("您嘗試錯誤的輸入太多,請下次再來吧");
        exit(1);
    }
    getdata(21, 0, "聯絡地址:", newuser.address,
        sizeof(newuser.address), DOECHO);
    }

    try = 0;
    while (newuser.year < 40) // magic number 40: see user.c
    {
    char birthday[sizeof("mmmm/yy/dd ")];
    int y, m, d;

    if (++try > 20) {
        vmsg("您嘗試錯誤的輸入太多,請下次再來吧");
        exit(1);
    }
    getdata(22, 0, "生日 (西元年/月/日, 如 1984/02/29):", birthday,
        sizeof(birthday), DOECHO);

    if (ParseDate(birthday, &y, &m, &d)) {
        vmsg("日期格式不正確");
        continue;
    } else if (y < 1940) {
        vmsg("你真的有那麼老嗎?");
        continue;
    }
    newuser.year  = (unsigned char)(y-1900);
    newuser.month = (unsigned char)m;
    newuser.day   = (unsigned char)d;
    }

    setupnewuser(&newuser);

    if( (uid = initcuser(newuser.userid)) < 0) {
    vmsg("無法建立帳號");
    exit(1);
    }
    log_usies("REGISTER", fromhost);
}

void 
check_birthday(void)
{
    // check birthday
    int changed = 0;
    time_t t = (time_t)now;
    struct tm tm;

    localtime_r(&t, &tm);
    while ( cuser.year < 40 || // magic number 40: see user.c
        cuser.year+3 > tm.tm_year) 
    {
    char birthday[sizeof("mmmm/yy/dd ")];
    int y, m, d;

    clear();
    stand_title("輸入生日");
    move(2,0);
    outs("本站為配合實行內容分級制度,請您輸入正確的生日資訊。");

    getdata(5, 0, "生日 (西元年/月/日, 如 1984/02/29):", birthday,
        sizeof(birthday), DOECHO);

    if (ParseDate(birthday, &y, &m, &d)) {
        vmsg("日期格式不正確");
        continue;
    } else if (y < 1940) {
        vmsg("你真的有那麼老嗎?");
        continue;
    } else if (y+3 > tm.tm_year+1900) {
        vmsg("嬰兒/未出生應該無法使用 BBS...");
        continue;
    }
    cuser.year  = (unsigned char)(y-1900);
    cuser.month = (unsigned char)m;
    cuser.day   = (unsigned char)d;
    changed = 1;
    }

    if (changed) {
    clear();
    resolve_over18();
    }
}

/////////////////////////////////////////////////////////////////////////////
// User Registration (Phase 2: Validation)
/////////////////////////////////////////////////////////////////////////////

void
check_register(void)
{
    char fn[PATHLEN];


    if (HasUserPerm(PERM_LOGINOK))
    return;

    /* 
     * 避免使用者被退回註冊單後,在知道退回的原因之前,
     * 又送出一次註冊單。
     */ 
    setuserfile(fn, FN_REJECT_NOTIFY);
    if (dashf(fn))
    {
    more(fn, NA);
    move(b_lines-3, 0);
    outs("上次註冊單審查失敗。 (本記錄已備份於您的信箱中)\n"
         "請重新申請並照上面指示正確填寫註冊單。");
    while(getans("請輸入 y 繼續: ") != 'y');
    unlink(fn);
    } 

    // alerting by email is only used by regform1
#ifndef USE_REGFORM2
    else
    if (ISNEWMAIL(currutmp))
    m_read();
#endif // !USE_REGFORM2

    if (!HasUserPerm(PERM_SYSOP)) {
    /* 回覆過身份認證信函,或曾經 E-mail post 過 */
    clear();
    move(9, 3);
    outs("請詳填寫" ANSI_COLOR(32) "註冊申請單" ANSI_RESET ","
           "通告站長以獲得進階使用權力。\n\n\n\n");
    u_register();

#ifdef NEWUSER_LIMIT
    if (cuser.lastlogin - cuser->firstlogin < 3 * 86400)
        cuser.userlevel &= ~PERM_POST;
    more("etc/newuser", YEA);
#endif
    }
}

int
create_regform_request(
    const char *career, const char *phone)
{
    FILE *fn;

#ifdef USE_REGFORM2
    char fname[PATHLEN];
    setuserfile(fname, FN_REGFORM);
    fn = fopen(fname, "wt");    // regform 2: replace model
#else
    fn = fopen(fn_register, "at");  // old regforms: append at last
#endif

    if (!fn)
    return 0;

    // create request data
    // fprintf(fn, "num: %d, %s", usernum, ctime4(&now));
    fprintf(fn, "uid: %s\n",    cuser.userid);
    fprintf(fn, "name: %s\n",   cuser.realname);
    fprintf(fn, "career: %s\n", career);
    fprintf(fn, "addr: %s\n",   cuser.address);
    fprintf(fn, "phone: %s\n",  phone);
    // fprintf(fn, "mobile: %s\n", mobile);
    fprintf(fn, "email: %s\n",  "x"); // email is apparently 'x' here.
    fprintf(fn, "----\n");
    fclose(fn);

#ifdef USE_REGFORM2
    // regform2 must update request list
    file_append_record(FN_REQLIST, cuser.userid);
#endif

    // save justify information
    snprintf(cuser.justify, sizeof(cuser.justify),
        "%s:%s:<Manual>", phone, career);
    return 1;
}

static void
toregister(char *email, char *phone, char *career, char *mobile)
{
    justify_wait(cuser.userid, phone, career, cuser.realname, cuser.address, mobile);

    clear();
    stand_title("認證設定");
    if (cuser.userlevel & PERM_NOREGCODE){
    strcpy(email, "x");
    goto REGFORM2;
    }
    move(1, 0);
    outs("您好, 本站認證認證的方式有:\n"
     "  1.若您有 E-Mail  (本站不接受 yahoo, kimo等免費的 E-Mail)\n"
     "    請輸入您的 E-Mail , 我們會寄發含有認證碼的信件給您\n"
     "    收到後請到 (U)ser => (R)egister 輸入認證碼, 即可通過認證\n"
     "\n"
     "  2.若您沒有 E-Mail 或是一直無法收到認證信, 請輸入 x \n"
     "  會有站長親自人工審核註冊資料," ANSI_COLOR(1;33)
       "但注意這可能會花上數週或更多時間。" ANSI_RESET "\n"
     "**********************************************************\n"
     "* 注意!                                                  *\n"
     "* 通常應該會在輸入完成後十分鐘內收到認證信, 若過久未收到 *\n"
     "* 請到郵件垃圾桶檢查是否被當作垃圾信(SPAM)了,另外若是   *\n"
     "* 輸入後發生認證碼錯誤請重填一次 E-Mail                  *\n"
     "**********************************************************\n");

#ifdef HAVEMOBILE
    outs("  3.若您有手機門號且想採取手機簡訊認證的方式 , 請輸入 m \n"
     "    我們將會寄發含有認證碼的簡訊給您 \n"
     "    收到後請到(U)ser => (R)egister 輸入認證碼, 即可通過認證\n");
#endif

    while (1) {
    email[0] = 0;
    getfield(15, "身分認證用", "E-Mail Address", email, 50);
    if (strcmp(email, "X") == 0) email[0] = 'x';
    if (strcmp(email, "x") == 0)
        break;
#ifdef HAVEMOBILE
    else if (strcmp(email, "m") == 0 || strcmp(email, "M") == 0) {
        if (isvalidmobile(mobile)) {
        char            yn[3];
        getdata(16, 0, "請再次確認您輸入的手機號碼正確嘛? [y/N]",
            yn, sizeof(yn), LCECHO);
        if (yn[0] == 'Y' || yn[0] == 'y')
            break;
        } else {
        move(15, 0); clrtobot();
        move(17, 0);
        outs("指定的手機號碼不正確,"
               "若您無手機門號請選擇其他方式認證");
        }

    }
#endif
    else if (isvalidemail(email)) {
        char            yn[3];
#ifdef USE_EMAILDB
        int email_count = emaildb_check_email(email, strlen(email));

        if (email_count < 0) {
        move(15, 0); clrtobot();
        move(17, 0);
        outs("暫時不允許\ email 認證註冊, 請稍後再試\n");
        pressanykey();
        return;
        } else if (email_count >= EMAILDB_LIMIT) { 
        move(15, 0); clrtobot();
        move(17, 0);
        outs("指定的 E-Mail 已註冊過多帳號, 請使用其他 E-Mail, 或輸入 x 採手動認證\n");
        outs("但注意手動認證通常會花上數週以上的時間。\n");
        } else {
#endif
        getdata(16, 0, "請再次確認您輸入的 E-Mail 位置正確嘛? [y/N]",
            yn, sizeof(yn), LCECHO);
        if (yn[0] == 'Y' || yn[0] == 'y')
        break;
#ifdef USE_EMAILDB
        }
#endif
    } else {
        move(15, 0); clrtobot();
        move(17, 0);
        outs("指定的 E-Mail 不正確, 若您無 E-Mail 請輸入 x 由站長手動認證\n");
        outs("但注意手動認證通常會花上數週以上的時間。\n");
    }
    }
#ifdef USE_EMAILDB
    if (emaildb_update_email(cuser.userid, strlen(cuser.userid),
        email, strlen(email)) < 0) {
    move(15, 0); clrtobot();
    move(17, 0);
    outs("暫時不允許\ email 認證註冊, 請稍後再試\n");
    pressanykey();
    return;
    }
#endif
    strlcpy(cuser.email, email, sizeof(cuser.email));
 REGFORM2:
    if (strcasecmp(email, "x") == 0) {  /* 手動認證 */
    if (!create_regform_request(career, phone))
    {
        vmsg("註冊申請單建立失敗。請至 " BN_BUGREPORT " 報告。");
    }
    } else {
    // register by mail of phone
    snprintf(cuser.justify, sizeof(cuser.justify),
        "%s:%s:<Email>", phone, career);
#ifdef HAVEMOBILE
    if (phone != NULL && email[1] == 0 && tolower(email[0]) == 'm')
        sprintf(cuser.justify, sizeof(cuser.justify),
            "%s:%s:<Mobile>", phone, career);
#endif
       email_justify(&cuser);
    }
}


int
u_register(void)
{
    char            rname[20], addr[50], mobile[16];
#ifdef FOREIGN_REG
    char            fore[2];
#endif
    char            phone[20], career[40], email[50], birthday[11], sex_is[2];
    unsigned char   year, mon, day;
    char            inregcode[14], regcode[50];
    char            ans[3], *errcode;
    char            genbuf[200];
    FILE           *fn;
    int         i = 0;

    if (cuser.userlevel & PERM_LOGINOK) {
    outs("您的身份確認已經完成,不需填寫申請表");
    return XEASY;
    }

#ifdef USE_REGFORM2
    // TODO REGFORM 2 checks 2 parts.
    i = file_find_record(FN_REQLIST, cuser.userid);
#else
    fn = fopen(fn_register, "rt");
    if (fn) {
    int found = 0;
    char *ptr;
    while (fgets(genbuf, sizeof(genbuf), fn)) {
        if (strncmp(genbuf, "uid: ", 5) != 0)
        continue;
        i++;
        if ((ptr = strchr(genbuf, '\n')))
        *ptr = '\0';
        if(strcmp(genbuf + 5, cuser.userid) != 0)
        continue;
        // found
        found = 1;
        break;
    }
    if (!found) i = 0; // drop index if not found.
    }
    if (fn) fclose(fn);
#endif // !USE_REGFORM2

    if (i > 0)
    {
    clear();
    move(3, 0);
    prints("   您的註冊申請單尚在處理中(處理順位: %d),請耐心等候\n\n", i);
    outs("   如果您已收到註冊碼卻看到這個畫面,那代表您在使用 Email 註冊後\n");
    outs("   " ANSI_COLOR(1;31) "又另外申請了站長直接人工審核的註冊申請單。" 
        ANSI_RESET "\n\n");
    outs("   進入人工審核程序後 Email 註冊自動失效,有註冊碼也沒用,\n");
    outs("   要等到審核完成 (會多花很多時間,通常起碼數天) ,所以請耐心等候。\n\n");
    /* 下面是國王的 code 所需要的 message */
#if 0
    outs("   另外請注意,若站長審註冊單時您正在站上則會無法審核、自動跳過。\n");
    outs("   所以等候審核時請勿掛站。若超過兩三天仍未被審到,通常就是這個原因。\n");
#endif
    vmsg("您的註冊申請單尚在處理中");
    return FULLUPDATE;
    }

    strlcpy(rname, cuser.realname, sizeof(rname));
    strlcpy(addr, cuser.address, sizeof(addr));
    strlcpy(email, cuser.email, sizeof(email));

    if (cuser.mobile)
    snprintf(mobile, sizeof(mobile), "0%09d", cuser.mobile);
    else
    mobile[0] = 0;
    if (cuser.month == 0 && cuser.day == 0 && cuser.year == 0)
    birthday[0] = 0;
    else
    snprintf(birthday, sizeof(birthday), "%04i/%02i/%02i",
         1900 + cuser.year, cuser.month, cuser.day);
    sex_is[0] = (cuser.sex % 8) + '1';
    sex_is[1] = 0;
    career[0] = phone[0] = '\0';
    sethomefile(genbuf, cuser.userid, FN_JUSTIFY_WAIT);
    if ((fn = fopen(genbuf, "r"))) {
    fgets(genbuf, sizeof(genbuf), fn);
    chomp(genbuf);
    strlcpy(phone, genbuf, sizeof(phone));

    fgets(genbuf, sizeof(genbuf), fn);
    chomp(genbuf);
    strlcpy(career, genbuf, sizeof(career));

    fgets(genbuf, sizeof(genbuf), fn); // old version compatible

    fgets(genbuf, sizeof(genbuf), fn);
    chomp(genbuf);
    strlcpy(rname, genbuf, sizeof(rname));

    fgets(genbuf, sizeof(genbuf), fn);
    chomp(genbuf);
    strlcpy(addr, genbuf, sizeof(addr));

    fgets(genbuf, sizeof(genbuf), fn);
    chomp(genbuf);
    strlcpy(mobile, genbuf, sizeof(mobile));

    fclose(fn);
    }

    if (cuser.userlevel & PERM_NOREGCODE) {
    vmsg("您不被允許\使用認證碼認證。請填寫註冊申請單");
    goto REGFORM;
    }

    // getregcode(regcode);

    // XXX why check by year? 
    // birthday is moved to earlier, so let's check email instead.
    if (cuser.email[0] && // cuser.year != 0 && /* 已經第一次填過了~ ^^" */
    strcmp(cuser.email, "x") != 0 &&    /* 上次手動認證失敗 */
    strcmp(cuser.email, "X") != 0) 
    {
    clear();
    stand_title("EMail認證");
    move(2, 0);

    prints("請輸入您的認證碼。(由 %s 開頭無空白的十三碼)\n"
           "或輸入 x 來重新填寫 E-Mail 或改由站長手動認證\n", REGCODE_INITIAL);
    inregcode[0] = 0;

    do{
        getdata(10, 0, "您的認證碼:",
            inregcode, sizeof(inregcode), DOECHO);
        if( strcmp(inregcode, "x") == 0 || strcmp(inregcode, "X") == 0 )
        break;
        if( strlen(inregcode) != 13 || inregcode[0] == ' ')
        vmsg("認證碼輸入不完整,總共應有十三碼,沒有空白字元。");
        else if( inregcode[0] != REGCODE_INITIAL[0] || inregcode[1] != REGCODE_INITIAL[1] ) {
        /* old regcode */
        vmsg("輸入的認證碼錯誤," // "或因系統昇級已失效,"
             "請輸入 x 重填一次 E-Mail");
        }
        else
        break;
    } while( 1 );

    // make it case insensitive.
    if (strcasecmp(inregcode, getregcode(regcode)) == 0) {
        int             unum;
        delregcodefile();
        if ((unum = searchuser(cuser.userid, NULL)) == 0) {
        vmsg("系統錯誤,查無此人!");
        u_exit("getuser error");
        exit(0);
        }
        mail_muser(cuser, "[註冊成功\囉]", "etc/registeredmail");
#if FOREIGN_REG_DAY > 0
        if(cuser.uflag2 & FOREIGN)
        mail_muser(cuser, "[出入境管理局]", "etc/foreign_welcome");
#endif
        cuser.userlevel |= (PERM_LOGINOK | PERM_POST);
        outs("\n註冊成功\, 重新上站後將取得完整權限\n"
           "請按下任一鍵跳離後重新上站~ :)");
        sethomefile(genbuf, cuser.userid, FN_JUSTIFY_WAIT);
        unlink(genbuf);
        snprintf(cuser.justify, sizeof(cuser.justify),
             "%s:%s:email", phone, career);
        sethomefile(genbuf, cuser.userid, FN_JUSTIFY);
        log_file(genbuf, LOG_CREAT, cuser.justify);
        pressanykey();
        u_exit("registed");
        exit(0);
        return QUIT;
    } else if (strcasecmp(inregcode, "x") != 0) {
        if (regcode[0])
        {
        vmsg("認證碼錯誤!");
        return FULLUPDATE;
        }
        else 
        {
        vmsg("認證碼已過期,請重新註冊。");
        toregister(email, phone, career, mobile);
        return FULLUPDATE;
        }
    } else {
        toregister(email, phone, career, mobile);
        return FULLUPDATE;
    }
    }

    REGFORM:
    getdata(b_lines - 1, 0, "您確定要填寫註冊單嗎(Y/N)?[N] ",
        ans, 3, LCECHO);
    if (ans[0] != 'y')
    return FULLUPDATE;

    move(2, 0);
    clrtobot();
    while (1) {
    clear();
    move(1, 0);
    prints("%s(%s) 您好,請據實填寫以下的資料:",
           cuser.userid, cuser.nickname);
#ifdef FOREIGN_REG
    fore[0] = 'y';
    fore[1] = 0;
    getfield(2, "Y/n", "是否現在住在台灣", fore, 2);
        if (fore[0] == 'n')
        fore[0] |= FOREIGN;
    else
        fore[0] = 0;
#endif
    while (1) {
        getfield(8, 
#ifdef FOREIGN_REG
                     "請用本名",
#else
                     "請用中文",
#endif
                     "真實姓名", rname, 20);
        if( (errcode = isvalidname(rname)) == NULL )
        break;
        else
        vmsg(errcode);
    }

    move(11, 0);
    outs("  請盡量詳細的填寫您的服務單位,大專院校請麻煩"
         "加" ANSI_COLOR(1;33) "系所" ANSI_RESET ",公司單位請加" ANSI_COLOR(1;33) "職稱" ANSI_RESET ",\n"
         "  暫無工作請麻煩填寫" ANSI_COLOR(1;33) "畢業學校" ANSI_RESET "。\n");
    while (1) {
        getfield(9, "(畢業)學校(含" ANSI_COLOR(1;33) "系所年級" ANSI_RESET ")或單位職稱",
             "服務單位", career, 40);
        if( (errcode = isvalidcareer(career)) == NULL )
        break;
        else
        vmsg(errcode);
    }
    move(10, 0); clrtobot();
    while (1) {
        getfield(10, "含" ANSI_COLOR(1;33) "縣市" ANSI_RESET "及門寢號碼"
             "(台北請加" ANSI_COLOR(1;33) "行政區" ANSI_RESET ")",
             "目前住址", addr, sizeof(addr));
        if( (errcode = isvalidaddr(addr)) == NULL
#ifdef FOREIGN_REG
                || fore[0] 
#endif
        )
        break;
        else
        vmsg(errcode);
    }
    while (1) {
        getfield(11, "不加-(), 包括長途區號", "連絡電話", phone, 11);
        if( (errcode = isvalidphone(phone)) == NULL )
        break;
        else
        vmsg(errcode);
    }
    getfield(12, "只輸入數字 如:0912345678 (可不填)",
         "手機號碼", mobile, 20);
    while (1) {
        getfield(13, "西元/月月/日日 如:1984/02/29", "生日", birthday, sizeof(birthday));
        if (birthday[0] == 0) {
        snprintf(birthday, sizeof(birthday), "%04i/%02i/%02i",
             1900 + cuser.year, cuser.month, cuser.day);
        mon = cuser.month;
        day = cuser.day;
        year = cuser.year;
        } else {
        int y, m, d;
        if (ParseDate(birthday, &y, &m, &d)) {
            vmsg("您的輸入不正確");
            continue;
        }
        mon = (unsigned char)m;
        day = (unsigned char)d;
        year = (unsigned char)(y - 1900);
        }
        if (year < 40) {
        vmsg("您的輸入不正確");
        continue;
        }
        break;
    }
    getfield(14, "1.葛格 2.姐接 ", "性別", sex_is, 2);
    getdata(20, 0, "以上資料是否正確(Y/N)?(Q)取消註冊 [N] ",
        ans, 3, LCECHO);
    if (ans[0] == 'q')
        return 0;
    if (ans[0] == 'y')
        break;
    }

    // copy values to cuser
    strlcpy(cuser.realname, rname, sizeof(cuser.realname));
    strlcpy(cuser.address, addr, sizeof(cuser.address));
    strlcpy(cuser.email, email, sizeof(cuser.email));
    cuser.mobile = atoi(mobile);
    cuser.sex = (sex_is[0] - '1') % 8;
    cuser.month = mon;
    cuser.day = day;
    cuser.year = year;
#ifdef FOREIGN_REG
    if (fore[0])
    cuser.uflag2 |= FOREIGN;
    else
    cuser.uflag2 &= ~FOREIGN;
#endif
    trim(career);
    trim(addr);
    trim(phone);

    // if reach here, email is apparently 'x'.
    toregister(email, phone, career, mobile);

    // update cuser
    passwd_update(usernum, &cuser);

    return FULLUPDATE;
}

////////////////////////////////////////////////////////////////////////////
// Regform Utilities
////////////////////////////////////////////////////////////////////////////

// TODO define and use structure instead, even in reg request file.
typedef struct {
    // current format:
    // (optional) num: unum, date
    // [0] uid: xxxxx   (IDLEN=12)
    // [1] name: RRRRRR (20)
    // [2] career: YYYYYYYYYYYYYYYYYYYYYYYYYY (40)
    // [3] addr: TTTTTTTTT (50)
    // [4] phone: 02DDDDDDDD (20)
    // [5] email: x (50) (deprecated)
    // [6] mobile: (deprecated)
    // [7] ----
    //     lasthost: 16
    char userid[IDLEN+1];

    char exist;
    char online;
    char pad   [ 5];     // IDLEN(12)+1+1+1+5=20

    char name  [20];
    char career[40];
    char addr  [50];
    char phone [20];
} RegformEntry;

// regform format utilities
int
load_regform_entry(RegformEntry *pre, FILE *fp)
{
    char buf[STRLEN];
    char *v;

    memset(pre, 0, sizeof(RegformEntry));
    while (fgets(buf, sizeof(buf), fp))
    {
    if (buf[0] == '-')
        break;
    buf[sizeof(buf)-1] = 0;
    v = strchr(buf, ':');
    if (v == NULL)
        continue;
    *v++ = 0;
    if (*v == ' ') v++;
    chomp(v);

    if (strcmp(buf, "uid") == 0)
        strlcpy(pre->userid, v, sizeof(pre->userid));
    else if (strcmp(buf, "name") == 0)
        strlcpy(pre->name, v, sizeof(pre->name));
    else if (strcmp(buf, "career") == 0)
        strlcpy(pre->career, v, sizeof(pre->career));
    else if (strcmp(buf, "addr") == 0)
        strlcpy(pre->addr, v, sizeof(pre->addr));
    else if (strcmp(buf, "phone") == 0)
        strlcpy(pre->phone, v, sizeof(pre->phone));
    }
    return pre->userid[0] ? 1 : 0;
}

int
print_regform_entry(const RegformEntry *pre, FILE *fp, int close)
{
    fprintf(fp, "uid: %s\n",    pre->userid);
    fprintf(fp, "name: %s\n",   pre->name);
    fprintf(fp, "career: %s\n", pre->career);
    fprintf(fp, "addr: %s\n",   pre->addr);
    fprintf(fp, "phone: %s\n",  pre->phone);
    if (close)
    fprintf(fp, "----\n");
    return 1;
}

int
print_regform_entry_localized(const RegformEntry *pre, FILE *fp, int close)
{
    fprintf(fp, "使用者ID: %s\n", pre->userid);
    fprintf(fp, "真實姓名: %s\n", pre->name);
    fprintf(fp, "職業學校: %s\n", pre->career);
    fprintf(fp, "目前住址: %s\n", pre->addr);
    fprintf(fp, "電話號碼: %s\n", pre->phone);
    if (close)
    fprintf(fp, "----\n");
    return 1;
}

int
append_regform(const RegformEntry *pre, const char *logfn, 
    const char *varname, const char *varval1, const char *varval2)
{
    FILE *fout = fopen(logfn, "at");
    if (!fout)
    return 0;

    print_regform_entry(pre, fout, 0);
    if (varname && *varname)
    {
    syncnow();
    fprintf(fout, "Date: %s\n", Cdate(&now));
    if (!varval1) varval1 = "";
    fprintf(fout, "%s: %s", varname, varval1);
    if (varval2) fprintf(fout, " %s", varval2);
    fprintf(fout, "\n");
    }
    // close it
    fprintf(fout, "----\n");
    fclose(fout);
    return 1;
}

// prototype declare
static void regform_print_reasons(const char *reason, FILE *fp);

void
regform_log2board(const RegformEntry *pre, char accept, const char *reason)
{
#ifdef BN_ID_RECORD
    char fn[PATHLEN];
    char title[STRLEN];
    FILE *fp = NULL;
    char *title2 = NULL;

    snprintf(title, sizeof(title), 
        "[審核結果] %s: %s (審核者: %s)", 
        accept ? "通過":"退回", pre->userid, cuser.userid);

    // reduce mail header title
    title2 = strchr(title, ' ');
    if (title2) title2++;

    if (post_msg_fpath(BN_ID_RECORD, title, title2 ? title2 : title, "[註冊系統]", fn) < 0 ||
    ((fp = fopen(fn, "at")) == NULL))
    return;

    fprintf(fp, "\n");

    if (!accept) // print out reject reasons
    regform_print_reasons(reason, fp);

    fprintf(fp, "\n");
    print_regform_entry_localized(pre, fp, 1);
    fclose(fp);
#endif  // BN_ID_RECORD
}

int regform_estimate_queuesize()
{
#ifdef USE_REGFORM2
    return dashs(FN_REQLIST) / IDLEN;
#else
    return dashs(fn_register) / 163;
#endif // !USE_REGFORM2
}

/////////////////////////////////////////////////////////////////////////////
// Administration (SYSOP Validation)
/////////////////////////////////////////////////////////////////////////////

#define REJECT_REASONS  (6)
#define REASON_LEN  (60)
static const char *reasonstr[REJECT_REASONS] = {
    "輸入真實姓名",
    "詳填(畢業)學校『系』『級』或服務單位(含所屬縣市及職稱)",
    "填寫完整的住址資料 (含縣市名稱, 台北市請含行政區域)",
    "詳填連絡電話 (含區碼, 中間不加 '-', '(', ')' 等符號)",
    "精確並完整填寫註冊申請表",
    "用中文填寫申請單",
};

#define REASON_FIRSTABBREV '0'
#define REASON_IN_ABBREV(x) \
    ((x) >= REASON_FIRSTABBREV && (x) - REASON_FIRSTABBREV < REJECT_REASONS)
#define REASON_EXPANDABBREV(x)   reasonstr[(x) - REASON_FIRSTABBREV]

void 
regform_accept(const char *userid, const char *justify)
{
    char buf[PATHLEN];
    int unum = 0;
    userec_t muser;

    unum = getuser(userid, &muser);
    if (unum == 0)
    return; // invalid user

    muser.userlevel |= (PERM_LOGINOK | PERM_POST);
    strlcpy(muser.justify, justify, sizeof(muser.justify));
    // manual accept sets email to 'x'
    strlcpy(muser.email, "x", sizeof(muser.email)); 

    // handle files
    sethomefile(buf, muser.userid, FN_JUSTIFY_WAIT);
    unlink(buf);
    sethomefile(buf, muser.userid, FN_REJECT_NOTIFY);
    unlink(buf);
    sethomefile(buf, muser.userid, FN_JUSTIFY);
    log_filef(buf, LOG_CREAT, "%s\n", muser.justify);

    // update password file
    passwd_update(unum, &muser);

    // alert online users?
    if (search_ulist(unum))
    {
    sendalert(muser.userid,  ALERT_PWD_PERM|ALERT_PWD_JUSTIFY); // force to reload perm
    kick_all(muser.userid);
    }

    // According to suggestions by PTT BBS account sysops,
    // it is better to use anonymous here.
#if FOREIGN_REG_DAY > 0
    if(muser.uflag2 & FOREIGN)
    mail_log2id(muser.userid, "[System] Registration Complete ", "etc/foreign_welcome",
        "[SYSTEM]", 1, 0);
    else
#endif
    // last: send notification mail
    mail_log2id(muser.userid, "[系統通知] 註冊成功\ ", "etc/registered",
        "[系統通知]", 1, 0);
}

void 
regform_reject(const char *userid, const char *reason, const RegformEntry *pre)
{
    char buf[PATHLEN];
    FILE *fp = NULL;
    int unum = 0;
    userec_t muser;

    unum = getuser(userid, &muser);
    if (unum == 0)
    return; // invalid user

    muser.userlevel &= ~(PERM_LOGINOK | PERM_POST);

    // handle files
    sethomefile(buf, muser.userid, FN_JUSTIFY_WAIT);
    unlink(buf);

    // update password file
    passwd_update(unum, &muser);

    // alert online users?
    if (search_ulist(unum))
    {
    sendalert(muser.userid,  ALERT_PWD_PERM); // force to reload perm
    kick_all(muser.userid);
    }

    // last: send notification
    mkuserdir(muser.userid);
    sethomefile(buf, muser.userid, FN_REJECT_NOTIFY);
    fp = fopen(buf, "wt");
    assert(fp);
    syncnow();

    if(pre) print_regform_entry_localized(pre, fp, 1);
    fprintf(fp, "%s 註冊失敗。\n", Cdate(&now));
    // prompt user for how to contact if they have problem
    fprintf(fp, ANSI_COLOR(1;31) "如有任何問題或需要與站務人員聯絡請至"
        BN_ID_PROBLEM "看板。" ANSI_RESET "\n"); 

    // multiple abbrev loop
    regform_print_reasons(reason, fp);

    fclose(fp);

    // According to suggestions by PTT BBS account sysops,
    // it is better to use anonymous here.
    //
    // XXX how to handle the notification file better?
    // mail_log2id: do not use move.
    // mail_muser(muser, "[註冊失敗]", buf);
#ifdef USE_REGFORM2
    // use regform2! no need to set 'newmail'.
    mail_log2id(muser.userid, "[註冊失敗記錄]", buf, "[註冊系統]", 0, 0);
#else 
    // must set newmail
    mail_log2id(muser.userid, "[註冊失敗記錄]", buf, "[註冊系統]", 1, 0);
#endif
}

// Regform v1 API
// read count entries from regsrc to a temp buffer
FILE *
pull_regform(const char *regfile, char *workfn, int count)
{
    FILE *fp = NULL;

    snprintf(workfn, PATHLEN, "%s.tmp", regfile);
    if (dashf(workfn)) {
    vmsg("其他 SYSOP 也在審核註冊申請單");
    return NULL;
    }

    // count < 0 means unlimited pulling
    Rename(regfile, workfn);
    if ((fp = fopen(workfn, "r")) == NULL) {
    vmsgf("系統錯誤,無法讀取註冊資料檔: %s", workfn);
    return NULL;
    }
    return fp;
}

// write all left in "remains" to regfn.
void
pump_regform(const char *regfn, FILE *remains)
{
    // restore trailing tickets
    char buf[PATHLEN];
    FILE *fout = fopen(regfn, "at");
    if (!fout)
    return;

    while (fgets(buf, sizeof(buf), remains))
    fputs(buf, fout);
    fclose(fout);
}

// New Regform UI
static void
prompt_regform_ui()
{
    move(b_lines, 0);
    outs(ANSI_COLOR(30;47)  "  "
        ANSI_COLOR(31) "y" ANSI_COLOR(30) "接受 "
        ANSI_COLOR(31) "n" ANSI_COLOR(30) "拒絕 "
        ANSI_COLOR(31) "d" ANSI_COLOR(30) "刪除 "
        ANSI_COLOR(31) "s" ANSI_COLOR(30) "跳過 "
        ANSI_COLOR(31) "u" ANSI_COLOR(30) "復原 "
        " "
        ANSI_COLOR(31) "0-9jk↑↓" ANSI_COLOR(30) "移動 "
        ANSI_COLOR(31) "空白/PgDn" ANSI_COLOR(30) "儲存+下頁 "
        " "
        ANSI_COLOR(31) "q/END" ANSI_COLOR(30) "結束  "
        ANSI_RESET);
}

static void
regform_print_reasons(const char *reason, FILE *fp)
{
    // multiple abbrev loop
    if (REASON_IN_ABBREV(reason[0]))
    {
    int i = 0;
    for (i = 0; i < REASON_LEN && REASON_IN_ABBREV(reason[i]); i++)
        fprintf(fp, "[退回原因] 請%s\n", REASON_EXPANDABBREV(reason[i]));
    } else {
    fprintf(fp, "[退回原因] %s\n", reason);
    }
}

static void
resolve_reason(char *s, int y)
{
    // should start with REASON_FIRSTABBREV
    const char *reason_prompt = 
    " (0)真實姓名 (1)詳填系級 (2)完整住址"
    " (3)詳填電話 (4)確實填寫 (5)中文填寫";

    s[0] = 0;
    move(y, 0);
    outs(reason_prompt); outs("\n");

    do {
    getdata(y+1, 0, 
        "退回原因: ", s, REASON_LEN, DOECHO);

    // convert abbrev reasons (format: single digit, or multiple digites)
    if (REASON_IN_ABBREV(s[0]))
    {
        if (s[1] == 0) // simple replace ment
        {
        strlcpy(s+2, REASON_EXPANDABBREV(s[0]),
            REASON_LEN-2);
        s[0] = 0xbd; // '請'[0];
        s[1] = 0xd0; // '請'[1];
        } else {
        // strip until all digites
        char *p = s;
        while (*p)
        {
            if (!REASON_IN_ABBREV(*p))
            *p = ' ';
            p++;
        }
        strip_blank(s, s);
        strlcat(s, " [多重原因]", REASON_LEN);
        }
    } 

    if (strlen(s) < 4)
    {
        if (vmsg("原因太短。 要取消退回嗎? (y/N): ") == 'y')
        {
        *s = 0;
        return;
        }
    }
    } while (strlen(s) < 4);
}

////////////////////////////////////////////////////////////////////////////
// Regform2 API
////////////////////////////////////////////////////////////////////////////

// registration queue
int
regq_append(const char *userid)
{
    if (file_append_record(FN_REQLIST, userid) < 0)
    return 0;
    return 1;
}

int 
regq_find(const char *userid)
{
    return file_find_record(FN_REQLIST, userid);
}

int
regq_delete(const char *userid)
{
    return file_delete_record(FN_REQLIST, userid, 0);
}

// user home regform operation
int 
regfrm_exist(const char *userid)
{
    char fn[PATHLEN];
    sethomefile(fn, userid, FN_REGFORM);
    return  dashf(fn) ? 1 : 0;
}

int
regfrm_load(const char *userid, RegformEntry *pre)
{
    FILE *fp = NULL;
    char fn[PATHLEN];
    int ret = 0;
    sethomefile(fn, userid, FN_REGFORM);
    if (!dashf(fn))
    return 0;

    fp = fopen(fn, "rt");
    if (!fp)
    return 0;
    ret = load_regform_entry(pre, fp);
    fclose(fp);
    return ret;
}

int 
regfrm_save(const char *userid, const RegformEntry *pre)
{
    FILE *fp = NULL;
    char fn[PATHLEN];
    int ret = 0;
    sethomefile(fn, userid, FN_REGFORM);

    fp = fopen(fn, "wt");
    if (!fp)
    return 0;
    ret = print_regform_entry(pre, fp, 1);
    fclose(fp);
    return ret;
}

int 
regfrm_trylock(const char *userid)
{
    int fd = 0;
    char fn[PATHLEN];
    sethomefile(fn, userid, FN_REGFORM);
    if (!dashf(fn)) return 0;
    fd = open(fn, O_RDONLY);
    if (fd < 0) return 0;
    if (flock(fd, LOCK_EX|LOCK_NB) == 0)
    return fd;
    close(fd);
    return 0;
}

int 
regfrm_unlock(int lockfd)
{
    int fd = lockfd;
    if (lockfd <= 0)
    return 0;
    lockfd =  flock(fd, LOCK_UN) == 0 ? 1 : 0;
    close(fd);
    return lockfd;
}

// regform processors
int
regfrm_accept(RegformEntry *pre)
{
    char justify[REGLEN+1], buf[STRLEN*2];
    char fn[PATHLEN], fnlog[PATHLEN];

#ifdef DBG_DRYRUN
    // dry run!
    vmsg("regfrm_accept");
    return 1;
#endif

    sethomefile(fn, pre->userid, FN_REGFORM);

    // build justify string
    removespace(pre->phone);
    removespace(pre->career);
    snprintf(justify, sizeof(justify),
        "%s:%s:%s", pre->phone, pre->career, cuser.userid);

    // call handler
    regform_accept(pre->userid, justify);

    // append current form to history.
    sethomefile(fnlog, pre->userid, FN_REGFORM_LOG);
    snprintf(buf, sizeof(buf), "Date: %s", Cdate(&now));
    file_append_line(fnlog, buf);
    AppendTail(fn, fnlog, 0);
    // global history
    snprintf(buf, sizeof(buf), "Approved: %s -> %s\nDate: %s\n", 
        cuser.userid, pre->userid, Cdate(&now));
    file_append_line(FN_REGISTER_LOG, buf);
    AppendTail(fn, FN_REGISTER_LOG, 0);
    regform_log2board(pre, 1, NULL);

    // remove from queue
    unlink(fn);
    regq_delete(pre->userid);
    return 1;
}

int
regfrm_reject(RegformEntry *pre, const char *reason)
{
    char buf[STRLEN*2];
    char fn[PATHLEN];

#ifdef DBG_DRYRUN
    // dry run!
    vmsg("regfrm_reject");
    return 1;
#endif

    sethomefile(fn, pre->userid, FN_REGFORM);

    // call handler
    regform_reject(pre->userid, reason, pre);

    // log it
    snprintf(buf, sizeof(buf), "Rejected: %s -> %s [%s]\nDate: %s\n", 
        cuser.userid, pre->userid, reason, Cdate(&now));
    file_append_line(FN_REGISTER_LOG, buf);
    AppendTail(fn, FN_REGISTER_LOG, 0);
    regform_log2board(pre, 0, reason);

    // remove from queue
    unlink(fn);
    regq_delete(pre->userid);
    return 1;
}

int
regfrm_delete(const char *userid)
{
    char fn[PATHLEN];
    sethomefile(fn, userid, FN_REGFORM);

#ifdef DBG_DRYRUN
    // dry run!
    vmsgf("regfrm_delete (%s)", userid);
    return 1;
#endif

    // directly delete.
    unlink(fn);

    // remove from queue
    regq_delete(userid);
    return 1;
}

// working queue
FILE *
regq_init_pull()
{
    FILE *fp = tmpfile(), *src =NULL;
    char buf[STRLEN];
    if (!fp) return NULL;
    src = fopen(FN_REQLIST, "rt");
    if (!src) { fclose(fp); return NULL; }
    while (fgets(buf, sizeof(buf), src))
    fputs(buf, fp);
    fclose(src);
    rewind(fp);
    return fp;
}

int 
regq_pull(FILE *fp, char *uid)
{
    char buf[STRLEN];
    size_t idlen = 0;
    uid[0] = 0;
    if (fgets(buf, sizeof(buf), fp) == NULL)
    return 0;
    idlen = strcspn(buf, str_space);
    if (idlen < 1) return 0;
    if (idlen > IDLEN) idlen = IDLEN;
    strlcpy(uid, buf, idlen+1);
    return 1;
}

int
regq_end_pull(FILE *fp)
{
    // no need to unlink because fp is a tmpfile.
    if (!fp) return 0;
    fclose(fp);
    return 1;
}

// UI part
int
ui_display_regform_single(
    const userec_t *xuser, 
    const RegformEntry *pre, 
    int tid, char *reason)
{
    int c;

    while (1)
    {
    move(1, 0);
    user_display(xuser, 1);
    move(14, 0);
    prints(ANSI_COLOR(1;32) 
        "--------------- 這是第 %2d 份註冊單 ------------------" 
        ANSI_RESET "\n", tid);
    prints("  %-12s: %s\n", "帳號", pre->userid);
    prints("0.%-12s: %s%s\n",   "真實姓名", pre->name,
        xuser->uflag2 & FOREIGN ? " (外籍)" : 
        "");
    prints("1.%-12s: %s\n", "服務單位", pre->career);
    prints("2.%-12s: %s\n", "目前住址", pre->addr);
    prints("3.%-12s: %s\n", "連絡電話", pre->phone);

    move(b_lines, 0);
    outs("是否接受此資料(Y/N/Q/Del/Skip)?[S] ");

    // round to ASCII
    while ((c = igetch()) > 0xFF);
    c = tolower(c);

    if (c == 'y' || c == 'q' || c == 'd' || c == 's')
        return c;
    if (c == 'n')
    {
        int n = 0;
        move(3, 0);
        outs("\n" ANSI_COLOR(1;31) 
            "請提出退回申請表原因,按 <Enter> 取消:\n" ANSI_RESET);
        for (n = 0; n < REJECT_REASONS; n++)
        prints("%d) 請%s\n", n, reasonstr[n]);
        outs("\n\n\n"); // preserved for prompt

        getdata(3+2+REJECT_REASONS+1, 0,"退回原因: ",
            reason, REASON_LEN, DOECHO);
        if (reason[0] == 0)
        continue;
        // interprete reason
        return 'n';
    } 
    else if (REASON_IN_ABBREV(c))
    {
        // quick set
        sprintf(reason, "%c", c);
        return 'n';
    }
    return 's';
    }
    // shall never reach here
    return 's';
}

void
regform2_validate_single(const char *xuid)
{
    int lfd = 0;
    int tid = 0;
    char uid[IDLEN+1];
    char rsn[REASON_LEN];
    FILE *fpregq = regq_init_pull();
    RegformEntry re;

    if (!fpregq)
    return;

    while (regq_pull(fpregq, uid))
    {
    userec_t muser;
    int unum = 0;
    int abort = 0;

    // if target assigned, loop until given target.
    if (xuid && strcasecmp(uid, xuid) != 0)
        continue;

    // check if user exists.
    memset(&muser, 0, sizeof(muser));
    unum = getuser(uid, &muser);

    if (unum < 1) 
    {
        regq_delete(uid);
        continue;
    }
    
    // check if regform exists.
    if (!regfrm_exist(uid))
    {
        // TODO delete here?
        regq_delete(uid);
        continue;
    }

    // TODO check if user is already registered
#ifdef DBG_DRYRUN
    if (muser.userlevel & PERM_LOGINOK)
    {
        regfrm_delete(uid);
        continue;
    }
#endif

    // try to lock
    lfd = regfrm_trylock(uid);
    if (lfd <= 0)
        continue;

    // load it
    if (!regfrm_load(uid, &re))
    {
        regfrm_delete(uid);
        regfrm_unlock(lfd);
        // regq_delete(uid); // done in regfrm_delete
        continue;
    }

    tid ++;
    // display regform and process
    switch(ui_display_regform_single(&muser, &re, tid, rsn))
    {
        case 'y': // accept
        regfrm_accept(&re);
        break;

        case 'd': // delete
        regfrm_delete(uid);
        break;

        case 'q': // quit
        abort = 1;
        break;

        case 'n': // reject
        regfrm_reject(&re, rsn);
        break;

        case 's': // skip
        // do nothing.
        break;

        default: // shall never reach here
        assert(0);
        break;
    }
    
    // final processing
    regfrm_unlock(lfd);

    if (abort)
        break;
    }
    regq_end_pull(fpregq);

    // finishing
    clear(); move(5, 0);
    if (xuid && tid == 0)
    prints("未發現 %s 的註冊單。", xuid);
    else
    prints("您審了 %d 份註冊單。", tid);
    pressanykey();
}

#define FORMS_IN_PAGE (10)

int
regform2_validate_page(int dryrun)
{
    int unum = 0;
    int yMsg = FORMS_IN_PAGE*2+1;
    userec_t muser;
    RegformEntry forms [FORMS_IN_PAGE];
    char ans    [FORMS_IN_PAGE];
    int  lfds   [FORMS_IN_PAGE];
    char rejects[FORMS_IN_PAGE][REASON_LEN];    // reject reason length
    char rsn    [REASON_LEN];
    int cforms = 0, // current loaded forms
    ci = 0, // cursor index
    ch = 0, // input key
    i;
    int tid = 0;
    char uid[IDLEN+1];
    FILE *fpregq = regq_init_pull();

    if (!fpregq)
    return 0;

    while (ch != 'q')
    {
    // initialize and prepare
    memset(ans, 0, sizeof(ans));
    memset(rejects, 0, sizeof(rejects));
    memset(forms, 0, sizeof(forms));
    memset(lfds, 0, sizeof(lfds));
    cforms = 0;
    clear();

    // load forms
    while (cforms < FORMS_IN_PAGE)
    {
        if (!regq_pull(fpregq, uid))
        break;
        i = cforms; // align index

        // check if user exists.
        memset(&muser, 0, sizeof(muser));
        unum = getuser(uid, &muser);
        if (unum < 1) 
        {
        regq_delete(uid);
        continue;
        }

        // TODO check if user is already registered
#ifdef DBG_DRYRUN
        if (muser.userlevel & PERM_LOGINOK)
        {
        regfrm_delete(uid);
        continue;
        }
#endif
        // check if regform exists.
        if (!regfrm_exist(uid))
        {
        // TODO delete here?
        regq_delete(uid);
        continue;
        }
        // try to lock
        lfds[i] = regfrm_trylock(uid);
        if (lfds[i] <= 0)
        continue;

        // load it
        if (!regfrm_load(uid, &forms[i]))
        {
        regfrm_delete(uid);
        regfrm_unlock(lfds[i]);
        // regq_delete(uid); // done in regfrm_delete
        continue;
        }

        forms[i].exist = 1;
        forms[i].online = search_ulist(unum) ? 1 : 0;

        // assign default answers
        if (muser.userlevel & PERM_LOGINOK)
        ans[i] = 'd';
#ifdef REGFORM_DISABLE_ONLINE_USER
        else if (forms[i].online)
        ans[i] = 's';
#endif // REGFORM_DISABLE_ONLINE_USER


        // display
        move(i*2, 0);
        prints("  %2d%s %s%-12s " ANSI_RESET, 
            i+1, 
            (unum == 0) ? ANSI_COLOR(1;31) "D" :
            ( (muser.userlevel & PERM_LOGINOK) ? 
              ANSI_COLOR(1;33) "Y" : 
#ifdef REGFORM_DISABLE_ONLINE_USER
              forms[i].online ? "s" : 
#endif
              "."),
            forms[i].online ?  ANSI_COLOR(1;35) : ANSI_COLOR(1),
            forms[i].userid);

        prints( ANSI_COLOR(1;31) "%19s " 
            ANSI_COLOR(1;32) "%-40s" ANSI_RESET"\n", 
            forms[i].name, forms[i].career);

        move(i*2+1, 0); 
        prints("    %s %-50s%20s\n", 
            (muser.userlevel & PERM_NOREGCODE) ? 
            ANSI_COLOR(1;31) "T" ANSI_RESET : " ",
            forms[i].addr, forms[i].phone);

        cforms++, tid ++;
    }

    // if no more forms then leave.
    if (cforms < 1)
        break;

    // adjust cursor if required
    if (ci >= cforms)
        ci = cforms-1;

    // display page info
    {
        char msg[STRLEN];
        snprintf(msg, sizeof(msg),
            "%s 已顯示 %d 份註冊單 ", // "(%2d%%)  ",
            dryrun? "(測試模式)" : "",
            tid);
        prints(ANSI_COLOR(7) "\n%78s" ANSI_RESET "\n", msg);
    }

    // handle user input
    prompt_regform_ui();
    ch = 0;
    while (ch != 'q' && ch != ' ') {
        ch = cursor_key(ci*2, 0);
        switch (ch)
        {
        // nav keys
        case KEY_UP:
        case 'k':
            if (ci > 0) ci--;
            break;

        case KEY_DOWN:
        case 'j':
            ch = 'j'; // go next
            break;

            // quick nav (assuming to FORMS_IN_PAGE=10)
        case '1': case '2': case '3': case '4': case '5':
        case '6': case '7': case '8': case '9':
            ci = ch - '1';
            if (ci >= cforms) ci = cforms-1;
            break;
        case '0':
            ci = 10-1;
            if (ci >= cforms) ci = cforms-1;
            break;

            /*
        case KEY_HOME: ci = 0; break;
        case KEY_END:  ci = cforms-1; break;
            */

            // abort
        case KEY_END:
        case 'q':
            ch = 'q';
            if (getans("確定要離開了嗎? (本頁變更將不會儲存) [y/N]: ") != 'y')
            {
            prompt_regform_ui();
            ch = 0;
            continue;
            }
            break;

            // prepare to go next page
        case KEY_PGDN:
        case ' ':
            ch = ' ';

            {
            int blanks = 0;
            // solving blank (undecided entries)
            for (i = 0, blanks = 0; i < cforms; i++)
                if (ans[i] == 0) blanks ++;

            if (!blanks)
                break;

            // have more blanks
            ch = getans("尚未指定的 %d 個項目要: (S跳過/y通過/n拒絕/e繼續編輯): ", 
                blanks);
            }

            if (ch == 'e')
            {
            prompt_regform_ui();
            ch = 0;
            continue;
            }
            if (ch == 'y') {
            // do nothing.
            } else if (ch == 'n') {
            // query reject reason
            resolve_reason(rsn, yMsg);
            if (*rsn == 0)
                ch = 's';
            } else ch = 's';

            // filling answers
            for (i = 0; i < cforms; i++)
            {
            if (ans[i] != 0)
                continue;
            ans[i] = ch;
            if (ch != 'n')
                continue;
            strlcpy(rejects[i], rsn, REASON_LEN);
            }

            ch = ' '; // go to page mode!
            break;

            // function keys
        case 'y':   // accept
#ifdef REGFORM_DISABLE_ONLINE_USER
            if (forms[ci].online)
            {
            vmsg("暫不開放審核在線上使用者。");
            break;
            }
#endif
        case 's':   // skip
        case 'd':   // delete
        case KEY_DEL:   //delete
            if (ch == KEY_DEL) ch = 'd';

            grayout(ci*2, ci*2+1, GRAYOUT_DARK);
            move_ansi(ci*2, 4); outc(ch);
            ans[ci] = ch;
            ch = 'j'; // go next
            break;

        case 'u':   // undo
#ifdef REGFORM_DISABLE_ONLINE_USER
            if (forms[ci].online)
            {
            vmsg("暫不開放審核在線上使用者。");
            break;
            }
#endif
            grayout(ci*2, ci*2+1, GRAYOUT_NORM);
            move_ansi(ci*2, 4); outc('.');
            ans[ci] = 0;
            ch = 'j'; // go next
            break;

        case 'n':   // reject
#ifdef REGFORM_DISABLE_ONLINE_USER
            if (forms[ci].online)
            {
            vmsg("暫不開放審核在線上使用者。");
            break;
            }
#endif
            // query for reason
            resolve_reason(rejects[ci], yMsg);
            prompt_regform_ui();

            if (!rejects[ci][0])
            break;

            move(yMsg, 0);
            prints("退回 %s 註冊單原因:\n %s\n", forms[ci].userid, rejects[ci]);

            // do reject
            grayout(ci*2, ci*2+1, GRAYOUT_DARK);
            move_ansi(ci*2, 4); outc(ch);
            ans[ci] = ch;
            ch = 'j'; // go next

            break;
        } // switch(ch)

        // change cursor
        if (ch == 'j' && ++ci >= cforms)
        ci = cforms -1;
    } // while(ch != QUIT/SAVE)

    // if exit, we still need to skip all read forms
    if (ch == 'q')
    {
        for (i = 0; i < cforms; i++)
        ans[i] = 's';
    }

    // page complete (save).
    assert(ch == ' ' || ch == 'q');

    // save/commit if required.
    if (dryrun) 
    {
        // prmopt for debug
        clear();
        stand_title("測試模式");
        outs("您正在執行測試模式,所以剛審的註冊單並不會生效。\n"
            "下面列出的是剛才您審完的結果:\n\n");

        for (i = 0; i < cforms; i++)
        {
        char justify[REGLEN];
        if (ans[i] == 'y')
            snprintf(justify, sizeof(justify), // build justify string
                "%s:%s:%s", forms[i].phone, forms[i].career, cuser.userid);

        prints("%2d. %-12s - %c %s\n", i+1, forms[i].userid, ans[i],
            ans[i] == 'n' ? rejects[i] : 
            ans[i] == 'y' ? justify : "");
        }
        if (ch != 'q')
        pressanykey();
    } 
    else 
    {
        // real functionality
        for (i = 0; i < cforms; i++)
        {
        switch(ans[i])
        {
            case 'y': // accept
            regfrm_accept(&forms[i]);
            break;

            case 'd': // delete
            regfrm_delete(uid);
            break;

            case 'n': // reject
            regfrm_reject(&forms[i], rsn);
            break;

            case 's': // skip
            // do nothing.
            break;

            default:
            assert(0);
            break;
        }
        }
    } // !dryrun

    // unlock all forms
    for (i = 0; i < cforms; i++)
        regfrm_unlock(lfds[i]);

    } // while (ch != 'q')

    regq_end_pull(fpregq);

    // finishing
    clear(); move(5, 0);
    prints("您審了 %d 份註冊單。", tid);
    pressanykey();
    return 0;
}

/////////////////////////////////////////////////////////////////////////////
// Regform UI 
// 處理 Register Form
/////////////////////////////////////////////////////////////////////////////

/* Auto-Regform-Scan
 * FIXME 真是一團垃圾
 *
 * fdata 用了太多 magic number
 * return value 應該是指 reason (return index + 1)
 * ans[0] 指的是帳管選擇的「錯誤的欄位」 (Register 選單裡看到的那些)
 */
static int
auto_scan(char fdata[][STRLEN], char ans[])
{
    int             good = 0;
    int             count = 0;
    int             i;
    char            temp[10];

    if (!strncmp(fdata[1], "小", 2) || strstr(fdata[1], "丫")
    || strstr(fdata[1], "誰") || strstr(fdata[1], "不")) {
    ans[0] = '0';
    return 1;
    }
    strlcpy(temp, fdata[1], 3);

    /* 疊字 */
    if (!strncmp(temp, &(fdata[1][2]), 2)) {
    ans[0] = '0';
    return 1;
    }
    if (strlen(fdata[1]) >= 6) {
    if (strstr(fdata[1], "陳水扁")) {
        ans[0] = '0';
        return 1;
    }
    if (strstr("趙錢孫李周吳鄭王", temp))
        good++;
    else if (strstr("杜顏黃林陳官余辛劉", temp))
        good++;
    else if (strstr("蘇方吳呂李邵張廖應蘇", temp))
        good++;
    else if (strstr("徐謝石盧施戴翁唐", temp))
        good++;
    }
    if (!good)
    return 0;

    if (!strcmp(fdata[2], fdata[3]) ||
    !strcmp(fdata[2], fdata[4]) ||
    !strcmp(fdata[3], fdata[4])) {
    ans[0] = '4';
    return 5;
    }
    if (strstr(fdata[2], "大")) {
    if (strstr(fdata[2], "台") || strstr(fdata[2], "淡") ||
        strstr(fdata[2], "交") || strstr(fdata[2], "政") ||
        strstr(fdata[2], "清") || strstr(fdata[2], "警") ||
        strstr(fdata[2], "師") || strstr(fdata[2], "銘傳") ||
        strstr(fdata[2], "中央") || strstr(fdata[2], "成") ||
        strstr(fdata[2], "輔") || strstr(fdata[2], "東吳"))
        good++;
    } else if (strstr(fdata[2], "女中"))
    good++;

    if (strstr(fdata[3], "地球") || strstr(fdata[3], "宇宙") ||
    strstr(fdata[3], "信箱")) {
    ans[0] = '2';
    return 3;
    }
    if (strstr(fdata[3], "市") || strstr(fdata[3], "縣")) {
    if (strstr(fdata[3], "路") || strstr(fdata[3], "街")) {
        if (strstr(fdata[3], "號"))
        good++;
    }
    }
    for (i = 0; fdata[4][i]; i++) {
    if (isdigit((int)fdata[4][i]))
        count++;
    }

    if (count <= 4) {
    ans[0] = '3';
    return 4;
    } else if (count >= 7)
    good++;

    if (good >= 3) {
    ans[0] = 'y';
    return -1;
    } else
    return 0;
}

/////////////////////////////////////////////////////////////////////////////
// Traditional Regform UI
/////////////////////////////////////////////////////////////////////////////
// TODO XXX process someone directly, according to target_uid.
int
scan_register_form(const char *regfile, int automode, const char *target_uid)
{
    char            genbuf[200];
    char    *field[] = {
    "uid", "name", "career", "addr", "phone", "email", NULL
    };
    char    *finfo[] = {
    "帳號", "真實姓名", "服務單位", "目前住址",
    "連絡電話", "電子郵件信箱", NULL
    };
    char    *reason[REJECT_REASONS+1] = {
    "輸入真實姓名",
    "詳填「(畢業)學校及『系』『級』」或「服務單位(含所屬縣市及職稱)」",
    "填寫完整的住址資料 (含縣市名稱, 台北市請含行政區域)",
    "詳填連絡電話 (含區域碼, 中間不用加 '-', '(', ')'等符號",
    "精確並完整填寫註冊申請表",
    "用中文填寫申請單",
    NULL
    };
    char    *autoid = "AutoScan";
    userec_t        muser;
    FILE           *fn, *fout, *freg;
    char            fdata[6][STRLEN];
    char            fname[STRLEN] = "", buf[STRLEN];
    char            ans[4], *ptr, *uid;
    int             n = 0, unum = 0, tid = 0;
    int             nSelf = 0, nAuto = 0;

    uid = cuser.userid;
    move(2, 0);

    fn = pull_regform(regfile, fname, -1);
    if (!fn)
    return -1;

    while( fgets(genbuf, STRLEN, fn) ){
    memset(fdata, 0, sizeof(fdata));
    do {
        if( genbuf[0] == '-' )
        break;
        if ((ptr = (char *)strstr(genbuf, ": "))) {
        *ptr = '\0';
        for (n = 0; field[n]; n++) {
            if (strcmp(genbuf, field[n]) == 0) {
            strlcpy(fdata[n], ptr + 2, sizeof(fdata[n]));
            if ((ptr = (char *)strchr(fdata[n], '\n')))
                *ptr = '\0';
            }
        }
        }
    } while( fgets(genbuf, STRLEN, fn) );
    tid ++;

    if ((unum = getuser(fdata[0], &muser)) == 0) {
        move(2, 0);
        clrtobot();
        outs("系統錯誤,查無此人\n\n");
        for (n = 0; field[n]; n++)
        prints("%s     : %s\n", finfo[n], fdata[n]);
        pressanykey();
    } else {
        if (automode)
        uid = autoid;

        if ((!automode || !auto_scan(fdata, ans))) {
        uid = cuser.userid;

        move(1, 0);
        clrtobot();
        prints("帳號位置    : %d\n", unum);
        user_display(&muser, 1);
        move(14, 0);
        prints(ANSI_COLOR(1;32) "------------- "
            "請站長嚴格審核使用者資料,這是第 %d 份"
            "------------" ANSI_RESET "\n", tid);
            prints("  %-12s: %s\n", finfo[0], fdata[0]);
#ifdef FOREIGN_REG
        prints("0.%-12s: %s%s\n", finfo[1], fdata[1],
               muser.uflag2 & FOREIGN ? " (外籍)" : "");
#else
        prints("0.%-12s: %s\n", finfo[1], fdata[1]);
#endif
        for (n = 2; field[n]; n++) {
            prints("%d.%-12s: %s\n", n - 1, finfo[n], fdata[n]);
        }
        if (muser.userlevel & PERM_LOGINOK) {
            ans[0] = getkey("此帳號已經完成註冊, "
                    "更新(Y/N/Skip)?[N] ");
            if (ans[0] != 'y' && ans[0] != 's')
            ans[0] = 'd';
        } else {
            if (search_ulist(unum) == NULL)
            {
            move(b_lines, 0); clrtoeol();
            outs("是否接受此資料(Y/N/Q/Del/Skip)?[S] ");
            // FIXME if the user got online here
                ans[0] = igetch();
            }
            else
            ans[0] = 's';
            ans[0] = tolower(ans[0]);
            if (ans[0] != 'y' && ans[0] != 'n' && 
            ans[0] != 'q' && ans[0] != 'd' && 
            !('0' <= ans[0] && ans[0] < ('0' + REJECT_REASONS)))
            ans[0] = 's';
            ans[1] = 0;
        }
        nSelf++;
        } else
        nAuto++;

        switch (ans[0]) {
        case 'q':
        if ((freg = fopen(regfile, "a"))) {
            for (n = 0; field[n]; n++)
            fprintf(freg, "%s: %s\n", field[n], fdata[n]);
            fprintf(freg, "----\n");
            while (fgets(genbuf, STRLEN, fn))
            fputs(genbuf, freg);
            fclose(freg);
        }
        case 'd':
        break;

        case '0': case '1': case '2':
        case '3': case '4': case '5':
        /* please confirm match REJECT_REASONS here */
        case 'n':
        if (ans[0] == 'n') {
            int nf = 0;
            move(8, 0);
            clrtobot();
            outs("請提出退回申請表原因,按 <enter> 取消\n");
            for (n = 0; n < REJECT_REASONS; n++)
            prints("%d) 請%s\n", n, reason[n]);
            outs("\n"); // preserved for prompt
            for (nf = 0; field[nf]; nf++)
            prints("%s: %s\n", finfo[nf], fdata[nf]);
        } else
            buf[0] = ans[0];

        if (ans[0] != 'n' ||
            getdata(9 + n, 0, "退回原因: ", buf, 60, DOECHO))
            if ((buf[0] - '0') >= 0 && (buf[0] - '0') < n) {
            int             i;
            fileheader_t    mhdr;
            char            title[128], buf1[80];
            FILE           *fp;

            sethomepath(buf1, muser.userid);
            stampfile(buf1, &mhdr);
            strlcpy(mhdr.owner, cuser.userid, sizeof(mhdr.owner));
            strlcpy(mhdr.title, "[註冊失敗]", TTLEN);
            mhdr.filemode = 0;
            sethomedir(title, muser.userid);
            if (append_record(title, &mhdr, sizeof(mhdr)) != -1) {
                char rejfn[PATHLEN];
                fp = fopen(buf1, "w");
                
                for(i = 0; buf[i] && i < sizeof(buf); i++){
                if (buf[i] >= '0' && buf[i] < '0'+n)
                {
                    fprintf(fp, "[退回原因] 請%s\n",
                        reason[buf[i] - '0']);
                }
                }

                fclose(fp);

                // build reject file
                sethomefile(rejfn, muser.userid, FN_REJECT_NOTIFY);
                Copy(buf1, rejfn);
            }
            if ((fout = fopen(FN_REGISTER_LOG, "a"))) {
                for (n = 0; field[n]; n++)
                fprintf(fout, "%s: %s\n", field[n], fdata[n]);
                fprintf(fout, "Date: %s\n", Cdate(&now));
                fprintf(fout, "Rejected: %s [%s]\n----\n",
                    uid, buf);
                fclose(fout);
            }
            break;
            }
        move(10, 0);
        clrtobot();
        outs("取消退回此註冊申請表");
        /* no break? */

        case 's':
        if ((freg = fopen(regfile, "a"))) {
            for (n = 0; field[n]; n++)
            fprintf(freg, "%s: %s\n", field[n], fdata[n]);
            fprintf(freg, "----\n");
            fclose(freg);
        }
        break;

        default:
        outs("以下使用者資料已經更新:\n");
        mail_muser(muser, "[註冊成功\囉]", "etc/registered");

#if FOREIGN_REG_DAY > 0
        if(muser.uflag2 & FOREIGN)
            mail_muser(muser, "[出入境管理局]", "etc/foreign_welcome");
#endif

        muser.userlevel |= (PERM_LOGINOK | PERM_POST);
        strlcpy(muser.realname, fdata[1], sizeof(muser.realname));
        strlcpy(muser.address, fdata[3], sizeof(muser.address));
        strlcpy(muser.email, fdata[5], sizeof(muser.email));
        snprintf(genbuf, sizeof(genbuf), "%s:%s:%s",
             fdata[4], fdata[2], uid);
        strlcpy(muser.justify, genbuf, sizeof(muser.justify));

        passwd_update(unum, &muser);
        // XXX TODO notify users?
        sendalert(muser.userid,  ALERT_PWD_PERM); // force to reload perm

        sethomefile(buf, muser.userid, FN_JUSTIFY);
        log_file(buf, LOG_CREAT, genbuf);

        if ((fout = fopen(FN_REGISTER_LOG, "a"))) {
            for (n = 0; field[n]; n++)
            fprintf(fout, "%s: %s\n", field[n], fdata[n]);
            fprintf(fout, "Date: %s\n", Cdate(&now));
            fprintf(fout, "Approved: %s\n", uid);
            fprintf(fout, "----\n");
            fclose(fout);
        }
        sethomefile(genbuf, muser.userid, FN_JUSTIFY_WAIT);
        unlink(genbuf);
        break;
        }
    }
    }

    fclose(fn);
    unlink(fname);

    clear(); move(5, 0);
    prints("您審了 %d 份註冊單,AutoScan 審了 %d 份", nSelf, nAuto);
    pressanykey();
    return (0);
}

/////////////////////////////////////////////////////////////////////////////
// New Regform UI
/////////////////////////////////////////////////////////////////////////////

// #define REGFORM_DISABLE_ONLINE_USER
// #define FORMS_IN_PAGE (10)

int
handle_register_form(const char *regfile, int dryrun)
{
    int unum = 0;
    int yMsg = FORMS_IN_PAGE*2+1;
    FILE *fp = NULL;
    userec_t muser;
    RegformEntry forms [FORMS_IN_PAGE];
    char ans    [FORMS_IN_PAGE];
    char rejects[FORMS_IN_PAGE][REASON_LEN];    // reject reason length
    char fname  [PATHLEN] = "";
    char justify[REGLEN+1];
    char rsn    [REASON_LEN];
    int cforms = 0, // current loaded forms
    parsed = 0, // total parsed forms
    ci = 0, // cursor index
    ch = 0, // input key
    i, blanks;
    long fsz = 0, fpos = 0;

    // prepare reg tickets
    if (dryrun)
    {
    // directly open regfile to try
    fp = fopen(regfile, "rt");
    } else {
    fp = pull_regform(regfile, fname, -1);
    }

    if (!fp)
    return 0;

    // retreieve file info
    fpos = ftell(fp);
    fseek(fp, 0, SEEK_END);
    fsz = ftell(fp);
    fseek(fp, fpos, SEEK_SET);
    if (!fsz) fsz = 1;

    while (ch != 'q')
    {
    // initialize and prepare
    memset(ans, 0, sizeof(ans));
    memset(rejects, 0, sizeof(rejects));
    memset(forms, 0, sizeof(forms));
    cforms = 0;

    // load forms
    while (cforms < FORMS_IN_PAGE && load_regform_entry(&forms[cforms], fp))
        cforms++, parsed ++;

    // if no more forms then leave.
    // TODO what if regform error?
    if (cforms < 1)
        break;

    // adjust cursor if required
    if (ci >= cforms)
        ci = cforms-1;

    // display them all.
    clear();
    for (i = 0; i < cforms; i++)
    {
        // fetch user information
        memset(&muser, 0, sizeof(muser));
        unum = getuser(forms[i].userid, &muser);
        forms[i].exist = unum ? 1 : 0;
        if (unum) forms[i].online = search_ulist(unum) ? 1 : 0;

        // if already got login level, delete by default.
        if (!unum)
        ans[i] = 'd';
        else {
        if (muser.userlevel & PERM_LOGINOK)
            ans[i] = 'd';
#ifdef REGFORM_DISABLE_ONLINE_USER
        else if (forms[i].online)
            ans[i] = 's';
#endif // REGFORM_DISABLE_ONLINE_USER
        }

        // print
        move(i*2, 0);
        prints("  %2d%s %s%-12s " ANSI_RESET, 
            i+1, 
            (unum == 0) ? ANSI_COLOR(1;31) "D" :
            ( (muser.userlevel & PERM_LOGINOK) ? 
              ANSI_COLOR(1;33) "Y" : 
#ifdef REGFORM_DISABLE_ONLINE_USER
              forms[i].online ? "s" : 
#endif
              "."),
            forms[i].online ?  ANSI_COLOR(1;35) : ANSI_COLOR(1),
            forms[i].userid);

        prints( ANSI_COLOR(1;31) "%19s " 
            ANSI_COLOR(1;32) "%-40s" ANSI_RESET"\n", 
            forms[i].name, forms[i].career);

        move(i*2+1, 0); 
        prints("    %s %-50s%20s\n", 
            (muser.userlevel & PERM_NOREGCODE) ? 
            ANSI_COLOR(1;31) "T" ANSI_RESET : " ",
            forms[i].addr, forms[i].phone);
    }

    // display page info
    {
        char msg[STRLEN];
        fpos = ftell(fp);
        if (fpos > fsz) fsz = fpos*10;
        snprintf(msg, sizeof(msg),
            "%s 已顯示 %d 份註冊單 (%2d%%)  ",
            dryrun? "(測試模式)" : "",
            parsed, (int)(fpos*100/fsz));
        prints(ANSI_COLOR(7) "\n%78s" ANSI_RESET "\n", msg);
    }

    // handle user input
    prompt_regform_ui();
    ch = 0;
    while (ch != 'q' && ch != ' ') {
        ch = cursor_key(ci*2, 0);
        switch (ch)
        {
        // nav keys
        case KEY_UP:
        case 'k':
            if (ci > 0) ci--;
            break;

        case KEY_DOWN:
        case 'j':
            ch = 'j'; // go next
            break;

            // quick nav (assuming to FORMS_IN_PAGE=10)
        case '1': case '2': case '3': case '4': case '5':
        case '6': case '7': case '8': case '9':
            ci = ch - '1';
            if (ci >= cforms) ci = cforms-1;
            break;
        case '0':
            ci = 10-1;
            if (ci >= cforms) ci = cforms-1;
            break;

            /*
        case KEY_HOME: ci = 0; break;
        case KEY_END:  ci = cforms-1; break;
            */

            // abort
        case KEY_END:
        case 'q':
            ch = 'q';
            if (getans("確定要離開了嗎? (本頁變更將不會儲存) [y/N]: ") != 'y')
            {
            prompt_regform_ui();
            ch = 0;
            continue;
            }
            break;

            // prepare to go next page
        case KEY_PGDN:
        case ' ':
            ch = ' ';

            // solving blank (undecided entries)
            for (i = 0, blanks = 0; i < cforms; i++)
            if (ans[i] == 0) blanks ++;

            if (!blanks)
            break;

            // have more blanks
            ch = getans("尚未指定的 %d 個項目要: (S跳過/y通過/n拒絕/e繼續編輯): ", 
                blanks);

            if (ch == 'e')
            {
            prompt_regform_ui();
            ch = 0;
            continue;
            }
            if (ch == 'y') {
            // do nothing.
            } else if (ch == 'n') {
            // query reject reason
            resolve_reason(rsn, yMsg);
            if (*rsn == 0)
                ch = 's';
            } else ch = 's';

            // filling answers
            for (i = 0; i < cforms; i++)
            {
            if (ans[i] != 0)
                continue;
            ans[i] = ch;
            if (ch != 'n')
                continue;
            strlcpy(rejects[i], rsn, REASON_LEN);
            }

            ch = ' '; // go to page mode!
            break;

            // function keys
        case 'y':   // accept
#ifdef REGFORM_DISABLE_ONLINE_USER
            if (forms[ci].online)
            {
            vmsg("暫不開放審核在線上使用者。");
            break;
            }
#endif
        case 's':   // skip
        case 'd':   // delete
        case KEY_DEL:   //delete
            if (ch == KEY_DEL) ch = 'd';

            grayout(ci*2, ci*2+1, GRAYOUT_DARK);
            move_ansi(ci*2, 4); outc(ch);
            ans[ci] = ch;
            ch = 'j'; // go next
            break;

        case 'u':   // undo
#ifdef REGFORM_DISABLE_ONLINE_USER
            if (forms[ci].online)
            {
            vmsg("暫不開放審核在線上使用者。");
            break;
            }
#endif
            grayout(ci*2, ci*2+1, GRAYOUT_NORM);
            move_ansi(ci*2, 4); outc('.');
            ans[ci] = 0;
            ch = 'j'; // go next
            break;

        case 'n':   // reject
#ifdef REGFORM_DISABLE_ONLINE_USER
            if (forms[ci].online)
            {
            vmsg("暫不開放審核在線上使用者。");
            break;
            }
#endif
            // query for reason
            resolve_reason(rejects[ci], yMsg);
            prompt_regform_ui();

            if (!rejects[ci][0])
            break;

            move(yMsg, 0);
            prints("退回 %s 註冊單原因:\n %s\n", forms[ci].userid, rejects[ci]);

            // do reject
            grayout(ci*2, ci*2+1, GRAYOUT_DARK);
            move_ansi(ci*2, 4); outc(ch);
            ans[ci] = ch;
            ch = 'j'; // go next

            break;
        } // switch(ch)

        // change cursor
        if (ch == 'j' && ++ci >= cforms)
        ci = cforms -1;
    } // while(ch != QUIT/SAVE)

    // if exit, we still need to skip all read forms
    if (ch == 'q')
    {
        for (i = 0; i < cforms; i++)
        ans[i] = 's';
    }

    // page complete (save).
    assert(ch == ' ' || ch == 'q');

    // save/commit if required.
    if (dryrun) 
    {
        // prmopt for debug
        clear();
        stand_title("測試模式");
        outs("您正在執行測試模式,所以剛審的註冊單並不會生效。\n"
            "下面列出的是剛才您審完的結果:\n\n");

        for (i = 0; i < cforms; i++)
        {
        if (ans[i] == 'y')
            snprintf(justify, sizeof(justify), // build justify string
                "%s:%s:%s", forms[i].phone, forms[i].career, cuser.userid);

        prints("%2d. %-12s - %c %s\n", i+1, forms[i].userid, ans[i],
            ans[i] == 'n' ? rejects[i] : 
            ans[i] == 'y' ? justify : "");
        }
        if (ch != 'q')
        pressanykey();
    } 
    else 
    {
        // real functionality
        for (i = 0; i < cforms; i++)
        {
        if (ans[i] == 'y')
        {
            // build justify string
            snprintf(justify, sizeof(justify), 
                "%s:%s:%s", forms[i].phone, forms[i].career, cuser.userid);

            regform_accept(forms[i].userid, justify);
            // log form to FN_REGISTER_LOG
            append_regform(&forms[i], FN_REGISTER_LOG,
                "Approved", cuser.userid, NULL);
            regform_log2board(&forms[i], 1, NULL);
        }
        else if (ans[i] == 'n')
        {
            regform_reject(forms[i].userid, rejects[i], &forms[i]);
            // log form to FN_REGISTER_LOG
            append_regform(&forms[i], FN_REGISTER_LOG,
                "Rejected", cuser.userid, rejects[i]);
            regform_log2board(&forms[i], 0, rejects[i]);
        }
        else if (ans[i] == 's')
        {
            // append form back to fn_register
            append_regform(&forms[i], fn_register,
                NULL, NULL, NULL);
        }
        }
    } // !dryrun

    } // while (ch != 'q')

    // cleaning left regforms
    if (!dryrun)
    {
    pump_regform(regfile, fp);
    fclose(fp);
        unlink(fname);
    } else {
    // directly close file should be OK.
    fclose(fp);
    }

    return 0;
}

int
m_register(void)
{
    FILE           *fn;
    int             x, y, wid, len;
    char            ans[4];
    char            genbuf[200];

#ifdef USE_REGFORM2
    if (dashs(FN_REQLIST) <= 0) {
    outs("目前並無新註冊資料");
    return XEASY;
    }
    fn = fopen(FN_REQLIST, "r");
    assert(fn);
#else
    if ((fn = fopen(fn_register, "r")) == NULL) {
    outs("目前並無新註冊資料");
    return XEASY;
    }
#endif // !USE_REGFORM2
    stand_title("審核使用者註冊資料");
    y = 2;
    x = wid = 0;

#ifdef USE_REGFORM2
    while (fgets(genbuf, STRLEN, fn) && x < 65) {
    move(y++, x);
    outs(genbuf);
    len = strlen(genbuf);
    if (len > wid)
        wid = len;
    if (y >= t_lines - 3) {
        y = 2;
        x += wid + 2;
    }
    }
#else
    while (fgets(genbuf, STRLEN, fn) && x < 65) {
    if (strncmp(genbuf, "uid: ", 5) == 0) {
        move(y++, x);
        outs(genbuf + 5);
        len = strlen(genbuf + 5);
        if (len > wid)
        wid = len;
        if (y >= t_lines - 3) {
        y = 2;
        x += wid + 2;
        }
    }
    }
#endif
    fclose(fn);

#ifdef USE_REGFORM2
    getdata(b_lines - 1, 0, 
        "開始審核嗎 (Y:單筆模式/N:不審/E:整頁模式/U:指定ID)?[N] ", 
        ans, sizeof(ans), LCECHO);

    if (ans[0] == 'y')
    regform2_validate_single(NULL);
    else if (ans[0] == 'e')
    regform2_validate_page(1);
    else if (ans[0] == 'u') {
    stand_title("指定審核");
    usercomplete(msg_uid, genbuf);
    if (genbuf[0])
        regform2_validate_single(genbuf);
    }
#else
    getdata(b_lines - 1, 0, 
        "開始審核嗎(Auto自動/Yes手動/No不審/Exp新界面)?[N] ", 
        ans, sizeof(ans), LCECHO);
    if (ans[0] == 'a')
    scan_register_form(fn_register, 1, NULL);
    else if (ans[0] == 'y')
    scan_register_form(fn_register, 0, NULL);
    else if (ans[0] == 'e')
    {
#ifdef EXP_ADMIN_REGFORM_DRYRUN
    int dryrun = 0;
    if (getans("你要進行純測試(T)還是真的執行審核(y)?") == 'y')
    {
        vmsg("進入實際執行模式,所有審核動作都是真的。");
        dryrun = 0;
    } else {
        vmsg("測試模式。");
        dryrun = 1;
    }
    handle_register_form(fn_register, dryrun);
#else
    // run directly.
    handle_register_form(fn_register, 0);
#endif
    }
#endif // !USE_REGFORM2

    return 0;
}

int
cat_register(void)
{
#ifdef USE_REGFORM2
    vmsg("新系統不需要此功\能。若有錯誤請直接報告。");
    return 0;
#else
    if (system("cat register.new.tmp >> register.new") == 0 &&
    unlink("register.new.tmp") == 0)
    vmsg("OK 嚕~~ 繼續去奮鬥吧!!");
    else
    vmsg("沒辦法CAT過去呢 去檢查一下系統吧!!");
    return 0;
#endif // !USE_REGFORM2
}

/* vim:sw=4
 */