Annotation of libwww/Library/src/HTNews.c, revision 2.16

1.1       timbl       1: /*                     NEWS ACCESS                             HTNews.c
                      2: **                     ===========
                      3: **
                      4: ** History:
                      5: **     26 Sep 90       Written TBL
                      6: **     29 Nov 91       Downgraded to C, for portable implementation.
2.16    ! luotonen    7: **     16 Feb 94       Added Lou Montulli's Lynx & LIST NEWSGROUPS diffs.
1.1       timbl       8: */
1.2       timbl       9: /* Implements:
                     10: */
                     11: #include "HTNews.h"
1.1       timbl      12: 
1.3       timbl      13: #define CR   FROMASCII('\015') /* Must be converted to ^M for transmission */
                     14: #define LF   FROMASCII('\012') /* Must be converted to ^J for transmission */
                     15: 
1.1       timbl      16: #define NEWS_PORT 119          /* See rfc977 */
                     17: #define APPEND                 /* Use append methods */
                     18: #define MAX_CHUNK      40      /* Largest number of articles in one window */
                     19: #define CHUNK_SIZE     20      /* Number of articles for quick display */
                     20: 
                     21: #ifndef DEFAULT_NEWS_HOST
                     22: #define DEFAULT_NEWS_HOST "news"
                     23: #endif
                     24: #ifndef SERVER_FILE
                     25: #define SERVER_FILE "/usr/local/lib/rn/server"
                     26: #endif
                     27: 
                     28: #include <ctype.h>
                     29: #include "HTUtils.h"           /* Coding convention macros */
                     30: #include "tcp.h"
                     31: 
1.2       timbl      32: #include "HTML.h"
1.1       timbl      33: #include "HTParse.h"
                     34: #include "HTFormat.h"
2.8       timbl      35: #include "HTAlert.h"
1.1       timbl      36: 
2.8       timbl      37: #define BIG 1024 /* @@@ */
                     38: 
1.2       timbl      39: struct _HTStructured {
                     40:        CONST HTStructuredClass *       isa;
                     41:        /* ... */
                     42: };
                     43: 
2.7       timbl      44: #define NEWS_PROGRESS(foo) HTProgress(foo)
1.1       timbl      45: 
                     46: 
2.12      timbl      47: #define NEXT_CHAR HTInputSocket_getCharacter(isoc)
1.1       timbl      48: #define LINE_LENGTH 512                        /* Maximum length of line of ARTICLE etc */
                     49: #define GROUP_NAME_LENGTH      256     /* Maximum length of group name */
                     50: 
                     51: 
                     52: /*     Module-wide variables
                     53: */
1.2       timbl      54: PUBLIC char * HTNewsHost;
1.1       timbl      55: PRIVATE struct sockaddr_in soc_address;                /* Binary network address */
                     56: PRIVATE int s;                                 /* Socket for NewsHost */
                     57: PRIVATE char response_text[LINE_LENGTH+1];     /* Last response */
1.2       timbl      58: /* PRIVATE HText *     HT;     */              /* the new hypertext */
                     59: PRIVATE HTStructured * target;                 /* The output sink */
                     60: PRIVATE HTStructuredClass targetClass;         /* Copy of fn addresses */
1.1       timbl      61: PRIVATE HTParentAnchor *node_anchor;           /* Its anchor */
                     62: PRIVATE int    diagnostic;                     /* level: 0=none 2=source */
                     63: 
1.2       timbl      64: 
                     65: #define PUTC(c) (*targetClass.put_character)(target, c)
                     66: #define PUTS(s) (*targetClass.put_string)(target, s)
                     67: #define START(e) (*targetClass.start_element)(target, e, 0, 0)
                     68: #define END(e) (*targetClass.end_element)(target, e)
                     69: 
2.11      timbl      70: PUBLIC HTInputSocket *isoc;            /* @@@ non-reentrant */
                     71: 
1.2       timbl      72: PUBLIC CONST char * HTGetNewsHost NOARGS
                     73: {
                     74:        return HTNewsHost;
                     75: }
1.1       timbl      76: 
1.2       timbl      77: PUBLIC void HTSetNewsHost ARGS1(CONST char *, value)
                     78: {
                     79:        StrAllocCopy(HTNewsHost, value);
                     80: }
