/* * Treeboard * * A hiearchical text message board. * The idea is to display a message tree drawn in SVG, embedded in HTML. * HTML is also embedded in the SVG, because not all browsers support textArea. * * alpha version * Just text, no formatting, no links, no pix, nothing fancy, not even flood control, and the text length is limited. * This version differs from previous ones in that messages are timestamped, which makes the database format incompatible. * * © 2013-2015 def@gmx.at * If you find a commercial use I'd like a slice of that pie. * Otherwise, have fun with it. * * clang tb.c -lfcgi -o tb.fcgi * (`gcc --std=c99 -lfcgi -o tb.fcgi` works as well.) * * Drop it in any directory where it has read-write-access, and have the server handle it as FCGI for a virtual directory. */ /* * The revoke feature is included for legal reasons. I hate it because it can be abused easily, as required by law. * If your jurisdiction doesn't require it, disable it. */ #define REVOKE // includes #include #include // strncpy #include // atoi #include // open #include #include #include // mmap #include #include // variable constants #define MB_BUF_SIZE 65536 // 2**16 #define MB_MSG_LEN (160*14) // HTML-escaped; will change in a future version #define MB_MAX_CONTENT_LEN (MB_MSG_LEN+26) // pass=this&msg=65535&text=160*14\0 /* #define MB_MSG_LEN (1024) // 256 (16×16 or 8×32) characters are better than 160 (8×20) characters, but will they be enough? #define MB_MAX_CONTENT_LEN (256*14+16) */ #if _BIG_ENDIAN #define TREE_MAGIC 0x74726565 #else #define TREE_MAGIC 0x65657274 // little endian is backwards #endif #define BUFFER "db.tb" // structs typedef struct leaf { unsigned short flags; // 0/1: deleted; 2/4: topic; and 14 bit flags to spare; little endian, no network byte order compatibility atm. unsigned short parent; // 0 .. 65535 unsigned long timestamp; // UNIX epoch, y2k38-compliant, but not y2106; time_t is as yet incompatible between Linux and BSD char text[MB_MSG_LEN]; } leaf_t; typedef struct tb_header { unsigned long magic; // tree unsigned short head; unsigned short tail; struct leaf content[]; } tbheader_t; // global vars /* let's get serious. */ tbheader_t* tree; leaf_t* message; // mmapped space // recomputed node values: will need to be mmap-shared also once we parallelize. unsigned short sib[MB_BUF_SIZE]; unsigned short heir[MB_BUF_SIZE]; // chronologically unsigned short children[MB_BUF_SIZE]; // we only need to know if a node has children, but we may as well count them. unsigned short b[MB_BUF_SIZE]; // breadth unsigned short d[MB_BUF_SIZE]; // depth short x[MB_BUF_SIZE]; //unsigned long t[MB_BUF_SIZE]; // the timestamp of the youngest descendant for any given branch unsigned short youngest[MB_BUF_SIZE]; // id of the youngest leaf on the sub-tree // when was the latest message? will also need to be mmapped once we parallelize, if ever. //long last_time; unsigned long now; /* some string constants */ const char* HEAD="Content-type: application/xhtml+xml; charset-encoding=UTF-8\r\n\ Charset-encoding: UTF-8\r\ "; const char* FOOT="\t

\n\ \n\ "; const char* HEAD_HTML="\r\n\ \n\ \n\ "; const char* HEAD_SVG="\r\n\ \n\ \n\ "; const char* BODY="\n\ treeboard\n\ \n\ \n\ \n\ \n\
\n\

Treeboard

\n\ latestallflipmodechanviewbb-view\n\
\n\

