1 /* Copyright (C) 2000-2015 Lavtech.com corp. All rights reserved.
2 
3    This program is free software; you can redistribute it and/or modify
4    it under the terms of the GNU General Public License as published by
5    the Free Software Foundation; either version 2 of the License, or
6    (at your option) any later version.
7 
8    This program is distributed in the hope that it will be useful,
9    but WITHOUT ANY WARRANTY; without even the implied warranty of
10    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11    GNU General Public License for more details.
12 
13    You should have received a copy of the GNU General Public License
14    along with this program; if not, write to the Free Software
15    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16 */
17 
18 #include "udm_config.h"
19 
20 #ifdef HAVE_SQL
21 
22 #include <stdio.h>
23 #include <stdlib.h>
24 #include <string.h>
25 #include <sys/types.h>
26 #include <sys/stat.h>
27 #include <fcntl.h>
28 #include <errno.h>
29 #include <ctype.h>
30 #include <time.h>
31 
32 #ifdef WIN32
33 #include <time.h>
34 #endif
35 
36 #ifdef HAVE_UNISTD_H
37 #include <unistd.h>
38 #endif
39 
40 #ifdef HAVE_SYS_TIME_H
41 #include <sys/time.h>
42 #endif
43 
44 #include "udm_common.h"
45 #include "udm_utils.h"
46 #include "udm_db.h"
47 #include "udm_url.h"
48 #include "udm_log.h"
49 #include "udm_proto.h"
50 #include "udm_vars.h"
51 #include "udm_hrefs.h"
52 #include "udm_db_int.h"
53 #include "udm_indexer.h"
54 #include "udm_textlist.h"
55 #include "udm_parsehtml.h"
56 #include "udm_parsexml.h"
57 #include "udm_http.h"
58 
59 /***********************************************************/
60 /*  HTDB stuff:  Indexing of database content              */
61 /***********************************************************/
62 
63 #define MAXNPART 32
64 
65 
66 static udm_rc_t
UdmDSTRParseUsingConstStringList(UDM_DSTR * dstr,const char * src,const char * const * part,size_t nparts)67 UdmDSTRParseUsingConstStringList(UDM_DSTR *dstr, const char *src,
68                                  const char * const *part, size_t nparts)
69 {
70   UdmDSTRReset(dstr);
71   for ( ; *src; )
72   {
73     if (*src == '\\')
74     {
75       UdmDSTRAppend(dstr, src + 1, 1);
76       src+= 2;
77       continue;
78     }
79     if (*src == '$')
80     {
81       int i= atoi(++src)- 1;
82 
83       while(*src >= '0' && *src<= '9')
84         src++;
85       if (i >= 0 && i < (int) nparts)
86       {
87         size_t len= strlen(part[i]);
88         if (UDM_OK != UdmDSTRAppendURLDecode(dstr, part[i], len))
89           return UDM_ERROR;
90       }
91       continue;
92     }
93     UdmDSTRAppend(dstr, src++, 1);
94   }
95   return UDM_OK;
96 }
97 
98 
99 static udm_rc_t
include_params(UDM_DB * db,const char * src,char * path,UDM_DSTR * dstr,size_t start,int limit)100 include_params(UDM_DB *db, const char *src, char *path,
101                UDM_DSTR *dstr, size_t start, int limit)
102 {
103   size_t nparts;
104   const char *part[MAXNPART];
105   char *lt;
106 
107   for (part[nparts= 0]= udm_strtok_r(path, "/", &lt) ;
108        part[nparts] && nparts < MAXNPART;
109        part[++nparts]= udm_strtok_r(NULL, "/", &lt))
110   {
111   }
112 
113   if (UDM_OK != UdmDSTRParseUsingConstStringList(dstr, src, part, nparts))
114     return UDM_ERROR;
115 
116   if (limit)
117   {
118     switch (UdmSQLDBType(db))
119     {
120       case UDM_DB_MYSQL:
121         UdmDSTRAppendf(dstr, " LIMIT %d,%d", (int) start, limit);
122         break;
123       case UDM_DB_PGSQL:
124       default:
125         UdmDSTRAppendf(dstr, " LIMIT %d OFFSET %d", limit, (int) start);
126         break;
127     }
128   }
129   return UDM_OK;
130 }
131 
132 
133 static udm_rc_t
UdmHTDBCreateHTTPResponse(UDM_AGENT * Indexer,UDM_DOCUMENT * Doc,UDM_SQLRES * SQLres)134 UdmHTDBCreateHTTPResponse(UDM_AGENT *Indexer,
135                           UDM_DOCUMENT *Doc,
136                           UDM_SQLRES *SQLres)
137 {
138   size_t i;
139   for (i= 0; i < UdmSQLNumCols(SQLres); i++)
140   {
141     size_t len;
142     const char *from;
143     if (i > 0)
144     {
145       UdmHTTPBufAppend(&Doc->Buf, "\r\n", 2);
146     }
147     len= UdmSQLLen(SQLres, 0, i);
148     from= UdmSQLValue(SQLres, 0, i);
149     if (len == 1 && *from == ' ')
150     {
151       /*
152          Sybase+unixODBC returns space character instead
153          of an empty string executing this query:
154            SELECT '' FROM t1;
155       */
156     }
157     else
158     {
159       UdmHTTPBufAppend(&Doc->Buf, from, len);
160     }
161   }
162   return UDM_OK;
163 }
164 
165 
166 static void
UdmRemoveWiki(char * str,char * strend)167 UdmRemoveWiki(char *str, char *strend)
168 {
169   for ( ; str < strend ; str++)
170   {
171     if (*str == '[')
172     {
173       int smcount= 0;
174       for (*str++= ' ' ; str < strend ; str++)
175       {
176         if (*str == ']')
177         {
178           *str++= ' ';
179           break;
180         }
181         if (*str == '[')
182           UdmRemoveWiki(str, strend);
183         if (*str == ':')
184         {
185           *str= ' ';
186           smcount++;
187         }
188         if (smcount < 2)
189           *str= ' ';
190       }
191     }
192   }
193 }
194 
195 
196 typedef struct udm_htdb_html_helper_st
197 {
198   UDM_DOCUMENT *Doc;
199   UDM_CONST_TEXTITEM ConstItem;
200   UDM_TEXT_PARAM TextParam;
201   const UDM_VAR *Sec;
202   UDM_DSTR tbuf;
203 } UDM_HTDB_HTML_HELPER;
204 
205 
206 static udm_rc_t
UdmHTDBProcessHTMLText(UDM_HTML_PARSER * parser)207 UdmHTDBProcessHTMLText(UDM_HTML_PARSER *parser)
208 {
209   UDM_HTDB_HTML_HELPER *param= (UDM_HTDB_HTML_HELPER *) parser->user_data;
210 
211   if (!parser->state.script && !parser->state.comment && !parser->state.style)
212   {
213     UdmDSTRReset(&param->tbuf);
214     if (UdmVarFlags(param->Sec) & UDM_VARFLAG_WIKI)
215       UdmRemoveWiki((char*) parser->lasttok.str, (char*) parser->lasttok.str + parser->lasttok.length);
216     UdmDSTRAppend(&param->tbuf, parser->lasttok.str, parser->lasttok.length);
217     param->ConstItem.text.str= UdmDSTRPtr(&param->tbuf);
218     param->ConstItem.text.length= UdmDSTRPtr(&param->tbuf) ? strlen(UdmDSTRPtr(&param->tbuf)) : 0;
219     param->TextParam.secno= UdmVarSecno(param->Sec);
220     param->ConstItem.section_name.str= UdmVarName(param->Sec);
221     param->ConstItem.section_name.length= UdmVarNameLength(param->Sec);
222     UdmTextListAddConst(&param->Doc->TextList, &param->ConstItem, &param->TextParam);
223   }
224   return UDM_OK;
225 }
226 
227 
228 static udm_rc_t
UdmHTDBProcessHTML(UDM_HTDB_HTML_HELPER * param,const char * src,size_t srclen)229 UdmHTDBProcessHTML(UDM_HTDB_HTML_HELPER *param, const char *src, size_t srclen)
230 {
231   UDM_HTML_PARSER parser;
232   UdmHTMLParserInit(&parser);
233   UdmHTMLParserSetUserData(&parser, param);
234   UdmHTMLParserSetTextHandler(&parser, UdmHTDBProcessHTMLText);
235   return UdmHTMLParserExec(&parser, src, srclen);
236 }
237 
238 
239 static udm_rc_t
UdmHTDBProcessNonHTTPResponse(UDM_AGENT * A,UDM_DOCUMENT * Doc,UDM_SQLRES * SQLRes)240 UdmHTDBProcessNonHTTPResponse(UDM_AGENT *A,
241                               UDM_DOCUMENT *Doc,
242                               UDM_SQLRES *SQLRes)
243 {
244   UDM_HTDB_HTML_HELPER param;
245   udm_rc_t rc= UDM_OK;
246   int status= 200;
247   size_t row, nrows, ncols= UdmSQLNumCols(SQLRes), length= 0, i;
248   char dbuf[UDM_MAXTIMESTRLEN]= "";
249 
250   /* Init user data */
251   bzero((void*) &param.ConstItem, sizeof(UDM_CONST_TEXTITEM));
252   param.Doc= Doc;
253   UdmDSTRInit(&param.tbuf, 1024);
254 
255   for (row=0, nrows= UdmSQLNumRows(SQLRes); row < nrows; row++)
256   {
257     size_t col;
258     for (col= 0; col  < ncols; col++)
259     {
260       const char *sqlname= SQLRes->Fields[col].sqlname;
261       const char *sqlval= UdmSQLValue(SQLRes, row, col);
262       size_t sqlval_length= UdmSQLLen(SQLRes, row, col);
263       if ((param.Sec= UdmVarListFind(&param.Doc->Sections, sqlname)))
264       {
265         param.ConstItem.section_name.str= UdmVarName(param.Sec);
266         param.ConstItem.section_name.length= UdmVarNameLength(param.Sec);
267         if (UdmVarFlags(param.Sec) & UDM_VARFLAG_HTMLSOURCE)
268         {
269           /* TODO34: add directly to Doc->Buf ? */
270           UdmHTDBProcessHTML(&param, sqlval, sqlval_length);
271         }
272         else
273         {
274           param.ConstItem.text.str= sqlval;
275           param.ConstItem.text.length= sqlval_length;
276           param.TextParam.secno= UdmVarSecno(param.Sec);
277           /* TODO34: add directly to Doc->Buf ? */
278           UdmTextListAddConst(&param.Doc->TextList, &param.ConstItem, &param.TextParam);
279         }
280         length+= UdmSQLLen(SQLRes, row, col);
281       }
282       if (!strcasecmp(sqlname, "status"))
283         status= atoi(sqlval);
284       else if (!strcasecmp(sqlname, "last_mod_time"))
285       {
286         int last_mod_time= atoi(sqlval);
287         strcpy(dbuf, "Last-Modified: ");
288         UdmTime_t2HttpStr(last_mod_time, dbuf + 15, sizeof(dbuf) - 15);
289       }
290     }
291   }
292 
293   /* Free user data */
294   UdmDSTRFree(&param.tbuf);
295   UdmHTTPBufPrintf(&param.Doc->Buf,
296                    "HTTP/1.0 %d %s\r\n"
297                    /*"HTDB-Content-Length: %d\r\n"*/
298                    "Content-Type: mnogosearch/htdb; charset=%s\r\n%s%s\r\n",
299                    status, UdmHTTPErrMsg(status),
300                    /*(int) length,*/
301                    A->Conf->lcs->name,
302                    dbuf[0] ? dbuf : "", dbuf[0] ? "\r\n" : "");
303 
304 
305   UdmHTTPBufAppendf(&param.Doc->Buf, "<doc>\n");
306   for (i= 0; i < param.Doc->TextList.nitems; i++)
307   {
308     UDM_TEXTITEM *Item= &param.Doc->TextList.Item[i];
309     UdmHTTPBufAppendf(&param.Doc->Buf,
310                       "<sec name=\"%s\"><![CDATA[%s]]></sec>\n",
311                       Item->section_name, Item->str);
312   }
313   UdmHTTPBufAppendf(&param.Doc->Buf, "</doc>\n");
314   UdmTextListFree(&param.Doc->TextList);
315   return rc;
316 }
317 
318 
319 static udm_rc_t
UdmHTDBGetDocument(UDM_AGENT * Indexer,UDM_DOCUMENT * Doc,UDM_DB * db,const UDM_URL * realURL)320 UdmHTDBGetDocument(UDM_AGENT *Indexer,
321                    UDM_DOCUMENT *Doc,
322                    UDM_DB *db,
323                    const UDM_URL *realURL)
324 {
325   udm_rc_t rc= UDM_OK;
326   const char  *htdbdoc= UdmVarListFindStr(&Doc->Sections, "HTDBDoc", "");
327   char real_path[1024]= "";
328   UDM_SQLRES SQLres;
329   UDM_DSTR qbuf;
330 
331   UdmDSTRInit(&qbuf, 128);
332   udm_snprintf(real_path, sizeof(real_path) - 1, "%s%s",
333                realURL->path, realURL->filename);
334   include_params(db, htdbdoc, real_path, &qbuf, 0, 0);
335   UdmLog(Indexer, UDM_LOG_DEBUG, "HTDBDoc: %s", UdmDSTRPtr(&qbuf));
336   if (UDM_OK != (rc= UdmDBSQLQuery(Indexer, db, &SQLres, UdmDSTRPtr(&qbuf))))
337     goto ret;
338 
339   if (UdmSQLNumRows(&SQLres) == 1)
340   {
341     /*
342       Can't use strncmp() in combination with UDM_CSTR_WITH_LEN.
343       It fails with -O2 on Linux, because strncmp() is defined
344       as an optimized macros in /usr/include/bits/string2.h
345     */
346     if (!strncmp(UdmSQLValue(&SQLres, 0, 0), "HTTP/", 5))
347       UdmHTDBCreateHTTPResponse(Indexer, Doc, &SQLres);
348     else
349       UdmHTDBProcessNonHTTPResponse(Indexer, Doc, &SQLres);
350   }
351   else
352   {
353     UdmHTTPBufAppendf(&Doc->Buf, "HTTP/1.0 404 Not Found\r\n\r\n");
354   }
355   UdmSQLFree(&SQLres);
356 ret:
357   UdmDSTRFree(&qbuf);
358   return rc;
359 }
360 
361 
362 static udm_rc_t
UdmHTDBGetDirectoryList(UDM_AGENT * Indexer,UDM_DOCUMENT * Doc,UDM_DB * db,const UDM_URL * realURL)363 UdmHTDBGetDirectoryList(UDM_AGENT *Indexer,
364                         UDM_DOCUMENT *Doc,
365                         UDM_DB *db,
366                         const UDM_URL *realURL)
367 {
368   udm_rc_t rc= UDM_OK;
369   size_t  i, start;
370   urlid_t  url_id= UdmVarListFindInt(&Doc->Sections, "ID", 0);
371   const char *htdblist= UdmVarListFindStr(&Doc->Sections,"HTDBList","");
372   int htdblimit= UdmVarListFindInt(&Doc->Sections, "HTDBLimit", 0);
373   udm_bool_t usehtdburlid = UdmVarListFindBool(&Indexer->Conf->Vars, "UseHTDBURLId", UDM_FALSE);
374   int done, hops= UdmVarListFindInt(&Doc->Sections,"Hops",0);
375   UDM_DSTR qbuf;
376 
377   UdmDSTRInit(&qbuf, 128);
378   UdmHTTPBufAppendf(&Doc->Buf,
379                     "HTTP/1.0 200 OK\r\nContent-type: text/html\r\n\r\n"
380                     "<HTML><BODY>\n"
381                     "</BODY></HTML>\n");
382 
383   for (start=0, done= 0; !done; )
384   {
385     size_t nrows;
386     char real_path[1024]="";
387     UDM_SQLRES SQLres;
388 
389     udm_snprintf(real_path, sizeof(real_path), "%s", realURL->path);
390     include_params(db, htdblist, real_path, &qbuf, start, htdblimit);
391     UdmLog(Indexer, UDM_LOG_DEBUG, "HTDBList: %s", UdmDSTRPtr(&qbuf));
392     if (UDM_OK != (rc= UdmDBSQLQuery(Indexer, db, &SQLres, UdmDSTRPtr(&qbuf))))
393       goto ret;
394 
395     nrows= UdmSQLNumRows(&SQLres);
396     done= htdblimit ? (htdblimit != (int) nrows) : 1;
397     start+= nrows;
398 
399     for (i= 0; i < nrows; i++)
400     {
401       UDM_HREFPARAM HrefParam;
402       UDM_CONST_STR url;
403       UdmSQLValueToConstStr(&url, &SQLres, i, 0);
404       UdmHrefParamInit(&HrefParam);
405       HrefParam.referrer= url_id;
406       HrefParam.hops= hops+1;
407       HrefParam.rec_id= usehtdburlid ? atoi(url.str) : 0;
408       HrefParam.link_source= UDM_LINK_SOURCE_HTDB;
409       UdmHrefListAddConstStr(&Doc->Hrefs, &HrefParam, &url);
410     }
411     UdmSQLFree(&SQLres);
412     UdmDocStoreHrefs(Indexer, Doc);
413     UdmHrefListFree(&Doc->Hrefs);
414     UdmStoreHrefs(Indexer);
415   }
416 ret:
417   UdmDSTRFree(&qbuf);
418   return rc;
419 }
420 
421 
422 udm_rc_t
UdmHTDBGet(UDM_AGENT * Indexer,UDM_DOCUMENT * Doc)423 UdmHTDBGet(UDM_AGENT *Indexer,UDM_DOCUMENT *Doc)
424 {
425   UDM_URL     realURL;
426   UDM_DB      dbnew, *db= NULL;
427   const char  *url=UdmVarListFindStr(&Doc->Sections,"URL","");
428   const char  *htdbaddr = UdmVarListFindStr(&Doc->Sections, "HTDBAddr", NULL);
429   udm_rc_t rc= UDM_OK;
430 
431   UdmHTTPBufReset(&Doc->Buf);
432   UdmURLInit(&realURL);
433   UdmURLParse(&realURL, url);
434 
435   if (htdbaddr)
436   {
437     UdmDBInit(&dbnew);
438     db= &dbnew;
439     if (UDM_OK != (rc= UdmDBSetAddr(db, htdbaddr)))
440     {
441       if (rc == UDM_NOTARGET)
442       {
443         UdmLog(Indexer, UDM_LOG_ERROR, "Unsupported DBAddr");
444         rc= UDM_ERROR;
445       }
446       else
447       {
448         UdmLog(Indexer, UDM_LOG_ERROR, "UdmDBSetAddr failed");
449         rc= UDM_ERROR;
450       }
451       goto HTDBexit;
452     }
453   }
454   else
455   {
456     if (Indexer->Conf->DBList.nitems != 1)
457     {
458       UdmLog(Indexer, UDM_LOG_ERROR, "HTDB cannot work with multiple DBAddr without HTDBAddr");
459       rc= UDM_ERROR;
460       goto HTDBexit;
461     }
462     db= &Indexer->Conf->DBList.Item[0];
463   }
464 
465   rc= realURL.filename != NULL ?
466       UdmHTDBGetDocument(Indexer, Doc, db, &realURL) :
467       UdmHTDBGetDirectoryList(Indexer, Doc, db, &realURL);
468 
469 HTDBexit:
470   if (db == &dbnew)
471     UdmDBFree(db);
472   UdmURLFree(&realURL);
473   return rc;
474 }
475 
476 
477 typedef struct
478 {
479   UDM_DOCUMENT *Doc;
480   UDM_CONST_STR secname;
481 } XML_PARSER_DATA;
482 
483 
484 static udm_rc_t
startElement(UDM_XML_PARSER * parser,const char * str,size_t len)485 startElement(UDM_XML_PARSER *parser, const char *str, size_t len)
486 {
487   return UDM_OK;
488 }
489 
490 
491 static udm_rc_t
endElement(UDM_XML_PARSER * parser,const char * str,size_t len)492 endElement(UDM_XML_PARSER *parser, const char *str, size_t len)
493 {
494   XML_PARSER_DATA *D= (XML_PARSER_DATA*) parser->user_data;
495   if (!udm_strnncasecmp(str, len, UDM_CSTR_WITH_LEN("/doc/sec")))
496     UdmConstStrSet(&D->secname, NULL, 0);
497   return UDM_OK;
498 }
499 
500 
501 static udm_rc_t
Text(UDM_XML_PARSER * parser,const char * str,size_t len)502 Text(UDM_XML_PARSER *parser, const char *str, size_t len)
503 {
504   XML_PARSER_DATA *D= (XML_PARSER_DATA*) parser->user_data;
505   if (!strcasecmp(parser->attr, "/doc/sec@name"))
506   {
507     UdmConstStrSet(&D->secname, str, len);
508   }
509   else if (!strcasecmp(parser->attr, "/doc/sec"))
510   {
511     if (D->secname.length)
512     {
513       char secname[128];
514       UDM_CONST_TEXTITEM ConstItem;
515       const UDM_VAR *Sec;
516       UdmConstTextItemInit(&ConstItem);
517       udm_snprintf(secname, sizeof(secname), "%.*s",
518                    (int) D->secname.length, D->secname.str);
519       if ((Sec= UdmVarListFind(&D->Doc->Sections, secname)))
520       {
521         UDM_TEXT_PARAM TextParam;
522         ConstItem.section_name.str= secname;
523         ConstItem.section_name.length= strlen(secname);
524         ConstItem.text.str= str;
525         ConstItem.text.length= len;
526         UdmTextParamInit(&TextParam, UDM_TEXTLIST_FLAG_NONE, UdmVarSecno(Sec));
527         UdmTextListAddConst(&D->Doc->TextList, &ConstItem, &TextParam);
528       }
529       /*
530       printf("Sec[%d] '%.*s'='%.*s'\n",
531              Sec ? Sec->section : 0,
532              (int) D->secname.length, D->secname.str,
533              (int) len, str);
534       */
535     }
536   }
537   return UDM_OK;
538 }
539 
540 
541 static udm_rc_t
UdmHTDBParseInternal(UDM_AGENT * Indexer,UDM_DOCUMENT * Doc,const UDM_CONST_STR * content)542 UdmHTDBParseInternal(UDM_AGENT *Indexer, UDM_DOCUMENT *Doc,
543                      const UDM_CONST_STR *content)
544 {
545   udm_rc_t rc= UDM_OK;
546   XML_PARSER_DATA Data;
547   UDM_XML_PARSER parser;
548 
549   UdmXMLParserCreate(&parser);
550   bzero(&Data, sizeof(Data));
551   Data.Doc= Doc;
552 
553   UdmXMLSetUserData(&parser, &Data);
554   UdmXMLSetEnterHandler(&parser, startElement);
555   UdmXMLSetLeaveHandler(&parser, endElement);
556   UdmXMLSetValueHandler(&parser, Text);
557 
558   if (UDM_OK!= (rc= UdmXMLParserExec(&parser, content->str, content->length)))
559   {
560     char err[256];
561     udm_snprintf(err, sizeof(err),
562                  "XML parsing error: %s at line %d pos %d",
563                   UdmXMLErrorString(&parser),
564                   (int) UdmXMLErrorLineno(&parser),
565                   (int) UdmXMLErrorPos(&parser));
566     UdmVarListReplaceStr(&Doc->Sections, "X-Reason", err);
567   }
568 
569   UdmXMLParserFree(&parser);
570   return rc;
571 }
572 
573 
574 udm_rc_t
UdmHTDBParse(UDM_AGENT * Indexer,UDM_DOCUMENT * Doc)575 UdmHTDBParse(UDM_AGENT *Indexer, UDM_DOCUMENT *Doc)
576 {
577   UDM_CONST_STR content;
578 
579   if (UdmHTTPBufContentToConstStr(&Doc->Buf, &content))
580     return UDM_ERROR;
581 
582   return UdmHTDBParseInternal(Indexer, Doc, &content);
583 }
584 
585 
586 size_t
UdmHTDBExcerptSource(UDM_AGENT * Agent,UDM_QUERY * Query,UDM_DOCUMENT * Doc,const UDM_CONST_STR * content,UDM_DSTR * dstr)587 UdmHTDBExcerptSource(UDM_AGENT *Agent,
588                      UDM_QUERY *Query,
589                      UDM_DOCUMENT *Doc,
590                      const UDM_CONST_STR *content,
591                      UDM_DSTR *dstr)
592 {
593   size_t i;
594 
595   UdmVarListAddStr(&Doc->Sections, "body", "");
596 
597   if (UDM_OK != UdmHTDBParseInternal(Agent, Doc, content))
598     return 0;
599 
600   /* Collect body chunks from Doc->TextList into dstr */
601   for (i= 0; i < Doc->TextList.nitems; i++)
602   {
603     UDM_TEXTITEM *Item= &Doc->TextList.Item[i];
604     if (!strcmp(Item->section_name, "body"))
605     {
606       if (UdmDSTRLength(dstr))
607         UdmDSTRAppend(dstr, " ", 1);
608       UdmDSTRAppend(dstr, Item->str, strlen(Item->str));
609     }
610   }
611   return UdmDSTRLength(dstr);
612 }
613 
614 
615 #endif /* HAVE_SQL */
616