1.1       timbl      81: 
                     82: /*     Initialisation for this module
                     83: **     ------------------------------
                     84: **
                     85: **     Except on the NeXT, we pick up the NewsHost name from
                     86: **
                     87: **     1.      Environment variable NNTPSERVER
                     88: **     2.      File SERVER_FILE
                     89: **     3.      Compilation time macro DEFAULT_NEWS_HOST
                     90: **     4.      Default to "news"
                     91: **
                     92: **     On the NeXT, we pick up the NewsHost name from, in order:
                     93: **
                     94: **     1.      WorldWideWeb default "NewsHost"
                     95: **     2.      Global default "NewsHost"
                     96: **     3.      News default "NewsHost"
                     97: **     4.      Compilation time macro DEFAULT_NEWS_HOST
                     98: **     5.      Default to "news"
                     99: */
                    100: PRIVATE BOOL initialized = NO;
                    101: PRIVATE BOOL initialize NOARGS
                    102: {
                    103:     CONST struct hostent  *phost;        /* Pointer to host - See netdb.h */
                    104:     struct sockaddr_in* sin = &soc_address;
                    105: 
                    106:         
                    107: /*  Set up defaults:
                    108: */
                    109:     sin->sin_family = AF_INET;         /* Family = internet, host order  */
                    110:     sin->sin_port = htons(NEWS_PORT);   /* Default: new port,    */
                    111: 
                    112: /*   Get name of Host
                    113: */
                    114: #ifdef NeXTStep
1.2       timbl     115:     if ((HTNewsHost = NXGetDefaultValue("WorldWideWeb","NewsHost"))==0)
                    116:         if ((HTNewsHost = NXGetDefaultValue("News","NewsHost")) == 0)
                    117:            HTNewsHost = DEFAULT_NEWS_HOST;
1.1       timbl     118: #else
                    119:     if (getenv("NNTPSERVER")) {
1.2       timbl     120:         StrAllocCopy(HTNewsHost, (char *)getenv("NNTPSERVER"));
1.1       timbl     121:        if (TRACE) fprintf(stderr, "HTNews: NNTPSERVER defined as `%s'\n",
1.2       timbl     122:                HTNewsHost);
1.1       timbl     123:     } else {
                    124:         char server_name[256];
                    125:         FILE* fp = fopen(SERVER_FILE, "r");
                    126:         if (fp) {
                    127:            if (fscanf(fp, "%s", server_name)==1) {
1.2       timbl     128:                StrAllocCopy(HTNewsHost, server_name);
1.1       timbl     129:                if (TRACE) fprintf(stderr,
                    130:                "HTNews: File %s defines news host as `%s'\n",
1.2       timbl     131:                        SERVER_FILE, HTNewsHost);
1.1       timbl     132:            }
                    133:            fclose(fp);
                    134:        }
                    135:     }
1.2       timbl     136:     if (!HTNewsHost) HTNewsHost = DEFAULT_NEWS_HOST;
1.1       timbl     137: #endif
                    138: 
1.2       timbl     139:     if (*HTNewsHost>='0' && *HTNewsHost<='9') {   /* Numeric node address: */
                    140:        sin->sin_addr.s_addr = inet_addr((char *)HTNewsHost); /* See arpa/inet.h */
1.1       timbl     141: 
                    142:     } else {               /* Alphanumeric node name: */
1.2       timbl     143:        phost=gethostbyname((char*)HTNewsHost); /* See netdb.h */
1.1       timbl     144:        if (!phost) {
2.7       timbl     145:            char message[150];          /* @@@ */
                    146:            sprintf(message, 
                    147:            "HTNews: Can't find news host `%s'.\n%s",HTNewsHost,
                    148:            "Please define your NNTP server");
                    149:            HTAlert(message);
1.1       timbl     150:            CTRACE(tfp,
1.2       timbl     151:              "HTNews: Can't find news host `%s'.\n",HTNewsHost);
1.1       timbl     152:            return NO;  /* Fail */
                    153:        }
                    154:        memcpy(&sin->sin_addr, phost->h_addr, phost->h_length);
                    155:     }
                    156: 
                    157:     if (TRACE) fprintf(stderr,  
                    158:        "HTNews: Parsed address as port %4x, inet %d.%d.%d.%d\n",
                    159:                (unsigned int)ntohs(sin->sin_port),
                    160:                (int)*((unsigned char *)(&sin->sin_addr)+0),
                    161:                (int)*((unsigned char *)(&sin->sin_addr)+1),
                    162:                (int)*((unsigned char *)(&sin->sin_addr)+2),
                    163:                (int)*((unsigned char *)(&sin->sin_addr)+3));
                    164: 
                    165:     s = -1;            /* Disconnected */
                    166:     
                    167:     return YES;
                    168: }
                    169: 
                    170: 
                    171: 
                    172: /*     Send NNTP Command line to remote host & Check Response
                    173: **     ------------------------------------------------------
                    174: **
                    175: ** On entry,
                    176: **     command points to the command to be sent, including CRLF, or is null
                    177: **             pointer if no command to be sent.
                    178: ** On exit,
                    179: **     Negative status indicates transmission error, socket closed.
                    180: **     Positive status is an NNTP status.
                    181: */
                    182: 
                    183: 
                    184: PRIVATE int response ARGS1(CONST char *,command)
                    185: {
                    186:     int result;    
                    187:     char * p = response_text;
                    188:     if (command) {
                    189:         int status;
                    190:        int length = strlen(command);
                    191:        if (TRACE) fprintf(stderr, "NNTP command to be sent: %s", command);
                    192: #ifdef NOT_ASCII
                    193:        {
                    194:            CONST char  * p;
                    195:            char        * q;
                    196:            char ascii[LINE_LENGTH+1];
                    197:            for(p = command, q=ascii; *p; p++, q++) {
                    198:                *q = TOASCII(*p);
                    199:            }
                    200:             status = NETWRITE(s, ascii, length);
                    201:        }
                    202: #else
                    203:         status = NETWRITE(s, command, length);
                    204: #endif
                    205:        if (status<0){
                    206:            if (TRACE) fprintf(stderr,
                    207:                "HTNews: Unable to send command. Disconnecting.\n");
                    208:            NETCLOSE(s);
2.11      timbl     209:            HTInputSocket_free(isoc);
1.1       timbl     210:            s = -1;
                    211:            return status;
                    212:        } /* if bad status */
                    213:     } /* if command to be sent */
                    214:     
                    215:     for(;;) {  
1.3       timbl     216:        if (((*p++=NEXT_CHAR) == LF)
                    217:                        || (p == &response_text[LINE_LENGTH])) {
1.1       timbl     218:            *p++=0;                             /* Terminate the string */
                    219:            if (TRACE) fprintf(stderr, "NNTP Response: %s\n", response_text);
                    220:            sscanf(response_text, "%d", &result);
                    221:            return result;          
                    222:        } /* if end of line */
                    223:        
                    224:        if (*(p-1) < 0) {
                    225:            if (TRACE) fprintf(stderr,
                    226:                "HTNews: EOF on read, closing socket %d\n", s);
                    227:            NETCLOSE(s);        /* End of file, close socket */
2.11      timbl     228:            HTInputSocket_free(isoc);
1.1       timbl     229:            return s = -1;      /* End of file on response */
                    230:        }
                    231:     } /* Loop over characters */
                    232: }
                    233: 
                    234: 
                    235: /*     Case insensitive string comparisons
                    236: **     -----------------------------------
                    237: **
                    238: ** On entry,
                    239: **     template must be already un upper case.
                    240: **     unknown may be in upper or lower or mixed case to match.
                    241: */
                    242: PRIVATE BOOL match ARGS2 (CONST char *,unknown, CONST char *,template)
                    243: {
                    244:     CONST char * u = unknown;
                    245:     CONST char * t = template;
                    246:     for (;*u && *t && (TOUPPER(*u)==*t); u++, t++) /* Find mismatch or end */ ;
                    247:     return (BOOL)(*t==0);              /* OK if end of template */
                    248: }
                    249: 
                    250: /*     Find Author's name in mail address
                    251: **     ----------------------------------
                    252: **
                    253: ** On exit,
                    254: **     THE EMAIL ADDRESS IS CORRUPTED
                    255: **
                    256: ** For example, returns "Tim Berners-Lee" if given any of
                    257: **     " Tim Berners-Lee <tim@online.cern.ch> "
                    258: **  or " tim@online.cern.ch ( Tim Berners-Lee ) "
                    259: */
                    260: PRIVATE char * author_name ARGS1 (char *,email)
                    261: {
                    262:     char *s, *e;
                    263:     
                    264:     if ((s=strchr(email,'(')) && (e=strchr(email, ')')))
                    265:         if (e>s) {
                    266:            *e=0;                       /* Chop off everything after the ')'  */
                    267:            return HTStrip(s+1);        /* Remove leading and trailing spaces */
                    268:        }
                    269:        
                    270:     if ((s=strchr(email,'<')) && (e=strchr(email, '>')))
                    271:         if (e>s) {
                    272:            strcpy(s, e+1);             /* Remove <...> */
                    273:            return HTStrip(email);      /* Remove leading and trailing spaces */
                    274:        }
                    275:        
                    276:     return HTStrip(email);             /* Default to the whole thing */
                    277: 
                    278: }
                    279: 
