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, "/", <) ;
108 part[nparts] && nparts < MAXNPART;
109 part[++nparts]= udm_strtok_r(NULL, "/", <))
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(¶m->tbuf);
214 if (UdmVarFlags(param->Sec) & UDM_VARFLAG_WIKI)
215 UdmRemoveWiki((char*) parser->lasttok.str, (char*) parser->lasttok.str + parser->lasttok.length);
216 UdmDSTRAppend(¶m->tbuf, parser->lasttok.str, parser->lasttok.length);
217 param->ConstItem.text.str= UdmDSTRPtr(¶m->tbuf);
218 param->ConstItem.text.length= UdmDSTRPtr(¶m->tbuf) ? strlen(UdmDSTRPtr(¶m->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(¶m->Doc->TextList, ¶m->ConstItem, ¶m->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*) ¶m.ConstItem, sizeof(UDM_CONST_TEXTITEM));
252 param.Doc= Doc;
253 UdmDSTRInit(¶m.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(¶m.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(¶m, 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(¶m.Doc->TextList, ¶m.ConstItem, ¶m.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(¶m.tbuf);
295 UdmHTTPBufPrintf(¶m.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(¶m.Doc->Buf, "<doc>\n");
306 for (i= 0; i < param.Doc->TextList.nitems; i++)
307 {
308 UDM_TEXTITEM *Item= ¶m.Doc->TextList.Item[i];
309 UdmHTTPBufAppendf(¶m.Doc->Buf,
310 "<sec name=\"%s\"><![CDATA[%s]]></sec>\n",
311 Item->section_name, Item->str);
312 }
313 UdmHTTPBufAppendf(¶m.Doc->Buf, "</doc>\n");
314 UdmTextListFree(¶m.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