"; const char* CSS= " body { background-color: #cccc00; font-family: sans-serif; }\n\ foreignObject body { background-color: transparent; font-size: 12px; }\n\ foreignObject body div { white-space: pre-wrap; text-shadow: 0 0 1px #006600; font-weight: bold; }\n\ .h { text-align: center; position: fixed; box-shadow: 1ex 1ex 1ex 1ex rgba(255,255,255,0.5); background-color: rgba(255,255,255,0.5); border-radius: 1ex 1em; }\n\ .h * { color: #000000; text-decoration: none; font-variant: small-caps; margin: 0.5ex; }\n\ div { color: #ffffff; }\n\ textArea { fill: #ffffff; }\n\ line { stroke: #006600; }\n\ .legal { color: #ff0000; text-decoration: none; font-size: small; }\n\ .del { stroke: #660000; }\n\ .leaf { stroke: #006600; }\n\ a { color: #ffff00; fill: #ffff00; text-decoration: none; font-weight: bold; }\n\ a:hover { /*stroke: #ffff00;*/ text-shadow: 0 0 0.2ex #ffff00; }\n\ .thread { color: #ffffff; margin: 2em; padding: 1ex; border-style: solid; border-radius: 0.5em 1em; background-color: #66cc00; border-color: #006600; border-width: medium; white-space: pre-wrap; text-align: left; }\n\ .inlink { color: #ffffff; text-decoration: none; }\n\ .post { border-style: solid; border-width: thin; border-color: #006600; padding: 1em; background-color: #00cc00; }\n\ .reply { color: #ffff00; }\n\ table { border:1; white-space: pre-wrap; empty-cells: hide; }\n\ "; // should maybe read this from a file /* In additon, referencing a @font-face src might allow adaptive leaf sizes. */ /* some colour constants */ //unsigned long faded=0xcccc00; // fades towards background //unsigned long fresh=0x00cc00; //unsigned long rotten=0xcc0000; /* a note on colours: * the brightness difference between leaf and background needs to be less than between text colour and leaf, or the text will be hard to read. * counter-intuitively this can mean the background needs to be bright enough. * it also means the leafs may fade only halfway towards the background. */ const int m_w=200; // message width const int m_h=185; // message height const int sep_w=10; // padding const int r_w=m_w-2*sep_w; // rect const int r_h=m_h-2*sep_w; const int t_w=r_w-2*sep_w; // text const int t_h=r_h-2*sep_w; /* auxilliary function: adjust positions to accomodate a new leaf */ void shift_sibs(unsigned short parent_id){ short cp=b[parent_id]; // current position in group short prev_brev=0; for(unsigned short chid=heir[parent_id];chid!=parent_id;chid=sib[chid]){ cp-=prev_brev; // half width of previous cp-=b[chid]; // half width of this x[chid]=cp; prev_brev=b[chid]; } } /* auxilliary function: calculates the colour of a message */ /* explanation: * * colour value goes from a minimum to a maximum. * the minimum is 0x00, the maximum is 0xcc/2=0x66 * the factor is delta/limit, where delta is between 0 and limit, * so the factor is between 0 and 1, inclusively. * 0 means the leaf is brand new. * 1 means the leaf has faded to the maximum. * * the colour value is therefore minimum+(delta/limit)*(maximum-minimum) * (if minimum and maximum are the same, the factor is always 0.) * in our case, the formula is 0x00+(delta/limit)*(0xcc/2-0x00) * or simplified: 0x66*delta/limit * * because we are dealing with integer values, we have to switch the terms around, * to avoid overflows, and also to avoid divisors being larger than dividends, * or 0 and 1 would be the only factor values we ever get. * * for maxima > 0x88, maximum*delta > max_int * in which case, do * delta/(limit/$maximum) * which works for any colour value except black. * with larger values it decreases in accuracy * * therefore, even though a colour value fits into an int, * it is best to calculate each component separately. * */ unsigned char colour_channel(unsigned char kid,unsigned char geezer, unsigned long delta){ // because nested functions are not allowed in C const unsigned long limit=31556952; // a year unsigned char age=geezer-kid; //return kid+(age*delta/limit); return age?kid+(delta/(limit/age)):kid; } /* c is the colour of new messages. o is the colour of new deleted messages. delta is their age in seconds */ unsigned long colour_channels(unsigned long c, unsigned long o, unsigned long delta){ unsigned long r,g,b,a; a=colour_channel( (c>>24)&0xff, (o>>24)&0xff , delta); r=colour_channel( (c>>16)&0xff, (o>>16)&0xff , delta); g=colour_channel( (c>> 8)&0xff, (o>> 8)&0xff , delta); b=colour_channel( (c )&0xff, (o )&0xff , delta); return (a<<24)+(r<<16)+(g<<8)+b; } unsigned long colour_original(unsigned short m){ // the original, recreated const unsigned long limit=31556952; // a year unsigned long delta=now-message[m].timestamp;if(delta>limit)delta=limit; unsigned char r,g,b; unsigned long c,o; if(message[m].flags&1){ // rotten c=0xcc0000; o=0xcc9900; }else{ // fresh c=0x00cc00; o=0x99cc00; } return colour_channels(c,o,delta); } unsigned long(*colour)(unsigned short m)=colour_original; /* auxilliary function: adjust values for the ancestry */ void update_lineage(unsigned short m){ // initialize properties of m heir[m]=m; children[m]=0; b[m]=1; // the message uses as much space as its heirs d[m]=1; // the message is at the lowest level in its subtree x[m]=0; // the message is centered initially //t[m]=message[m].timestamp; // the message is now youngest[m]=m; // paternal matters unsigned short par=message[m].parent; if(par!=m){ sib[m]=heir[par]; heir[par]=m; children[par]++; //t[par]=t[m]; youngest[par]=m; if(children[par]!=1){ // first child does not alter width par=m; do{ par=message[par].parent; b[par]++; shift_sibs(par); //t[par]=t[m]; youngest[par]=m; }while(par!=message[par].parent); }else{ // first child does alter depth unsigned short nd=2; // new depth do{ d[par]=nd++; // must be the same as nd=++d[par]; //nd=++d[par]; if(par==message[par].parent)break; par=message[par].parent; //t[par]=t[m]; youngest[par]=m; }while(d[par]<=nd); // not the case when par==message[par].parent //do{par=message[par].parent;t[par]=t[m];}while(message[par].parent!=par); do{par=message[par].parent;youngest[par]=m;}while(message[par].parent!=par); } }else{ sib[m]=m; // possible loss of relationship /* if(m==tree->head)sibling[m]=-1; else{ // alternatively unsigned short cur=tree->head; while(sibling[cur]!=-1)cur=(unsigned short)(sibling[cur]); sibling[cur]=m; sibling[m]=-1; x[m]=x[cur]-b[cur]-b[m]; // left of visible pane, unaccessible } */ } } /* aux func: escape strings for json. rfc4627 */ void mdea(char* fleece){ while(*fleece){ switch(*fleece){ case '\n': putchar('\\'); putchar('n'); break; case '\r': putchar('\\'); putchar('r'); break; case '\t': putchar('\\'); putchar('t'); break; case '"': //case '\'': case '\\': putchar('\\'); default: putchar(*fleece); } fleece++; } } /* aux func: recursively assemble JSON rep of tree */ void json_print(unsigned short n){ printf("\"%hu\":{\"text\":\"",n); mdea(message[n].text); printf("\",\"timestamp\":%lu",message[n].timestamp); if(message[n].flags&0x01)fputs(",\"deleted\":true",stdout); if(heir[n]!=n){ putchar(','); json_print(heir[n]); } putchar('}'); if(sib[n]!=message[n].parent){ putchar(','); json_print(sib[n]); } } /* aux func: traverse subtrees in search of the youngest */ int chan_rec(unsigned short list[5], int l, unsigned short cur, unsigned short pre){ if(heir[cur]!=cur){ // all younger l=chan_rec(list, l, heir[cur], pre); } if(sib[cur]!=message[cur].parent){ // older siblings. l=chan_rec(list, l, sib[cur], pre); } int i=0; for(;i4){ textbuffer[i++]='&'; textbuffer[i++]='l'; textbuffer[i++]='t'; textbuffer[i++]=';'; }else{ return -2; } }else if(x=='&'){ // alternatively we could unescape the character, but why bother? if(MB_MSG_LEN-i>5){ textbuffer[i++]='&'; textbuffer[i++]='a'; textbuffer[i++]='m'; textbuffer[i++]='p'; textbuffer[i++]=';'; }else{ return -2; } }else if(x<0x20 && x!=0x09 && x!=0x0a && x!=0x0d) return -3; // some control sequences are rejected else textbuffer[i++]=x; } else if(c=='<'){ // should never happen, but better safe than sorry. if(MB_MSG_LEN-i>4){ textbuffer[i++]='&'; textbuffer[i++]='l'; textbuffer[i++]='t'; textbuffer[i++]=';'; }else{ return -2; } } else textbuffer[i++]=(char)c; } if(i==0)return -1; // no empty messages. TODO: should also check for non-whitespace. /* unicode spaces: 0009 - 000d, 0020, 0085, 00a0, 1680, 180e, 2000 - 200d, 2028, 2029, 202f, 205f, 3000, feff */ textbuffer[i++]='\0'; /* * It would be convenient to parse for links and make them clickable. * Doing it here means it only needs to be done once per message. * (It also eats up message space.) * However, Presto doesn't support clickable links in foreignObject, * Links: * : 15+2m // for foreignObject only, doesn't work in Opera * to open in a separate window, we'd also have to insert target="_blank", 16 bytes more * Images: * : 30+m+(w+h) ; malicious image files might crash the browser * also note: embedded images can be SVG, and SVG can contain ECMAScript, possible XSS vector. * * ASIDE: if we do detect links, it might be SPAM. * I believe spammers should pay for advertisement space. They use it commercially, so commerce. */ unsigned short mx=tree->tail++; // copy: message index; advance it to reserve this space. if(mx==tree->head){ // the oldest message has just been phased out. if(heir[mx]!=mx){ // process the orphans for(int i=heir[mx];i!=mx;i=sib[i]){ message[i].parent=i; // condolences } } tree->head++; // moving on. will need to be moved down once we parallelize } // turn a new leaf message[mx].parent=parent; message[mx].flags=0; // original message message[mx].timestamp=now; /* char* cursor=&(message[mx].text); if((char* h=strstr(textbuffer,"http"))!=NULL){ char* end_of_url; // check if message buffer has space enough for link (first determine length of link. then if(15+m < MSG_BUF_LEN-i) ) for(end_of_url=h+7;*end_of_url!=0;end_of_url++){ if(*end_of_url<'#' && *end_of_url!='!')break; if(*end_of_url>'&' && *end_of_url<'+') break; if(*end_of_url>'=' && *end_of_url<'?')break; // if(*end_of_url=='>')break; if(*end_of_url>'Z' && *end_of_url<'_')break; if(*end_of_url>'_' && *end_of_url<'a')break; if(*end_of_url>'z') break; } // else return(-2) if(15+(end_of_url-h)",2); cursor+=2; memcpy(cursor,h,end_of_url-h); cursor+=end_of_url-h; // copy url into message buffer again memcpy(cursor,"",4); cursor+=4; // memcpy remainder of message, or look for next link }else // look for next link }else */ memcpy(&(message[mx].text),textbuffer,i); // msync(m,sizeof(leaf_t),MS_ASYNC|MS_INVALIDATE); // m needs to be aligned to page size, length set accordingly. update_lineage(mx); //last_time=now; //sort_by_activity(mx); // temporary test // msync(0,sizeof(tbheader_t),MS_ASYNC|MS_INVALIDATE); return mx; } #ifdef REVOKE /* For legal reasons, it must be possible to delete any content. * We would like to contain this damage. * Logistically, it's easiest to just overwrite the content, * ideally with the reason why it was harmonized (and who to blame.) */ void redact_message(int id, char* reason){ strncpy(message[id].text,reason,MB_MSG_LEN); } #else /* In jurisdictions where deletion is not required, but covering up is sufficient, * or where destruction of evidence is even forbidden, * the original text should be preserved, but not displayed until further notice. */ void restore_message(int id){ message[id].flags&=0xfffe; } #endif /* this helper function prints a naked message in html */ void print_message_html(unsigned short id){ if(message[id].parent!=id) printf("%hu >>%hu %s ",id,id,message[id].parent,message[id].parent,message[id].text,id); else printf("%hu %s ",id,id,message[id].text,id); } /* this function only prints the message in its container, without decorations or buttons. */ void print_message(unsigned short m){ /* printf("\n\ \n\ \n\ \n\ \n\ \n\ \n\ \n\ \n\

%s
\n\ \n\ \n\ \n\ \n\ ",(message[m].deleted?"del":"leaf"),sep_w,sep_w,r_h,r_w,sep_w*2,sep_w*2,t_w,t_h,message[m].text,sep_w,sep_w,t_w,t_h,message[m].text); *//* printf("\n\ \n\ \n\
%s
\n\ \n\
\n\ ",(message[m].flags&1?"del":"leaf"),sep_w,sep_w,r_h,r_w, 0x99*(tree->tail-m-1)/(tree->tail-tree->head) ,sep_w,sep_w,t_w,t_h,message[m].text); return; */ // colour by timestamp const unsigned long limit=31556952; // a year unsigned long delta=now-message[m].timestamp;if(delta>limit)delta=limit; printf("\n\ \n\ \n\
%s
\n\ \n\
\n\ ",(message[m].flags&1?"del":"leaf"),sep_w,sep_w,r_h,r_w, //0x99*delta/limit // linear // for sufficiently large delta, 0x99*delta > max_int ; limit/0x99==206255 message[m].flags&1?0xcc:delta/(limit/0x99), // linear, red message[m].flags&1?delta/(limit/0x99):0xcc, // linear, green sep_w,sep_w,t_w,t_h,message[m].text); } /* this function prints the tree from any given root */ void print_tree(unsigned short m){ printf("",x[m]*m_w/2,m_h); print_message(m); printf("reply",m,sep_w*2,r_h); if(heir[m]!=m)print_tree(heir[m]); puts(""); printf("",m_w/2,m_h-sep_w,(x[m]+1)*m_w/2,m_h+sep_w); if(sib[m]!=message[m].parent)print_tree(sib[m]); else printf("",message[m].parent,m_w/2,m_h-sep_w); } /* this function moves the root to provide space for its children */ void print_tree_top(short nx, unsigned short m){ printf("",nx*m_w+(b[m]-1)*(m_w/2), sep_w/2); if(message[m].parent!=m) printf("\n",message[m].parent,sep_w,m_w/2,sep_w); print_message(m); printf("reply\n",m,sep_w*2,m_h-2*sep_w); if(heir[m]!=m)print_tree(heir[m]); puts(""); } /* * put the tree with the youngest descendant leftmost * traverse list from youngest, * pick only the freshest leafs, * find their ancestors, * and print their trees */ void woodcraft(unsigned short n){ /* { unsigned short total=tree->tail-tree->head; if(total && n>total)n=total; } */ unsigned short btotal=0; unsigned short dtotal=0; const unsigned short begin=tree->tail-n; for(unsigned short i=begin;i!=tree->tail;i++){ if(i==message[i].parent || i-message[i].parent>i-begin){ btotal+=b[i]; if(dtotal\n",btotal*m_w,dtotal*m_h); //puts("\n"); unsigned short end=tree->tail-1; short nx=0; while(end!=begin){ // this will not work if buffer is full and entire tree is requested, but in that case tree?(tree->head) would be more efficient anyway. if(heir[end]==end){ // a leaf unsigned short my_a=message[end].parent-begin,my_b=end-begin; // circumvent a bug in clang 3.5.0 //if(heir[message[end].parent]==end // a youngest if(youngest[message[end].parent]==end // a youngest //|| message[end].parent-begin>=end-begin // or an orphan || my_a>=my_b // or an orphan // why do we need this? ){ unsigned short m=end; int skip=0; while(message[m].parent!=m && m-begin>=m-message[m].parent){ // if any of the ancestors has siblings that we would have printed, skip the tree m=message[m].parent; //if(heir[message[m].parent]!=m){ // not the youngest if(youngest[m]!=end){ // not the youngest skip=1; break; } } if(!skip){ print_tree_top(nx,m); nx+=b[m]; } } } end--; } if(heir[begin]==begin)print_tree_top(nx,begin); // the oldest might not have been printed yet, especially if it is the only. puts(""); } /* like print_tree, after an encounter of the lumberjack kind */ void print_tree_flipped(unsigned short m){ printf("",m_w,x[m]*m_h/2); print_message(m); printf("reply",m,sep_w*2,r_h); if(heir[m]!=m)print_tree_flipped(heir[m]); puts(""); //printf("",m_w/2,m_h-sep_w,(x[m]+1)*m_w/2,m_h+sep_w); printf("",m_w-sep_w,m_h/2,m_w+sep_w,(x[m]+1)*m_h/2); if(sib[m]!=message[m].parent)print_tree_flipped(sib[m]); else printf("",message[m].parent,m_w-sep_w,m_h/2); //printf("",message[m].parent,m_w/2,m_h-sep_w); } /* like print_tree_top, but growing sideways */ void print_tree_top_flipped(short nx, unsigned short m){ printf("",sep_w/2, nx*m_h+(b[m]-1)*(m_h/2)); if(message[m].parent!=m) printf("\n",message[m].parent,sep_w,sep_w,m_w/2); //printf("\n",message[m].parent,sep_w,sep_w,m_w/2); print_message(m); printf("reply\n",m,sep_w*2,m_h-2*sep_w); if(heir[m]!=m)print_tree_flipped(heir[m]); puts(""); } /* like woodcraft, but multiplied by the negative square root of -1 */ void flipmode(unsigned short n){ unsigned short btotal=0; unsigned short dtotal=0; const unsigned short begin=tree->tail-n; for(unsigned short i=begin;i!=tree->tail;i++){ if(i==message[i].parent || i-message[i].parent>i-begin){ btotal+=b[i]; if(dtotal\n",dtotal*m_w,btotal*m_h); //puts("\n"); unsigned short end=tree->tail-1; short nx=0; while(end!=begin){ // this will not work if buffer is full and entire tree is requested if(heir[end]==end){ // a leaf unsigned short my_a=message[end].parent-begin,my_b=end-begin; // circumvent a bug in clang 3.5.0 if(heir[message[end].parent]==end // a youngest //|| message[end].parent-begin>=end-begin // or an orphan || my_a>=my_b // or an orphan ){ unsigned short m=end; int skip=0; while(message[m].parent!=m && m-begin>=m-message[m].parent){ // if any of the ancestors has siblings that we would have printed, skip the tree m=message[m].parent; //if(heir[message[m].parent]!=m){ // not the youngest if(youngest[m]!=end){ // not the youngest skip=1; break; } } if(!skip){ print_tree_top_flipped(nx,m); nx+=b[m]; } } } end--; } if(heir[begin]==begin)print_tree_top_flipped(nx,begin); // the oldest might not have been printed yet, especially if it is the only. puts(""); } /* chanview */ void print_chan(/*unsigned short dummy*/){ puts(HEAD); puts(HEAD_HTML); puts(BODY); // get latest five tops, get latest 5 nodes in each of their subtrees unsigned short curt=tree->tail-1; int threadcount=0; unsigned short tclist[5]; // list of thread tops // gather tops (crude) while(threadcount<5 && curt!=tree->head){ unsigned short parcurt; for(parcurt=curt;message[parcurt].parent!=tree->head && message[parcurt].parent!=parcurt;parcurt=message[parcurt].parent); int i=0; while(ihead; unsigned short t_c=parcurt;while(heir[t_c]!=t_c)t_c=heir[t_c];t_c-=tree->head; if(t_j>t_c){ // youth first tclist[j]^=parcurt; parcurt^=tclist[j]; tclist[j]^=parcurt; } } tclist[threadcount++]=parcurt; } curt--; } // print for(int i=0;i