1.2       timbl     280: /*     Start anchor element
                    281: **     --------------------
                    282: */
                    283: PRIVATE void start_anchor ARGS1(CONST char *,  href)
                    284: {
                    285:     BOOL               present[HTML_A_ATTRIBUTES];
                    286:     CONST char*                value[HTML_A_ATTRIBUTES];
                    287:     
                    288:     {
                    289:        int i;
                    290:        for(i=0; i<HTML_A_ATTRIBUTES; i++)
                    291:            present[i] = (i==HTML_A_HREF);
                    292:     }
                    293:     value[HTML_A_HREF] = href;
                    294:     (*targetClass.start_element)(target, HTML_A , present, value);
                    295: 
                    296: }
1.1       timbl     297: 
2.16    ! luotonen  298: 
        !           299: /*      Start link element
        !           300: **      --------------------
        !           301: */
        !           302: PRIVATE void start_link ARGS2(CONST char *,  href, CONST char *, rev)
        !           303: {
        !           304: #ifdef WHEN_WE_HAVE_HTMLPLUS
        !           305: 
        !           306:     BOOL                present[HTML_LINK_ATTRIBUTES];
        !           307:     CONST char*         value[HTML_LINK_ATTRIBUTES];
        !           308:    
        !           309:     {
        !           310:         int i;
        !           311:         for(i=0; i<HTML_LINK_ATTRIBUTES; i++)
        !           312:             present[i] = (i==HTML_LINK_HREF || i==HTML_LINK_REV);
        !           313:     }
        !           314:     value[HTML_LINK_HREF] = href;
        !           315:     value[HTML_LINK_REV]  = rev;
        !           316:     (*targetClass.start_element)(target, HTML_LINK , present, value);
        !           317: 
        !           318: #endif
        !           319: }
        !           320: 
        !           321: 
        !           322: 
        !           323: 
1.1       timbl     324: /*     Paste in an Anchor
                    325: **     ------------------
                    326: **
                    327: **
                    328: ** On entry,
                    329: **     HT      has a selection of zero length at the end.
                    330: **     text    points to the text to be put into the file, 0 terminated.
                    331: **     addr    points to the hypertext refernce address,
                    332: **             terminated by white space, comma, NULL or '>' 
                    333: */
                    334: PRIVATE void write_anchor ARGS2(CONST char *,text, CONST char *,addr)
                    335: {
                    336:     char href[LINE_LENGTH+1];
                    337:                
                    338:     {
                    339:        CONST char * p;
                    340:        strcpy(href,"news:");
                    341:        for(p=addr; *p && (*p!='>') && !WHITE(*p) && (*p!=','); p++);
                    342:         strncat(href, addr, p-addr);   /* Make complete hypertext reference */
                    343:     }
                    344:     
1.2       timbl     345:     start_anchor(href);
                    346:     PUTS(text);
                    347:     END(HTML_A);
1.1       timbl     348: }
                    349: 
                    350: 
                    351: /*     Write list of anchors
                    352: **     ---------------------
                    353: **
                    354: **     We take a pointer to a list of objects, and write out each,
                    355: **     generating an anchor for each.
                    356: **
                    357: ** On entry,
                    358: **     HT      has a selection of zero length at the end.
                    359: **     text    points to a comma or space separated list of addresses.
                    360: ** On exit,
                    361: **     *text   is NOT any more chopped up into substrings.
                    362: */
                    363: PRIVATE void write_anchors ARGS1 (char *,text)
                    364: {
                    365:     char * start = text;
                    366:     char * end;
                    367:     char c;
                    368:     for (;;) {
                    369:         for(;*start && (WHITE(*start)); start++);  /* Find start */
                    370:        if (!*start) return;                    /* (Done) */
                    371:         for(end=start; *end && (*end!=' ') && (*end!=','); end++);/* Find end */
                    372:        if (*end) end++;        /* Include comma or space but not NULL */
                    373:        c = *end;
                    374:        *end = 0;
                    375:        write_anchor(start, start);
2.16    ! luotonen  376:        START(HTML_BR);
1.1       timbl     377:        *end = c;
                    378:        start = end;                    /* Point to next one */
                    379:     }
                    380: }
                    381: 
                    382: /*     Abort the connection                                    abort_socket
                    383: **     --------------------
                    384: */
                    385: PRIVATE void abort_socket NOARGS
                    386: {
                    387:     if (TRACE) fprintf(stderr,
                    388:            "HTNews: EOF on read, closing socket %d\n", s);
                    389:     NETCLOSE(s);       /* End of file, close socket */
2.11      timbl     390:     HTInputSocket_free(isoc);
1.2       timbl     391:     PUTS("Network Error: connection lost");
                    392:     PUTC('\n');
1.1       timbl     393:     s = -1;            /* End of file on response */
                    394:     return;
                    395: }
                    396: 
                    397: /*     Read in an Article                                      read_article
                    398: **     ------------------
                    399: **
                    400: **
                    401: **     Note the termination condition of a single dot on a line by itself.
                    402: **     RFC 977 specifies that the line "folding" of RFC850 is not used, so we
                    403: **     do not handle it here.
                    404: **
                    405: ** On entry,
                    406: **     s       Global socket number is OK
                    407: **     HT      Global hypertext object is ready for appending text
                    408: */       
                    409: PRIVATE void read_article NOARGS
                    410: {
                    411: 
                    412:     char line[LINE_LENGTH+1];
                    413:     char *references=NULL;                     /* Hrefs for other articles */
                    414:     char *newsgroups=NULL;                     /* Newsgroups list */
                    415:     char *p = line;
                    416:     BOOL done = NO;
                    417:     
                    418: /*     Read in the HEADer of the article:
                    419: **
                    420: **     The header fields are either ignored, or formatted and put into the
                    421: **      Text.
                    422: */
                    423:     if (!diagnostic) {
1.2       timbl     424:         (*targetClass.start_element)(target, HTML_ADDRESS, 0, 0);
1.1       timbl     425:        while(!done){
                    426:            char ch = *p++ = NEXT_CHAR;
                    427:            if (ch==(char)EOF) {
                    428:                abort_socket(); /* End of file, close socket */
                    429:                return;         /* End of file on response */
                    430:            }
1.3       timbl     431:            if ((ch == LF) || (p == &line[LINE_LENGTH])) {
1.1       timbl     432:                *--p=0;                         /* Terminate the string */
                    433:                if (TRACE) fprintf(stderr, "H %s\n", line);
                    434: 
                    435:                if (line[0]=='.') {     
                    436:                    if (line[1]<' ') {          /* End of article? */
                    437:                        done = YES;
                    438:                        break;
                    439:                    }
                    440:                
                    441:                } else if (line[0]<' ') {
                    442:                    break;              /* End of Header? */
2.16    ! luotonen  443: 
1.1       timbl     444:                } else if (match(line, "SUBJECT:")) {
1.2       timbl     445:                    END(HTML_ADDRESS);
                    446:                    START(HTML_TITLE);                  /** Uuugh! @@@ */
2.16    ! luotonen  447:                    PUTS(line+9);
        !           448:                    END(HTML_TITLE);
        !           449:                    START(HTML_H1);
1.2       timbl     450:                    PUTS(line+8);
2.16    ! luotonen  451:                    END(HTML_H1);
1.2       timbl     452:                    START(HTML_ADDRESS);
2.16    ! luotonen  453: 
1.1       timbl     454:                } else if (match(line, "DATE:")
                    455:                        || match(line, "ORGANIZATION:")) {
2.16    ! luotonen  456:                    PUTS(strchr(line,':')+2);
        !           457:                    START(HTML_BR);
        !           458: 
        !           459:                } else if(match(line, "FROM:")) {
        !           460:                   char * temp=0;
        !           461:                   char * href=0;
        !           462:                   char *cp1, *cp2;
        !           463: 
        !           464:                   /* copy into temporary storage */
        !           465:                   StrAllocCopy(temp, strchr(line,':')+1);
        !           466: 
        !           467:                   cp1=temp;
        !           468:                   while(isspace(*cp1)) cp1++;
        !           469:                   /* remove space and stuff after */
        !           470:                   if((cp2 = strchr(cp1,' ')) != NULL)
        !           471:                      *cp2 = '\0';
        !           472: 
        !           473:                   StrAllocCopy(href,"mailto:");
        !           474:                   StrAllocCat(href,cp1);
        !           475: 
        !           476:                   start_anchor(href);
        !           477:                   PUTS("Reply to ");
        !           478:                   PUTS(strchr(line,':')+1);
        !           479:                   END(HTML_A);
        !           480:                   START(HTML_BR);
        !           481: 
        !           482:                   /* put in the owner as a link rel. as well */
        !           483:                   start_link(href, "made");
        !           484:                
        !           485:                   /* free of temp vars */
        !           486:                   free(temp);
        !           487:                   free(href);
        !           488: 
1.1       timbl     489:                } else if (match(line, "NEWSGROUPS:")) {
                    490:                    StrAllocCopy(newsgroups, HTStrip(strchr(line,':')+1));
                    491:                    
                    492:                } else if (match(line, "REFERENCES:")) {
                    493:                    StrAllocCopy(references, HTStrip(strchr(line,':')+1));
                    494:                    
                    495:                } /* end if match */
                    496:                p = line;                       /* Restart at beginning */
                    497:            } /* if end of line */
                    498:        } /* Loop over characters */
2.16    ! luotonen  499:        END(HTML_ADDRESS);
1.1       timbl     500:     
1.2       timbl     501:        if (newsgroups || references) {
2.16    ! luotonen  502:            START(HTML_DL);
1.2       timbl     503:            if (newsgroups) {
2.16    ! luotonen  504: #ifdef POSTING
        !           505:                char *href=0;
        !           506: #endif
        !           507: 
1.2       timbl     508:                (*targetClass.start_element)(target, HTML_DT , 0, 0);
                    509:                PUTS("Newsgroups:");
                    510:                (*targetClass.start_element)(target, HTML_DD , 0, 0);
                    511:                write_anchors(newsgroups);
2.16    ! luotonen  512: 
        !           513: #ifdef POSTING
        !           514:                /* make posting possible */
        !           515:                StrAllocCopy(href,"newspost:");
        !           516:                StrAllocCat(href,newsgroups);
        !           517:                START(HTML_DT);
        !           518:                 start_anchor(href);
        !           519:                 PUTS("Reply to newsgroup(s)");
        !           520:                 END(HTML_A);
        !           521: #endif
        !           522: 
1.2       timbl     523:                free(newsgroups);
                    524:            }
                    525:            
                    526:            if (references) {
                    527:                (*targetClass.start_element)(target, HTML_DT , 0, 0);
                    528:                PUTS("References:");
                    529:                (*targetClass.start_element)(target, HTML_DD , 0, 0);
                    530:                write_anchors(references);
                    531:                free(references);
                    532:            }
2.16    ! luotonen  533: #ifdef WHEN_WE_HAVE_HTMLPLUS
        !           534:            (*targetClass.end_element)(target, HTML_DLC);
        !           535: #else
2.10      timbl     536:            (*targetClass.end_element)(target, HTML_DL);
2.16    ! luotonen  537: #endif
1.1       timbl     538:        }
1.2       timbl     539:        PUTS("\n\n\n");
1.1       timbl     540:        
                    541:     }
                    542:     
                    543: /*     Read in the BODY of the Article:
                    544: */
1.2       timbl     545:     (*targetClass.start_element)(target, HTML_PRE , 0, 0);
                    546: 
1.1       timbl     547:     p = line;
                    548:     while(!done){
                    549:        char ch = *p++ = NEXT_CHAR;
                    550:        if (ch==(char)EOF) {
                    551:            abort_socket();     /* End of file, close socket */
                    552:            return;             /* End of file on response */
                    553:        }
1.3       timbl     554:        if ((ch == LF) || (p == &line[LINE_LENGTH])) {
1.1       timbl     555:            *p++=0;                             /* Terminate the string */
                    556:            if (TRACE) fprintf(stderr, "B %s", line);
                    557:            if (line[0]=='.') {
                    558:                if (line[1]<' ') {              /* End of article? */
                    559:                    done = YES;
                    560:                    break;
                    561:                } else {                        /* Line starts with dot */
1.2       timbl     562:                    PUTS(&line[1]);     /* Ignore first dot */
1.1       timbl     563:                }
                    564:            } else {
                    565: 
                    566: /*     Normal lines are scanned for buried references to other articles.
                    567: **     Unfortunately, it will pick up mail addresses as well!
                    568: */
                    569:                char *l = line;
                    570:                char * p;
2.14      luotonen  571:                while ((p=strchr(l, '<'))) {
1.1       timbl     572:                    char *q  = strchr(p,'>');
                    573:                    char *at = strchr(p, '@');
                    574:                    if (q && at && at<q) {
                    575:                        char c = q[1];
                    576:                        q[1] = 0;               /* chop up */
                    577:                        *p = 0;
1.2       timbl     578:                        PUTS(l);
1.1       timbl     579:                        *p = '<';               /* again */
                    580:                        *q = 0;
1.2       timbl     581:                        start_anchor(p+1);
1.1       timbl     582:                        *q = '>';               /* again */
1.2       timbl     583:                        PUTS(p);
                    584:                        (*targetClass.end_element)(target, HTML_A);
1.1       timbl     585:                        q[1] = c;               /* again */
                    586:                        l=q+1;
                    587:                    } else break;               /* line has unmatched <> */
                    588:                } 
1.2       timbl     589:                PUTS( l);       /* Last bit of the line */
1.1       timbl     590:            } /* if not dot */
                    591:            p = line;                           /* Restart at beginning */
                    592:        } /* if end of line */
                    593:     } /* Loop over characters */
1.2       timbl     594:     
                    595:     (*targetClass.end_element)(target, HTML_PRE);
1.1       timbl     596: }
                    597: 
                    598: 
                    599: /*     Read in a List of Newsgroups
                    600: **     ----------------------------
                    601: */
                    602: /*
                    603: **     Note the termination condition of a single dot on a line by itself.
                    604: **     RFC 977 specifies that the line "folding" of RFC850 is not used, so we
                    605: **     do not handle it here.
                    606: */        
                    607: PRIVATE void read_list NOARGS
                    608: {
                    609: 
                    610:     char line[LINE_LENGTH+1];
                    611:     char *p;
                    612:     BOOL done = NO;
                    613:     
                    614: /*     Read in the HEADer of the article:
                    615: **
                    616: **     The header fields are either ignored, or formatted and put into the
                    617: **     Text.
                    618: */
1.2       timbl     619:     (*targetClass.start_element)(target, HTML_H1 , 0, 0);
                    620:     PUTS( "Newsgroups");
                    621:     (*targetClass.end_element)(target, HTML_PRE);
1.1       timbl     622:     p = line;
2.16    ! luotonen  623:     (*targetClass.start_element)(target, HTML_DL, 0, 0);
1.1       timbl     624:     while(!done){
                    625:        char ch = *p++ = NEXT_CHAR;
                    626:        if (ch==(char)EOF) {
                    627:            abort_socket();     /* End of file, close socket */
                    628:            return;             /* End of file on response */
                    629:        }
1.3       timbl     630:        if ((ch == LF) || (p == &line[LINE_LENGTH])) {
1.1       timbl     631:            *p++=0;                             /* Terminate the string */
                    632:            if (TRACE) fprintf(stderr, "B %s", line);
2.16    ! luotonen  633:            (*targetClass.start_element)(target, HTML_DT , 0, 0);
1.1       timbl     634:            if (line[0]=='.') {
                    635:                if (line[1]<' ') {              /* End of article? */
                    636:                    done = YES;
                    637:                    break;
                    638:                } else {                        /* Line starts with dot */
1.2       timbl     639:                    PUTS( &line[1]);
1.1       timbl     640:                }
                    641:            } else {
                    642: 
                    643: /*     Normal lines are scanned for references to newsgroups.
                    644: */
2.16    ! luotonen  645:                int i=0;
        !           646: 
        !           647:                /* find whitespace if it exits */
        !           648:                for(; line[i] != '\0' && !WHITE(line[i]); i++)
        !           649:                    ;  /* null body */
        !           650:        
        !           651:                if(line[i] != '\0') {
        !           652:                    line[i] = '\0';
        !           653:                    write_anchor(line, line);
        !           654:                    (*targetClass.start_element)(target, HTML_DD , 0, 0);
        !           655:                    PUTS(&line[i+1]); /* put description */
        !           656:                } else {
        !           657:                    write_anchor(line, line);
        !           658:                }
        !           659: 
        !           660: #ifdef OLD_CODE
1.1       timbl     661:                char group[LINE_LENGTH];
                    662:                int first, last;
                    663:                char postable;
                    664:                if (sscanf(line, "%s %d %d %c", group, &first, &last, &postable)==4)
                    665:                    write_anchor(line, group);
                    666:                else
1.2       timbl     667:                    PUTS(line);
2.16    ! luotonen  668: #endif /*OLD_CODE*/
        !           669: 
1.1       timbl     670:            } /* if not dot */
                    671:            p = line;                   /* Restart at beginning */
                    672:        } /* if end of line */
                    673:     } /* Loop over characters */
2.16    ! luotonen  674:     (*targetClass.end_element)(target, HTML_DL);
1.1       timbl     675: }
                    676: 
                    677: 
                    678: /*     Read in a Newsgroup
                    679: **     -------------------
                    680: **     Unfortunately, we have to ask for each article one by one if we
                    681: **     want more than one field.
                    682: **
                    683: */
                    684: PRIVATE void read_group ARGS3(
                    685:   CONST char *,groupName,
                    686:   int,first_required,
                    687:   int,last_required
                    688: )
                    689: {
                    690:     char line[LINE_LENGTH+1];
                    691:     char author[LINE_LENGTH+1];
                    692:     char subject[LINE_LENGTH+1];
                    693:     char *p;
                    694:     BOOL done;
                    695: 
                    696:     char buffer[LINE_LENGTH];
                    697:     char *reference=0;                 /* Href for article */
                    698:     int art;                           /* Article number WITHIN GROUP */
                    699:     int status, count, first, last;    /* Response fields */
                    700:                                        /* count is only an upper limit */
                    701: 
                    702:     sscanf(response_text, " %d %d %d %d", &status, &count, &first, &last);
2.16    ! luotonen  703:     if(TRACE) fprintf(fprintf,
        !           704:                 "Newsgroup status=%d, count=%d, (%d-%d) required:(%d-%d)\n",
        !           705:                 status, count, first, last, first_required, last_required);
1.1       timbl     706:     if (last==0) {
1.2       timbl     707:         PUTS( "\nNo articles in this group.\n");
2.16    ! luotonen  708: #ifdef POSTING
        !           709:        goto add_post;
        !           710: #endif
1.1       timbl     711:        return;
                    712:     }
                    713:     
                    714: #define FAST_THRESHOLD 100     /* Above this, read IDs fast */
                    715: #define CHOP_THRESHOLD 50      /* Above this, chop off the rest */
                    716: 
                    717:     if (first_required<first) first_required = first;          /* clip */
                    718:     if ((last_required==0) || (last_required > last)) last_required = last;
                    719:     
                    720:     if (last_required<=first_required) {
1.2       timbl     721:         PUTS( "\nNo articles in this range.\n");
2.16    ! luotonen  722: #ifdef POSTING
        !           723:        goto add_post;
        !           724: #endif
1.1       timbl     725:        return;
                    726:     }
                    727: 
                    728:     if (last_required-first_required+1 > MAX_CHUNK) {  /* Trim this block */
                    729:         first_required = last_required-CHUNK_SIZE+1;
                    730:     }
2.16    ! luotonen  731:     if (TRACE) printf (stderr, "    Chunk will be (%d-%d)\n",
        !           732:                       first_required, last_required);
1.1       timbl     733: 
1.2       timbl     734: /*     Set window title
                    735: */
                    736:     sprintf(buffer, "Newsgroup %s,  Articles %d-%d",
                    737:                groupName, first_required, last_required);
                    738:     START(HTML_TITLE);
                    739:     PUTS(buffer);
                    740:     END(HTML_TITLE);
                    741: 
1.1       timbl     742: /*     Link to earlier articles
                    743: */
                    744:     if (first_required>first) {
                    745:        int before;                     /* Start of one before */
                    746:        if (first_required-MAX_CHUNK <= first) before = first;
                    747:        else before = first_required-CHUNK_SIZE;
                    748:        sprintf(buffer, "%s/%d-%d", groupName, before, first_required-1);
                    749:        if (TRACE) fprintf(stderr, "    Block before is %s\n", buffer);
1.2       timbl     750:        PUTS( " (");
                    751:        start_anchor(buffer);
                    752:        PUTS("Earlier articles");
                    753:        END(HTML_A);
                    754:        PUTS( "...)\n");
1.1       timbl     755:     }
                    756:     
                    757:     done = NO;
                    758: 
                    759: /*#define USE_XHDR*/
                    760: #ifdef USE_XHDR
                    761:     if (count>FAST_THRESHOLD)  {
                    762:         sprintf(buffer,
                    763:        "\nThere are about %d articles currently available in %s, IDs as follows:\n\n",
                    764:                count, groupName); 
1.2       timbl     765:         PUTS(buffer);
1.3       timbl     766:         sprintf(buffer, "XHDR Message-ID %d-%d%c%c", first, last, CR, LF);
1.1       timbl     767:        status = response(buffer);
                    768:        if (status==221) {
                    769: 
                    770:            p = line;
                    771:            while(!done){
                    772:                char ch = *p++ = NEXT_CHAR;
                    773:                if (ch==(char)EOF) {
                    774:                    abort_socket();     /* End of file, close socket */
                    775:                    return;             /* End of file on response */
                    776:                }
                    777:                if ((ch == '\n') || (p == &line[LINE_LENGTH])) {
                    778:                    *p++=0;                             /* Terminate the string */
                    779:                    if (TRACE) fprintf(stderr, "X %s", line);
                    780:                    if (line[0]=='.') {
                    781:                        if (line[1]<' ') {              /* End of article? */
                    782:                            done = YES;
                    783:                            break;
                    784:                        } else {                        /* Line starts with dot */
                    785:                                /* Ignore strange line */
                    786:                        }
                    787:                    } else {
                    788:        
                    789:        /*      Normal lines are scanned for references to articles.
                    790:        */
                    791:                        char * space = strchr(line, ' ');
                    792:                        if (space++)
                    793:                            write_anchor(space, space);
                    794:                    } /* if not dot */
                    795:                    p = line;                   /* Restart at beginning */
                    796:                } /* if end of line */
                    797:            } /* Loop over characters */
                    798: 
                    799:            /* leaving loop with "done" set */
                    800:        } /* Good status */
                    801:     };
                    802: #endif
                    803: 
                    804: /*     Read newsgroup using individual fields:
                    805: */
                    806:     if (!done) {
                    807:         if (first==first_required && last==last_required)
1.2       timbl     808:                PUTS("\nAll available articles in ");
                    809:         else PUTS( "\nArticles in ");
                    810:        PUTS(groupName);
                    811:        START(HTML_MENU);
1.1       timbl     812:        for(art=first_required; art<=last_required; art++) {
                    813:     
                    814: /*#define OVERLAP*/
                    815: #ifdef OVERLAP
                    816: /* With this code we try to keep the server running flat out by queuing just
                    817: ** one extra command ahead of time. We assume (1) that the server won't abort
                    818: ** if it gets input during output, and (2) that TCP buffering is enough for the
                    819: ** two commands. Both these assumptions seem very reasonable. However, we HAVE
                    820: ** had a hangup with a loaded server.
                    821: */
                    822:            if (art==first_required) {
                    823:                if (art==last_required) {
1.3       timbl     824:                        sprintf(buffer, "HEAD %d%c%c", art, CR, LF);    /* Only one */
1.1       timbl     825:                        status = response(buffer);
                    826:                    } else {                                    /* First of many */
1.3       timbl     827:                        sprintf(buffer, "HEAD %d%c%cHEAD %d%c%c",
                    828:                                art, CR, LF, art+1, CR, LF);
1.1       timbl     829:                        status = response(buffer);
                    830:                    }
                    831:            } else if (art==last_required) {                    /* Last of many */
                    832:                    status = response(NULL);
                    833:            } else {                                            /* Middle of many */
1.3       timbl     834:                    sprintf(buffer, "HEAD %d%c%c", art+1, CR, LF);
1.1       timbl     835:                    status = response(buffer);
                    836:            }
                    837:            
                    838: #else  /* NOT OVERLAP */
1.3       timbl     839:            sprintf(buffer, "HEAD %d%c%c", art, CR, LF);
1.1       timbl     840:            status = response(buffer);
                    841: #endif /* NOT OVERLAP */
                    842: 
                    843:            if (status == 221) {        /* Head follows - parse it:*/
                    844:     
                    845:                p = line;                               /* Write pointer */
                    846:                done = NO;
                    847:                while(!done){
                    848:                    char ch = *p++ = NEXT_CHAR;
                    849:                    if (ch==(char)EOF) {
                    850:                        abort_socket(); /* End of file, close socket */
                    851:                        return;         /* End of file on response */
                    852:                    }
1.3       timbl     853:                    if ((ch == LF)
1.1       timbl     854:                        || (p == &line[LINE_LENGTH]) ) {
                    855:                    
                    856:                        *--p=0;         /* Terminate  & chop LF*/
                    857:                        p = line;               /* Restart at beginning */
                    858:                        if (TRACE) fprintf(stderr, "G %s\n", line);
                    859:                        switch(line[0]) {
                    860:     
                    861:                        case '.':
                    862:                            done = (line[1]<' ');       /* End of article? */
                    863:                            break;
                    864:     
                    865:                        case 'S':
                    866:                        case 's':
                    867:                            if (match(line, "SUBJECT:"))
                    868:                                strcpy(subject, line+9);/* Save subject */
                    869:                            break;
                    870:     
                    871:                        case 'M':
                    872:                        case 'm':
                    873:                            if (match(line, "MESSAGE-ID:")) {
                    874:                                char * addr = HTStrip(line+11) +1; /* Chop < */
                    875:                                addr[strlen(addr)-1]=0;         /* Chop > */
                    876:                                StrAllocCopy(reference, addr);
                    877:                            }
                    878:                            break;
                    879:     
                    880:                        case 'f':
                    881:                        case 'F':
                    882:                            if (match(line, "FROM:")) {
                    883:                                char * p;
                    884:                                strcpy(author,
                    885:                                        author_name(strchr(line,':')+1));
                    886:                                p = author + strlen(author) - 1;
1.3       timbl     887:                                if (*p==LF) *p = 0;     /* Chop off newline */
1.1       timbl     888:                            }
                    889:                            break;
                    890:                                    
                    891:                        } /* end switch on first character */
                    892:                    } /* if end of line */
                    893:                } /* Loop over characters */
                    894:     
1.2       timbl     895:                START(HTML_LI);
1.1       timbl     896:                sprintf(buffer, "\"%s\" - %s", subject, author);
                    897:                if (reference) {
                    898:                    write_anchor(buffer, reference);
                    899:                    free(reference);
                    900:                    reference=0;
                    901:                } else {
1.2       timbl     902:                    PUTS(buffer);
1.1       timbl     903:                }
                    904:                
                    905:     
1.2       timbl     906: /*      indicate progress!   @@@@@@
1.1       timbl     907: */
                    908:     
                    909:            } /* If good response */
                    910:        } /* Loop over article */           
                    911:     } /* If read headers */
1.2       timbl     912:     END(HTML_MENU);
                    913:     START(HTML_P);
1.1       timbl     914:     
                    915: /*     Link to later articles
                    916: */
                    917:     if (last_required<last) {
                    918:        int after;                      /* End of article after */
                    919:        after = last_required+CHUNK_SIZE;
                    920:        if (after==last) sprintf(buffer, "news:%s", groupName); /* original group */
                    921:        else sprintf(buffer, "news:%s/%d-%d", groupName, last_required+1, after);
                    922:        if (TRACE) fprintf(stderr, "    Block after is %s\n", buffer);
1.2       timbl     923:        PUTS( "(");
                    924:        start_anchor(buffer);
                    925:        PUTS( "Later articles");
                    926:        END(HTML_A);
                    927:        PUTS( "...)\n");
1.1       timbl     928:     }
2.16    ! luotonen  929: 
        !           930: add_post:
        !           931:     {
        !           932:        char *href=0;
        !           933:        START(HTML_HR);
        !           934:        
        !           935:        StrAllocCopy(href,"newspost:");
        !           936:        StrAllocCat(href,groupName);
        !           937:        start_anchor(href);
        !           938:        PUTS("Post to ");
        !           939:        PUTS(groupName);
        !           940:        END(HTML_A);
        !           941: 
        !           942:        free(href);
        !           943:     }
1.1       timbl     944:     
                    945: 
                    946: }
                    947: 
                    948: 
                    949: /*             Load by name                                    HTLoadNews
                    950: **             ============
                    951: */