%hu %s

",tclist[i],tclist[i],message[tclist[i]].text); if(heir[tclist[i]]!=tclist[i]){ unsigned short list[5]; // to be filled with youngest descendants int l=chan_rec(list, 0, heir[tclist[i]], tclist[i]); for(int postcount=l-1;postcount>=0;postcount--){ //puts("
"); // puts adds spaces in between posts fputs("
",stdout); print_message_html(list[postcount]); //puts("
"); fputs("
",stdout); } } printf("Reply",tclist[i]); //puts(""); fputs("",stdout); } /* // - puts("
New topic: "); puts("
"); printf("",tree->head); puts(""); puts("
"); puts("
"); */ #ifdef REVOKE puts("

illegal content?"); #endif puts(FOOT); } /* per request: output phpbb style */ void rec_bb(unsigned short n, unsigned short level){ /* for(unsigned short hu=0;hu",stdout); rec_bb(child,level+1); fputs("",stdout); } } void print_bb(unsigned short n){ puts(HEAD); puts(HEAD_HTML); puts(BODY); puts("

"); rec_bb(n,0); puts("
"); #ifdef REVOKE puts("

illegal content?"); #endif puts(FOOT); } void print_bb_latest(unsigned short n){ if(tree->tail!=tree->head && tree->tail-tree->headtail-tree->head; unsigned short start=tree->tail-n; puts(HEAD); puts(HEAD_HTML); puts(BODY); // printf("",tree->head,tree->tail,n,start); for(unsigned short cur=start;cur!=tree->tail;cur++){ if(cur-message[cur].parent>cur-start || message[cur].parent==cur){ puts("

"); rec_bb(cur,0); puts("
"); } } #ifdef REVOKE puts("

illegal content?"); #endif puts(FOOT); } /* another html output, equivalent to tree */ void print_table(unsigned short n){ long q[MB_BUF_SIZE]; unsigned short p=0; unsigned short s=1; unsigned short r=1; unsigned short z=d[n]; q[0]=n; const unsigned long limit=31556952; //unsigned long now=(unsigned long)(time(NULL)); puts(""); puts(""); while(r){ //printf("",p,s,r,z); if(q[p]<0){ puts(""); q[s++]=-1; }else{ unsigned short m=q[p]; unsigned long delta=now-message[m].timestamp;if(delta>limit)delta=limit; printf(""); if(heir[m]!=m){ for(unsigned short l=heir[m];l!=m;l=sib[l]){ q[s++]=l; } }else{ q[s++]=-1; } } p++; r--; if(r<=0){ puts(""); if(--z){ r=s-p; if(r)puts(""); } } } puts("
",b[m],message[m].flags&1?0xcc:(delta/(limit/0x99)),message[m].flags&1?(delta/(limit/0x99)):0xcc); print_message_html(m); puts("
"); } /* table output almost equivalent to latest, but not sorted by descendants */ void print_table_latest(unsigned short n){ if(tree->tail!=tree->head && tree->tail-tree->headtail-tree->head; unsigned short start=tree->tail-n; puts(HEAD); puts(HEAD_HTML); puts(BODY); for(unsigned short cur=start;cur!=tree->tail;cur++){ if(cur-message[cur].parent>cur-start || message[cur].parent==cur){ puts("

"); print_table(cur); puts("

"); } } #ifdef REVOKE puts("

illegal content?"); #endif puts(FOOT); } /* print a thread */ void aux_thread(unsigned short leaf){ // note that this is recursive, and might max out the stack // alternatively malloc a list the size of depth, or define a list the size of MB_BUF_SIZE if(message[leaf].parent!=leaf){ // alteratively, stop if leaf is a topic aux_thread(message[leaf].parent); } fputs("

",stdout); // if deleted, we should indicate that. also, colour code age maybe. print_message_html(leaf); fputs("
",stdout); } void print_thread(unsigned short leaf){ puts(HEAD); puts(HEAD_HTML); puts(BODY); printf("
"); if(message[leaf].parent!=leaf) aux_thread(message[leaf].parent); // followed by tree, if any print_table(leaf); puts("
"); #ifdef REVOKE puts("

illegal content?"); #endif puts(FOOT); } /* print a discussion from any point */ void thread_svg(int leaf){ // collect a list of ancestors instead of recursing? puts(HEAD); puts(HEAD_SVG); puts(BODY); // svg unsigned short height=1;for(unsigned short par=message[leaf].parent;par!=message[par].parent;par=message[par].parent)height++; printf("",b[leaf]*m_w,(height+d[leaf])*m_h); // print all ancestors if(message[leaf].parent!=leaf){ unsigned short trunk=leaf; unsigned short lift=height-1; do{ trunk=message[trunk].parent; printf("",(b[leaf]-1)*(m_w/2),lift*m_h); print_message(trunk); printf("reply",trunk,sep_w*2,r_h); printf("",m_w/2,m_h-sep_w,m_w/2,m_h+sep_w); printf("",message[trunk].parent,m_w/2,m_h-sep_w); puts(""); lift--; }while(message[trunk].parent!=trunk); }else height=0; // print node and all descendants printf("",0,height*m_h); print_tree_top(0,leaf); puts(""); #ifdef REVOKE puts("

illegal content?"); #endif printf("",(b[leaf])*m_w/2,height*m_h); puts(FOOT); } // aux func for mmapping files size_t load_file(const char* filename, void** pointer){ int fd=open(filename,O_RDONLY); if(fd==-1){ // an error occurred *pointer=NULL; return -1; } struct stat fs; if(fstat(fd,&fs)<0){ // an error occurred close(fd); *pointer=NULL; return -2; } *pointer=mmap(NULL,fs.st_size,PROT_READ,MAP_PRIVATE,fd,0); close(fd); if(*pointer==MAP_FAILED){ *pointer=NULL; return -3; } return fs.st_size; } void unload_file(void* pointer, size_t size){ munmap(pointer,size); } /* handler for all your 404 needs */ void user_error(const char* text){ FCGI_SetExitStatus(404); puts(HEAD); puts(HEAD_HTML); puts(BODY); puts(text); puts(FOOT); } void reply_form_proper(const unsigned int n){ /* printf("",m_w,m_h); if(message[n].parent!=-1) printf("",message[n].parent,message[n].deleted&1?"del":"leaf",m_w/2); print_message(n); puts(""); */ fputs("

",stdout); /* print_message_html(n); */ aux_thread(n); puts("
"); puts("
"); // text field to check if poster has even seen the website puts("Please type leaf into this field:
"); printf("",n); puts(""); puts("
"); puts("
"); puts("
"); } //char* last_visitor; // 32 bit (ulong) are enough for IPv4; IPv6 requires 128; if string, don't use after free //char last_visitor[16]; //unsigned short number_of_visits=0; // but what should the limit be? number of leafs? better: rate limiter, timecheck. //unsigned long last_post_timestamp; // should we calculate the number of visitors during the last 24 hours? // would be easy: just go from tail back until timestamp is older than that. // better: keep an index, compare every time, adjust if necessary; deliver difference from tail. int main(void){ const int size=sizeof(tbheader_t)+sizeof(leaf_t)*MB_BUF_SIZE; printf("size: %d\n",size); int r=0; int buffd=open(BUFFER,O_RDWR); if(buffd==-1){ // need to create the file, need to make it big enough, and need to map it. buffd=open(BUFFER,O_RDWR|O_CREAT|O_TRUNC,0664); if(buffd!=-1){ if( lseek(buffd, size, SEEK_SET) != -1 ){ if( write(buffd, "", 1) != -1 ){ tree=mmap(NULL,size,PROT_READ|PROT_WRITE,MAP_SHARED,buffd,0); if(tree!=MAP_FAILED){ tree->magic=TREE_MAGIC; tree->tail=1; // add message message=tree->content; message[0].timestamp=(unsigned long)time(NULL); heir[0]=0; children[0]=0; b[0]=1; // the message is as wide as its heirs combined d[0]=1; // the message is at the lowest level in its subtree x[0]=0; // the message is centered initially //t[0]=message[0].timestamp; // youngest descendent is itself strcpy(message[0].text,"Welcome to the treeboard. What would you like to talk about?"); }else{ perror("when mmapping new file"); r=2; } }else{ perror("when adjusting file size"); r=6; } }else{ perror("lseek failed:"); r=5; } }else{ perror("When opening content buffer file"); r=1; } }else{ // file exists tree=mmap(NULL,size,PROT_READ|PROT_WRITE,MAP_SHARED,buffd,0); if(tree!=MAP_FAILED){ if(tree->magic==TREE_MAGIC){ message=tree->content; // recalculate variables here: traverse tree (chronologically) unsigned short cur=tree->head; do{ // assume tree contains at least one node update_lineage(cur); cur++; }while(cur!=tree->tail); }else{ fprintf(stderr,"wrong file type. magic number was %04X\n",tree->magic); r=7; } }else{ perror("When mmapping file"); r=2; } } // favicons, if any void* favico=NULL,*favpng=NULL; size_t favico_size=0, favpng_size=0; favico_size=load_file("./favicon.ico",&favico); favpng_size=load_file("./favicon.png",&favpng); if(r==0){ // let's get it started in here char* query_string; char* script_name; char* request_method; while(FCGI_Accept()>=0){ script_name=getenv("SCRIPT_NAME"); request_method=getenv("REQUEST_METHOD"); if(script_name==NULL){ FCGI_SetExitStatus(404); continue; } // script_name contains the path; we want to be path agnostic. char* script_path=script_name; script_name=strrchr(script_name,'/'); *(script_name++)='\0'; now=(unsigned long)time(NULL); // this cascade determines how we handle the request // should be sorted by likelihood of relevance // NOTE: a byte-by-byte parser would be more efficient // than repeated strncmp()s, but less legible // and thus less maintainable. // may be implemented later. // // the commands are: // GET: // "css" : for style // "report" : form for deleting a message // "reply" : form for adding a message // "tree" : print a tree // "latest" : newest n messages // "flip": latest sideways // "json" : json api // "chan" : kareha-style // "bb" : phpbb-style // "table" : sort of like tree, but pure HTML // "favicon.{png,ico}" : favicons, if they exist // TODO: replace tableview with flipmode, we already have chanview for html // TODO: javascript, for adding leafs in place // POST: // "reply" : add a new message // "revoke" : overwrite a message and change its class if(strncmp("GET",request_method,3)==0){ query_string=getenv("QUERY_STRING"); if((*script_name)=='\0' || strncmp("latest",script_name,7)==0){ int n; if(query_string==NULL || (*query_string)=='\0')n=50; else n=atoi(query_string); { unsigned short total=tree->tail-tree->head; if(total && n>total)n=total; } // unsigned long date=strtotime(getenv("HTTP_IF_MODIFIED_SINCE")); // if(datetail-tree->head)printf("more?\n",n+50); woodcraft(n); #ifdef REVOKE puts("

illegal content?"); #endif printf("",(b[(unsigned short)(tree->tail-n)])*m_w/2); puts(FOOT); // }else{ // FCGI_SetExitStatus(304); // } } else if(strncmp("flip",script_name,5)==0){ int n; if(query_string==NULL || (*query_string)=='\0')n=50; else n=atoi(query_string); { unsigned short total=tree->tail-tree->head; if(total && n>total)n=total; } // unsigned long date=strtotime(getenv("HTTP_IF_MODIFIED_SINCE")); // if(datetail-tree->head)printf("more?\n",n+50); flipmode(n); #ifdef REVOKE puts("

illegal content?"); #endif printf("",(b[(unsigned short)(tree->tail-n)])*m_w/2); puts(FOOT); // }else{ // FCGI_SetExitStatus(304); // } } else if(strncmp("tree",script_name,5)==0){ int n; if(query_string==NULL)n=tree->head; // TODO: if buffer is full, there is most likely more than one root else{ n=atoi(query_string); if(n<0 || (tree->head==0 && n>=tree->tail)){ user_error("ID does not exist. Go back to top?"); continue; } } FCGI_SetExitStatus(200); thread_svg(n); } else if(strcmp("css",script_name)==0){ FCGI_SetExitStatus(200); puts("Content-type: text/css\r\n\r"); puts(CSS); } else if(strncmp("chan",script_name,4)==0){ FCGI_SetExitStatus(200); int n; if(query_string==NULL || (*query_string)=='\0'){ FCGI_SetExitStatus(200); print_chan(); continue; }else{ n=atoi(query_string); if(n<0||n>MB_BUF_SIZE||(tree->head==0 && tree->tailGo back?"); continue; } } FCGI_SetExitStatus(200); print_thread(n); } else if(strncmp("bb",script_name,4)==0){ int n; if(query_string==NULL || (*query_string)=='\0')n=50; else{ n=atoi(query_string); if(n<0 || (tree->head==0 && n>=tree->tail)){ user_error("ID does not exist. Go back to top?"); continue; } } FCGI_SetExitStatus(200); print_bb_latest(n); } else if(strncmp("json",script_name,5)==0){ FCGI_SetExitStatus(200); puts("Content-Type: application/json; charset-encoding=UTF-8\r\n\r"); if(query_string==NULL || *query_string=='\0'){ // entire database as list unsigned short i=tree->head; printf("[{\"id\":%d,\"timestamp\":%lu,\"flags\":%d,\"parent\":%d,\"text\":\"",i,message[i].timestamp,message[i].flags,message[i].parent); mdea(message[i].text); putchar('"');putchar('}'); for(i++;i!=tree->tail;i++){ printf(",{\"id\":%d,\"timestamp\":%lu,\"flags\":%d,\"parent\":%d,\"text\":\"",i,message[i].timestamp,message[i].flags,message[i].parent); mdea(message[i].text); putchar('"');putchar('}'); } puts("]"); } else{ // subtree as s-exp int n=atoi(query_string); if(n<0||(tree->head==0 && n>=tree->tail)){ puts("{}"); FCGI_SetExitStatus(404); continue; } fputs("{\"text\":\"",stdout); mdea(message[n].text); //fputs("\"",stdout); printf("\",\"timestamp\":%lu",message[n].timestamp); if(message[n].flags&1)fputs(",\"deleted\"",stdout); if(heir[n]!=n){ putchar(','); json_print(heir[n]); } puts("}"); } } else if(strcmp("table",script_name)==0){ int n; if(query_string==NULL || (*query_string)=='\0'){ FCGI_SetExitStatus(200); print_table_latest(50); continue; }else{ n=atoi(query_string); if(n<0||n>MB_BUF_SIZE||(tree->head==0 && tree->tail

illegal content?"); #endif puts(FOOT); } else if(strncmp("reply",script_name,6)==0){ int n; if(query_string==NULL) n=tree->head; else{ n=atoi(query_string); if(n<0||n>MB_BUF_SIZE){ user_error(""); continue; } } FCGI_SetExitStatus(200); puts(HEAD); puts(HEAD_HTML); puts(BODY); reply_form_proper(n); puts(FOOT); } else if(strncmp("favicon.",script_name,8)==0){ if(strcmp(script_name+8,"png")==0 && favpng!=NULL){ // serve favpng FCGI_SetExitStatus(200); puts("Content-type: image/png\r"); printf("Content-length: %d\r\n\r\n",favpng_size); for(size_t remaining=favpng_size;remaining>0;remaining-=fwrite(favpng,1,favpng_size,stdout)); continue; } if(strcmp(script_name+8,"ico")==0 && favico!=NULL){ // serve favico FCGI_SetExitStatus(200); puts("Content-type: image/x-icon\r"); printf("Content-length: %d\r\n\r\n",favico_size); for(size_t remaining=favico_size;remaining>0;remaining-=fwrite(favico,1,favico_size,stdout)); continue; } user_error("I don't have a favicon for you, sorry."); } #ifdef REVOKE else if(strncmp("report",script_name,7)==0){ FCGI_SetExitStatus(200); puts(HEAD); puts(HEAD_HTML); puts(BODY); /* puts("Dieses Formular existiert, weil nach geltendem Recht gewisse Inhalte unverzüglich und ohne Prüfung gelöscht werden müssen, sobald diese dem Server bekannt gemacht werden. Geben Sie hier die ID-Nummer des Beitrags ein, den Sie beschuldigen wollen, und wir lassen die Beweise verschwinden, wie vom Gesetz gefordert.

Austrian law requires the immediate deletion of any content suspected of being of a certain nature as soon as the server receives notice, without delay. If you intend to accuse any post here, enter its ID below, and the evidence will be erased, as demanded by law."); */ puts("This form exists because some jurisdictions require the immediate deletion of any content accused of being of a certain nature as soon as the server receives notice, without delay. If you intend to accuse any post here, enter its ID below, and the evidence will be erased immediately, as demanded by law."); puts("

"); puts(""); puts("
"); puts(FOOT); } #endif else { // what other names would this program even have? char* path=getenv("PATH_INFO"); FCGI_SetExitStatus(404); puts(HEAD); puts(HEAD_HTML); puts(BODY); printf("You must have mistyped. Don't do that. Find your way here."); puts(FOOT); //user_error(); } } else if(strncmp("POST",request_method,4)==0){ int content_length; char* cl_s=getenv("CONTENT_LENGTH"); // content length string if(cl_s!=NULL){ content_length=atoi(cl_s); if(content_length>MB_MAX_CONTENT_LEN){ FCGI_SetExitStatus(413); puts(HEAD); puts(HEAD_HTML); puts("Content length exceeds allotted space.
Go back?"); puts(FOOT); continue; } }else{ // FCGI_SetExitStatus(411); content_length=0; } if(strncmp("reply",script_name,6)==0){ /* we might want to extract the requesting IP. * if the same IP posts too often in a row, it should receive a 402: Free advertising if you pay me enough! * * getenv("REMOTE_ADDR"); */ int c; // insert spam field, check its content (leaf) if((c=getchar())=='s' && (c=getchar())=='p' && (c=getchar())=='m' && (c=getchar())=='=' && (c=getchar())=='l' && (c=getchar())=='e' && (c=getchar())=='a' && (c=getchar())=='f' && (c=getchar())=='&'){ if( (c=getchar())=='m' && (c=getchar())=='s' && (c=getchar())=='g' && (c=getchar())=='='){ unsigned short msg=0; while((c=getchar())>='0' && c<='9'){ msg*=10; msg+=c-'0'; } if(c!='&'){ FCGI_SetExitStatus(400); puts(HEAD); puts(HEAD_HTML); puts(BODY); puts("You must be tired. Get some sleep."); puts(FOOT); continue; } if((c=getchar())=='t' && (c=getchar())=='e' && (c=getchar())=='x' && (c=getchar())=='t' && (c=getchar())=='='){ if(msg==tree->tail || (tree->head!=tree->tail && msg>tree->tail)){ FCGI_SetExitStatus(403); puts(HEAD); puts(HEAD_HTML); puts(BODY); puts("The message you attempt to reply to should exist first."); puts(FOOT); continue; } int nid=add_message(msg); if(nid>=0){ // cobble together the location string char* host; if((host=getenv("HTTP_HOST"))==NULL){ // only if HTTP<1.1 if((host=getenv("SEVER_NAME"))==NULL){ // other fallbacks? referer? listen addr? FCGI_SetExitStatus(200); puts(HEAD); puts(HEAD_HTML); puts(BODY); printf("Message has been added, not to worry. Find it here.
I would redirect you there automatically, but your browser is too old. And for some reason I am not sure of my name.\n",query_string); puts(FOOT); continue; } } FCGI_SetExitStatus(201); // location printf("Location: http"); if(getenv("HTTPS"))putchar('s'); printf("://%s",host); if(*script_path)printf("%s",script_path); printf("/tree?%d\r\n",nid); // refresh: all browsers seem to ignore this in favour of location printf("Refresh: 0;url="); if(getenv("HTTPS"))printf("https");else printf("http"); printf("://%s",host); if(*script_path)printf("%s",script_path); printf("/tree?%d\r\n\r\n",msg); /* FCGI_SetExitStatus(202); printf("Refresh: 0;url="); if(getenv("HTTPS"))printf("https");else printf("http"); printf("://%s",host); if(*script_path)printf("%s",script_path); printf("/tree?%d\r\n",msg); */ puts(HEAD); }else if(nid==-1){ FCGI_SetExitStatus(404); puts(HEAD); puts(HEAD_HTML); puts(BODY); puts("No empty messages, please.
If you are using Internet Explorer, you will not be able to see message content in the tree view. Use tableview or chanview instead. Also consider switching to any other web browser."); puts(FOOT); }else if(nid==-2){ FCGI_SetExitStatus(413); puts(HEAD); puts(HEAD_HTML); puts(BODY); printf("Message length exceeds allotted space.
Try again or Go back.",msg,msg); puts(FOOT); }else if(nid==-3){ FCGI_SetExitStatus(404); puts(HEAD); puts(HEAD_HTML); puts(BODY); puts("Message contains at least one illegal character. For shame!"); puts(FOOT); }else if(nid==-4){ FCGI_SetExitStatus(402); puts(HEAD); puts(HEAD_HTML); puts(BODY); puts("Looks like you found a commercial use for my board. Care to cut me in?"); puts(FOOT); }else{ FCGI_SetExitStatus(500); puts(HEAD); puts(HEAD_HTML); puts(BODY); puts("I have no idea what just happened."); puts(FOOT); } }else{ FCGI_SetExitStatus(400); puts(HEAD); puts(HEAD_HTML); puts(BODY); puts("It's just two parameters, it's not that complicated."); puts(FOOT); } }else{ FCGI_SetExitStatus(400); puts(HEAD); puts(HEAD_HTML); puts(BODY); puts("You botched the first variable. Keep trying."); puts(FOOT); continue; } }else{ puts(HEAD); puts(HEAD_HTML); puts(BODY); puts("

Please read the instructions carefully.
We apologize for the inconvenience.
Brought to you by the initiative for a greener and healthier network environment.

"); // attempt to extract message id from parameters // unfortunately we duplicate code here while(c!='&')c=getchar(); // skip towards next parameter if we are not there already if( (c=getchar())=='m' && (c=getchar())=='s' && (c=getchar())=='g' && (c=getchar())=='='){ unsigned short msg=0; while((c=getchar())>='0' && c<='9'){ msg*=10; msg+=c-'0'; } reply_form_proper(msg); // NOTE: we could insert the message text into the form, but the user agent probably retained it anyway }else{ // it failed printf("

back

",getenv("HTTP_REFERER")); } puts(FOOT); FCGI_SetExitStatus(400); } } #ifdef REVOKE else if(strncmp("revoke",script_name,3)==0){ int c; int msg_id=0; if((c=getchar())=='d' && (c=getchar())=='e' && (c=getchar())=='l' && (c=getchar())=='='){ while((c=getchar())>='0'&&c<='9'){ msg_id*=10; msg_id+=c-'0'; } if(c!=EOF){ /* get angry */ } }else{ FCGI_SetExitStatus(400); continue; } // TBI: also check referer if(!message[msg_id].flags&1){ sprintf(message[msg_id].text,"Content erased by: %s",getenv("REMOTE_ADDR")); message[msg_id].flags|=1; FCGI_SetExitStatus(303); puts(HEAD); printf("Location: %s://%s%s/tree?%d\r\n\r\n",getenv("HTTPS")?"https":"http",getenv("HTTP_HOST"),script_path,msg_id); puts(HEAD_HTML); puts(BODY); puts("You should be ashamed of yourself."); puts(FOOT); }else{ FCGI_SetExitStatus(410); puts(HEAD); puts(HEAD_HTML); puts(BODY); puts("That message had been deleted already."); puts(FOOT); } } #endif else{ // handle a POST invalid_url FCGI_SetExitStatus(418); //FCGI_SetExitStatus(404); puts(HEAD); puts(HEAD_HTML); puts(BODY); puts("Wrong server?"); puts(FOOT); } }else{ // neither GET nor POST FCGI_SetExitStatus(501); puts(HEAD); puts(HEAD_HTML); puts(BODY); puts("Method not implemented.
Method was: "); puts(request_method); puts(FOOT); } } // wend } munmap(tree,0); close(buffd); unload_file(favico,favico_size); unload_file(favpng,favpng_size); return r; }