2.13      timbl     952: PUBLIC int HTLoadNews ARGS1(HTRequest *,               request)
1.1       timbl     953: {
2.13      timbl     954:     CONST char * arg = HTAnchor_physical(request->anchor);
1.1       timbl     955:     char command[257];                 /* The whole command */
                    956:     char groupName[GROUP_NAME_LENGTH]; /* Just the group name */
                    957:     int status;                                /* tcp return */
                    958:     int retries;                       /* A count of how hard we have tried */ 
                    959:     BOOL group_wanted;                 /* Flag: group was asked for, not article */
                    960:     BOOL list_wanted;                  /* Flag: group was asked for, not article */
                    961:     int first, last;                   /* First and last articles asked for */
                    962: 
2.10      timbl     963:     diagnostic = (request->output_format == WWW_SOURCE);       /* set global flag */
1.1       timbl     964:     
                    965:     if (TRACE) fprintf(stderr, "HTNews: Looking for %s\n", arg);
                    966:     
                    967:     if (!initialized) initialized = initialize();
                    968:     if (!initialized) return -1;       /* FAIL */
                    969:     
                    970:     {
                    971:         CONST char * p1=arg;
                    972: 
                    973: /*     We will ask for the document, omitting the host name & anchor.
                    974: **
                    975: **     Syntax of address is
                    976: **             xxx@yyy                 Article
                    977: **             <xxx@yyy>               Same article
                    978: **             xxxxx                   News group (no "@")
                    979: **             group/n1-n2             Articles n1 to n2 in group
                    980: */        
                    981:        group_wanted = (strchr(arg, '@')==0) && (strchr(arg, '*')==0);
                    982:        list_wanted  = (strchr(arg, '@')==0) && (strchr(arg, '*')!=0);
                    983: 
                    984:        /* p1 = HTParse(arg, "", PARSE_PATH | PARSE_PUNCTUATION); */
                    985:        /* Don't use HTParse because news: access doesn't follow traditional
                    986:           rules. For instance, if the article reference contains a '#',
                    987:           the rest of it is lost -- JFG 10/7/92, from a bug report */
                    988:        if (!strncasecomp (arg, "news:", 5))
                    989:          p1 = arg + 5;  /* Skip "news:" prefix */
                    990:        if (list_wanted) {
2.16    ! luotonen  991:            strcpy(command, "LIST NEWSGROUPS");
1.1       timbl     992:        } else if (group_wanted) {
                    993:            char * slash = strchr(p1, '/');
                    994:            strcpy(command, "GROUP ");
                    995:            first = 0;
                    996:            last = 0;
                    997:            if (slash) {
                    998:                *slash = 0;
                    999:                strcpy(groupName, p1);
                   1000:                *slash = '/';
                   1001:                (void) sscanf(slash+1, "%d-%d", &first, &last);
                   1002:            } else {
                   1003:                strcpy(groupName, p1);
                   1004:            }
                   1005:            strcat(command, groupName);
                   1006:        } else {
                   1007:            strcpy(command, "ARTICLE ");
                   1008:            if (strchr(p1, '<')==0) strcat(command,"<");
                   1009:            strcat(command, p1);
                   1010:            if (strchr(p1, '>')==0) strcat(command,">");
                   1011:        }
                   1012: 
1.3       timbl    1013:         {
                   1014:            char * p = command + strlen(command);
                   1015:            *p++ = CR;          /* Macros to be correct on Mac */
                   1016:            *p++ = LF;
                   1017:            *p++ = 0;
                   1018:            /* strcat(command, "\r\n"); */      /* CR LF, as in rfc 977 */
                   1019:        }
1.1       timbl    1020:     } /* scope of p1 */
                   1021:     
                   1022:     if (!*arg) return NO;                      /* Ignore if no name */
                   1023: 
                   1024:     
                   1025: /*     Make a hypertext object with an anchor list.
                   1026: */       
2.10      timbl    1027:     node_anchor = request->anchor;
2.11      timbl    1028:     target = HTML_new(request, NULL, WWW_HTML,
                   1029:                request->output_format, request->output_stream);
1.2       timbl    1030:     targetClass = *target->isa;        /* Copy routine entry points */
                   1031:     
1.1       timbl    1032:        
                   1033: /*     Now, let's get a stream setup up from the NewsHost:
                   1034: */       
                   1035:     for(retries=0;retries<2; retries++){
                   1036:     
                   1037:         if (s<0) {
                   1038:             NEWS_PROGRESS("Connecting to NewsHost ...");
                   1039:            s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
                   1040:            status = connect(s, (struct sockaddr*)&soc_address, sizeof(soc_address));
                   1041:            if (status<0){
                   1042:                char message[256];
                   1043:                NETCLOSE(s);
                   1044:                s = -1;
                   1045:                if (TRACE) fprintf(stderr, "HTNews: Unable to connect to news host.\n");
                   1046: /*             if (retries<=1) continue;   WHY TRY AGAIN ?     */
                   1047:                sprintf(message,
                   1048: "\nCould not access %s.\n\n (Check default WorldWideWeb NewsHost ?)\n",
1.2       timbl    1049:                    HTNewsHost);
2.15      luotonen 1050:                return HTLoadError(request, 500, message);
1.1       timbl    1051:            } else {
                   1052:                if (TRACE) fprintf(stderr, "HTNews: Connected to news host %s.\n",
1.2       timbl    1053:                                HTNewsHost);
2.11      timbl    1054:                isoc = HTInputSocket_new(s);    /* set up buffering */
1.1       timbl    1055:                if ((response(NULL) / 100) !=2) {
2.8       timbl    1056:                        char message[BIG];
1.1       timbl    1057:                        NETCLOSE(s);
2.11      timbl    1058:                        HTInputSocket_free(isoc);
1.1       timbl    1059:                        s = -1;
2.8       timbl    1060:                        sprintf(message, 
                   1061:                  "Can't read news info. News host %.20s responded: %.200s",
                   1062:                            HTNewsHost, response_text);
2.15      luotonen 1063:                        return HTLoadError(request, 500, message);
1.1       timbl    1064:                }
                   1065:            }
                   1066:        } /* If needed opening */
                   1067:        
1.2       timbl    1068:        /* @@@@@@@@@@@@@@Tell user something's happening */
                   1069:        
1.1       timbl    1070:        status = response(command);
                   1071:        if (status<0) break;
                   1072:        if ((status/ 100) !=2) {
2.8       timbl    1073:            HTProgress(response_text);
1.1       timbl    1074: /*         NXRunAlertPanel("News access", response_text,
                   1075:                NULL,NULL,NULL);
                   1076: */
                   1077:            NETCLOSE(s);
2.11      timbl    1078:            HTInputSocket_free(isoc);
1.1       timbl    1079:            s = -1;
                   1080: /* return HT; -- no:the message might be "Timeout-disconnected" left over */
                   1081:            continue;   /*      Try again */
                   1082:        }
                   1083:   
                   1084: /*     Load a group, article, etc
                   1085: */
1.2       timbl    1086:         
1.1       timbl    1087:        
                   1088:        if (list_wanted) read_list();
                   1089:        else if (group_wanted) read_group(groupName, first, last);
                   1090:         else read_article();
                   1091: 
2.6       timbl    1092:        (*targetClass.free)(target);
1.2       timbl    1093:        return HT_LOADED;
1.1       timbl    1094:        
                   1095:     } /* Retry loop */
                   1096:     
1.2       timbl    1097:     
2.8       timbl    1098:     /* HTAlert("Sorry, could not load requested news.\n"); */
                   1099:         
1.1       timbl    1100: /*    NXRunAlertPanel(NULL, "Sorry, could not load `%s'.",
                   1101:            NULL,NULL,NULL, arg);No -- message earlier wil have covered it */
                   1102: 
1.2       timbl    1103:     return HT_LOADED;
1.1       timbl    1104: }
                   1105: 
2.10      timbl    1106: GLOBALDEF PUBLIC HTProtocol HTNews = { "news", HTLoadNews, NULL, NULL};

Webmaster