Annotation of libwww/Library/src/HTParse.c, revision 2.44
2.41 frystyk 1: /* HTParse.c
2: ** URI MANAGEMENT
3: **
4: ** (c) COPYRIGHT CERN 1994.
5: ** Please first read the full copyright statement in the file COPYRIGH.
2.26 frystyk 6: **
7: ** history:
8: ** May 12 94 TAB added as legal char in HTCleanTelnetString
9: **
1.1 timbl 10: */
2.31 frystyk 11:
2.44 ! frystyk 12: /* Library include files */
! 13: #include "tcp.h"
1.1 timbl 14: #include "HTUtils.h"
2.38 frystyk 15: #include "HTParse.h"
2.44 ! frystyk 16: #include "HTString.h"
2.31 frystyk 17: #include "HTTCP.h"
2.6 timbl 18:
1.1 timbl 19: struct struct_parts {
2.20 timbl 20: char * access; /* Now known as "scheme" */
1.1 timbl 21: char * host;
22: char * absolute;
23: char * relative;
24: /* char * search; no - treated as part of path */
25: char * anchor;
26: };
27:
28: /* Strip white space off a string
29: ** ------------------------------
30: **
31: ** On exit,
32: ** Return value points to first non-white character, or to 0 if none.
33: ** All trailing white space is OVERWRITTEN with zero.
34: */
35:
2.13 luotonen 36: PUBLIC char * HTStrip ARGS1(char *, s)
1.1 timbl 37: {
38: #define SPACE(c) ((c==' ')||(c=='\t')||(c=='\n'))
39: char * p=s;
2.13 luotonen 40: if (!s) return NULL; /* Doesn't dump core if NULL */
41: for(p=s;*p;p++); /* Find end of string */
1.1 timbl 42: for(p--;p>=s;p--) {
43: if(SPACE(*p)) *p=0; /* Zap trailing blanks */
44: else break;
45: }
46: while(SPACE(*s))s++; /* Strip leading blanks */
47: return s;
48: }
49:
50:
51: /* Scan a filename for its consituents
52: ** -----------------------------------
53: **
54: ** On entry,
55: ** name points to a document name which may be incomplete.
56: ** On exit,
57: ** absolute or relative may be nonzero (but not both).
58: ** host, anchor and access may be nonzero if they were specified.
59: ** Any which are nonzero point to zero terminated strings.
60: */
2.32 frystyk 61: PRIVATE void scan ARGS2(char *, name, struct struct_parts *, parts)
1.1 timbl 62: {
63: char * after_access;
64: char * p;
65: int length = strlen(name);
66:
67: parts->access = 0;
68: parts->host = 0;
69: parts->absolute = 0;
70: parts->relative = 0;
71: parts->anchor = 0;
72:
73: after_access = name;
74: for(p=name; *p; p++) {
75: if (*p==':') {
76: *p = 0;
2.20 timbl 77: parts->access = after_access; /* Scheme has been specified */
2.37 howcome 78:
2.44 ! frystyk 79: /* The combination of gcc, the "-O" flag and the HP platform is
! 80: unhealthy. The following three lines is a quick & dirty fix, but is
! 81: not recommended. Rather, turn off "-O". */
! 82:
2.42 howcome 83: /* after_access = p;*/
2.44 ! frystyk 84: /* while (*after_access == 0)*/
! 85: /* after_access++;*/
2.42 howcome 86:
1.1 timbl 87: after_access = p+1;
2.37 howcome 88:
2.22 luotonen 89: if (0==strcasecomp("URL", parts->access)) {
2.20 timbl 90: parts->access = NULL; /* Ignore IETF's URL: pre-prefix */
91: } else break;
1.1 timbl 92: }
2.20 timbl 93: if (*p=='/') break; /* Access has not been specified */
1.1 timbl 94: if (*p=='#') break;
95: }
96:
97: for(p=name+length-1; p>=name; p--) {
98: if (*p =='#') {
99: parts->anchor=p+1;
100: *p=0; /* terminate the rest */
101: }
102: }
103: p = after_access;
104: if (*p=='/'){
105: if (p[1]=='/') {
106: parts->host = p+2; /* host has been specified */
107: *p=0; /* Terminate access */
108: p=strchr(parts->host,'/'); /* look for end of host name if any */
109: if(p) {
110: *p=0; /* Terminate host */
111: parts->absolute = p+1; /* Root has been found */
112: }
113: } else {
114: parts->absolute = p+1; /* Root found but no host */
115: }
116: } else {
117: parts->relative = (*after_access) ? after_access : 0; /* zero for "" */
118: }
119:
2.16 timbl 120: #ifdef OLD_CODE
1.1 timbl 121: /* Access specified but no host: the anchor was not really one
2.16 timbl 122: e.g. news:j462#36487@foo.bar -- JFG 10/jul/92, from bug report */
123: /* This kludge doesn't work for example when coming across
124: file:/usr/local/www/fred#123
125: which loses its anchor. Correct approach in news is to
126: escape weird characters not allowed in URL. TBL 21/dec/93
127: */
1.1 timbl 128: if (parts->access && ! parts->host && parts->anchor) {
129: *(parts->anchor - 1) = '#'; /* Restore the '#' in the address */
130: parts->anchor = 0;
131: }
2.16 timbl 132: #endif
1.1 timbl 133:
134: #ifdef NOT_DEFINED /* search is just treated as part of path */
135: {
136: char *p = relative ? relative : absolute;
137: if (p) {
138: char * q = strchr(p, '?'); /* Any search string? */
139: if (q) {
140: *q = 0; /* If so, chop that off. */
141: parts->search = q+1;
142: }
143: }
144: }
145: #endif
146: } /*scan */
147:
148:
149: /* Parse a Name relative to another name
150: ** -------------------------------------
151: **
152: ** This returns those parts of a name which are given (and requested)
153: ** substituting bits from the related name where necessary.
154: **
155: ** On entry,
156: ** aName A filename given
2.33 howcome 157: ** relatedName A name relative to which aName is to be parsed. Give
158: ** it an empty string if aName is absolute.
1.1 timbl 159: ** wanted A mask for the bits which are wanted.
160: **
161: ** On exit,
162: ** returns A pointer to a malloc'd string which MUST BE FREED
163: */
2.32 frystyk 164: char * HTParse ARGS3(CONST char *, aName, CONST char *, relatedName,
165: int, wanted)
1.1 timbl 166: {
167: char * result = 0;
168: char * return_value = 0;
169: int len;
170: char * name = 0;
171: char * rel = 0;
172: char * p;
2.12 timbl 173: char * access;
1.1 timbl 174: struct struct_parts given, related;
2.33 howcome 175:
176: if (!relatedName) /* HWL 23/8/94: dont dump due to NULL */
177: relatedName = "";
1.1 timbl 178:
179: /* Make working copies of input strings to cut up:
180: */
181: len = strlen(aName)+strlen(relatedName)+10;
182: result=(char *)malloc(len); /* Lots of space: more than enough */
183: if (result == NULL) outofmem(__FILE__, "HTParse");
184:
185: StrAllocCopy(name, aName);
186: StrAllocCopy(rel, relatedName);
187:
188: scan(name, &given);
189: scan(rel, &related);
190: result[0]=0; /* Clear string */
2.12 timbl 191: access = given.access ? given.access : related.access;
1.1 timbl 192: if (wanted & PARSE_ACCESS)
2.12 timbl 193: if (access) {
194: strcat(result, access);
1.1 timbl 195: if(wanted & PARSE_PUNCTUATION) strcat(result, ":");
196: }
197:
198: if (given.access && related.access) /* If different, inherit nothing. */
199: if (strcmp(given.access, related.access)!=0) {
200: related.host=0;
201: related.absolute=0;
202: related.relative=0;
203: related.anchor=0;
204: }
205:
206: if (wanted & PARSE_HOST)
207: if(given.host || related.host) {
208: if(wanted & PARSE_PUNCTUATION) strcat(result, "//");
209: strcat(result, given.host ? given.host : related.host);
210: }
211:
212: if (given.host && related.host) /* If different hosts, inherit no path. */
213: if (strcmp(given.host, related.host)!=0) {
214: related.absolute=0;
215: related.relative=0;
216: related.anchor=0;
217: }
218:
219: if (wanted & PARSE_PATH) {
220: if(given.absolute) { /* All is given */
221: if(wanted & PARSE_PUNCTUATION) strcat(result, "/");
222: strcat(result, given.absolute);
223: } else if(related.absolute) { /* Adopt path not name */
224: strcat(result, "/");
225: strcat(result, related.absolute);
226: if (given.relative) {
227: p = strchr(result, '?'); /* Search part? */
228: if (!p) p=result+strlen(result)-1;
229: for (; *p!='/'; p--); /* last / */
230: p[1]=0; /* Remove filename */
231: strcat(result, given.relative); /* Add given one */
2.31 frystyk 232: result = HTSimplify (result);
1.1 timbl 233: }
234: } else if(given.relative) {
235: strcat(result, given.relative); /* what we've got */
236: } else if(related.relative) {
237: strcat(result, related.relative);
238: } else { /* No inheritance */
239: strcat(result, "/");
240: }
241: }
242:
243: if (wanted & PARSE_ANCHOR)
244: if(given.anchor || related.anchor) {
245: if(wanted & PARSE_PUNCTUATION) strcat(result, "#");
246: strcat(result, given.anchor ? given.anchor : related.anchor);
247: }
248: free(rel);
249: free(name);
250:
251: StrAllocCopy(return_value, result);
252: free(result);
253: return return_value; /* exactly the right length */
254: }
255:
2.11 timbl 256:
2.21 frystyk 257: #if 0 /* NOT USED FOR THE MOMENT */
2.15 luotonen 258: /*
259: ** As strcpy() but guaranteed to work correctly
260: ** with overlapping parameters. AL 7 Feb 1994
261: */
262: PRIVATE void ari_strcpy ARGS2(char *, to,
263: char *, from)
264: {
265: char * tmp;
266:
267: if (!to || !from) return;
268:
269: tmp = (char*)malloc(strlen(from)+1);
270: if (!tmp) outofmem(__FILE__, "my_strcpy");
271:
272: strcpy(tmp, from);
273: strcpy(to, tmp);
274: free(tmp);
275: }
2.21 frystyk 276: #endif
277:
2.20 timbl 278:
279: /* Simplify a URI
280: // --------------
281: // A URI is allowed to contain the seqeunce xxx/../ which may be
1.1 timbl 282: // replaced by "" , and the seqeunce "/./" which may be replaced by "/".
2.20 timbl 283: // Simplification helps us recognize duplicate URIs.
1.1 timbl 284: //
285: // Thus, /etc/junk/../fred becomes /etc/fred
286: // /etc/junk/./fred becomes /etc/junk/fred
2.11 timbl 287: //
288: // but we should NOT change
289: // http://fred.xxx.edu/../..
290: //
291: // or ../../albert.html
2.26 frystyk 292: //
293: // In the same manner, the following prefixed are preserved:
294: //
295: // ./<etc>
296: // //<etc>
297: //
298: // In order to avoid empty URLs the following URLs become:
299: //
300: // /fred/.. becomes /fred/..
301: // /fred/././.. becomes /fred/..
2.27 frystyk 302: // /fred/.././junk/.././ becomes /fred/..
2.26 frystyk 303: //
2.30 frystyk 304: // If more than one set of `://' is found (several proxies in cascade) then
305: // only the part after the last `://' is simplified.
2.44 ! frystyk 306: //
! 307: // Returns: A string which might be the old one or a new one.
1.1 timbl 308: */
2.31 frystyk 309: PUBLIC char *HTSimplify ARGS1(char *, filename)
1.1 timbl 310: {
2.31 frystyk 311: char *path;
312: char *p;
2.19 frystyk 313:
2.31 frystyk 314: if (!filename) {
315: if (URI_TRACE)
2.44 ! frystyk 316: fprintf(TDEST, "HTSimplify.. Bad argument\n");
2.31 frystyk 317: return filename;
318: }
319: if (URI_TRACE)
2.44 ! frystyk 320: fprintf(TDEST, "HTSimplify.. `%s\' ", filename);
2.27 frystyk 321:
2.31 frystyk 322: if ((path = strstr(filename, "://")) != NULL) { /* Find host name */
2.30 frystyk 323: char *newptr;
2.31 frystyk 324: path += 3;
325: while ((newptr = strstr(path, "://")) != NULL)
326: path = newptr+3;
327: path = HTCanon(&filename, path); /* We have a host name */
328: } else if ((path = strstr(filename, ":/")) != NULL) {
329: path += 2;
2.27 frystyk 330: } else
2.31 frystyk 331: path = filename;
332: if (*path == '/' && *(path+1)=='/') { /* Some URLs start //<foo> */
333: path += 1;
2.34 frystyk 334: } else if (!strncmp(path, "news:", 5)) {
335: char *ptr = strchr(path+5, '@');
336: if (!ptr) ptr = path+5;
337: while (*ptr) { /* Make group or host lower case */
338: *ptr = TOLOWER(*ptr);
339: ptr++;
2.31 frystyk 340: }
341: if (URI_TRACE)
2.44 ! frystyk 342: fprintf(TDEST, "into\n............ `%s'\n", filename);
2.31 frystyk 343: return filename; /* Doesn't need to do any more */
344: }
345: if ((p = path)) {
346: int segments = 0;
347:
348: /* Parse string first time to find number of `real' tokens */
349: while (*p) {
350: if (*p=='/' || p==path) {
351: if (!((*(p+1)=='/' || !*(p+1)) ||
352: (*(p+1)=='.' && (*(p+2)=='/' || !*(p+2))) ||
353: (*(p+1)=='.' && *(p+2)=='.' &&(*(p+3)=='/' || !*(p+3)))))
354: segments++;
355: }
356: p++;
357: }
2.19 frystyk 358:
2.31 frystyk 359: /* Parse string second time to simplify */
360: p = path;
361: while(*p) {
362: if (*p=='/') {
363: if (p>path && *(p+1)=='.' && (*(p+2)=='/' || !*(p+2))) {
364: char *orig=p, *dest=p+2;
365: while ((*orig++ = *dest++)); /* Remove a slash and a dot */
366: p--;
367: } else if (segments>1 && *(p+1)=='.' && *(p+2)=='.' &&
368: (*(p+3)=='/' || !*(p+3))) {
369: char *q = p;
370: while (q>path && *--q!='/'); /* prev slash */
371: if (strncmp(q, "/../", 4) && strncmp(q, "/./", 3) &&
372: strncmp(q, "./", 2)) {
373: char *orig=q, *dest=p+3;
374: if (*q!='/') dest++;
375: while ((*orig++ = *dest++)); /* Remove /xxx/.. */
376: segments--;
377: p = q-1; /* Start again with prev slash */
378: } else
379: p++;
380: } else if (*(p+1)=='/') {
381: while (*(p+1)=='/') {
382: char *orig=p, *dest=p+1;
383: while ((*orig++ = *dest++)); /* Remove multiple /'s */
2.19 frystyk 384: }
385: }
386: }
2.31 frystyk 387: p++;
388: } /* end while (*p) */
2.19 frystyk 389: }
2.31 frystyk 390: if (URI_TRACE)
2.44 ! frystyk 391: fprintf(TDEST, "into\n............ `%s'\n", filename);
2.31 frystyk 392: return filename;
2.19 frystyk 393: }
2.31 frystyk 394:
2.19 frystyk 395: #ifdef OLD_CODE
2.17 frystyk 396: char * p = filename;
1.1 timbl 397: char * q;
2.17 frystyk 398:
399: if (p) {
400: while (*p && (*p == '/' || *p == '.')) /* Pass starting / or .'s */
401: p++;
402: while(*p) {
403: if (*p=='/') {
1.1 timbl 404: if ((p[1]=='.') && (p[2]=='.') && (p[3]=='/' || !p[3] )) {
2.11 timbl 405: for (q=p-1; (q>=filename) && (*q!='/'); q--); /* prev slash */
406: if (q[0]=='/' && 0!=strncmp(q, "/../", 4)
407: &&!(q-1>filename && q[-1]=='/')) {
2.15 luotonen 408: ari_strcpy(q, p+3); /* Remove /xxx/.. */
1.1 timbl 409: if (!*filename) strcpy(filename, "/");
410: p = q-1; /* Start again with prev slash */
2.11 timbl 411: } else { /* xxx/.. leave it! */
2.9 timbl 412: #ifdef BUG_CODE
2.15 luotonen 413: ari_strcpy(filename, p[3] ? p+4 : p+3); /* rm xxx/../ */
1.1 timbl 414: p = filename; /* Start again */
2.9 timbl 415: #endif
1.1 timbl 416: }
417: } else if ((p[1]=='.') && (p[2]=='/' || !p[2])) {
2.15 luotonen 418: ari_strcpy(p, p+2); /* Remove a slash and a dot */
2.13 luotonen 419: } else if (p[-1] != ':') {
420: while (p[1] == '/') {
2.15 luotonen 421: ari_strcpy(p, p+1); /* Remove multiple slashes */
2.13 luotonen 422: }
1.1 timbl 423: }
2.17 frystyk 424: }
425: p++;
426: } /* end while (*p) */
427: } /* end if (p) */
1.1 timbl 428: }
2.19 frystyk 429: #endif /* OLD_CODE */
1.1 timbl 430:
431:
432: /* Make Relative Name
433: ** ------------------
434: **
435: ** This function creates and returns a string which gives an expression of
436: ** one address as related to another. Where there is no relation, an absolute
437: ** address is retured.
438: **
439: ** On entry,
440: ** Both names must be absolute, fully qualified names of nodes
441: ** (no anchor bits)
442: **
443: ** On exit,
444: ** The return result points to a newly allocated name which, if
445: ** parsed by HTParse relative to relatedName, will yield aName.
446: ** The caller is responsible for freeing the resulting name later.
447: **
448: */
2.32 frystyk 449: char * HTRelative ARGS2(CONST char *, aName, CONST char *, relatedName)
1.1 timbl 450: {
451: char * result = 0;
452: CONST char *p = aName;
453: CONST char *q = relatedName;
454: CONST char * after_access = 0;
455: CONST char * path = 0;
456: CONST char * last_slash = 0;
457: int slashes = 0;
458:
459: for(;*p; p++, q++) { /* Find extent of match */
460: if (*p!=*q) break;
461: if (*p==':') after_access = p+1;
462: if (*p=='/') {
463: last_slash = p;
464: slashes++;
465: if (slashes==3) path=p;
466: }
467: }
468:
469: /* q, p point to the first non-matching character or zero */
470:
471: if (!after_access) { /* Different access */
472: StrAllocCopy(result, aName);
473: } else if (slashes<3){ /* Different nodes */
474: StrAllocCopy(result, after_access);
2.29 frystyk 475: #if 0 /* Henrik */
1.1 timbl 476: } else if (slashes==3){ /* Same node, different path */
477: StrAllocCopy(result, path);
2.21 frystyk 478: #endif
1.1 timbl 479: } else { /* Some path in common */
480: int levels= 0;
481: for(; *q && (*q!='#'); q++) if (*q=='/') levels++;
482: result = (char *)malloc(3*levels + strlen(last_slash) + 1);
483: if (result == NULL) outofmem(__FILE__, "HTRelative");
484: result[0]=0;
485: for(;levels; levels--)strcat(result, "../");
486: strcat(result, last_slash+1);
487: }
2.44 ! frystyk 488: if (URI_TRACE) fprintf(TDEST,
2.21 frystyk 489: "HTRelative.. `%s' expressed relative to `%s' is `%s'\n",
490: aName, relatedName, result);
1.1 timbl 491: return result;
492: }
2.1 timbl 493:
494:
2.31 frystyk 495: /* HTCanon
496: **
497: ** Canonicalizes the URL in the following manner starting from the host
498: ** pointer:
499: **
500: ** 1) The host name is converted to lowercase
501: ** 2) Expands the host name of the URL from a local name to a full
502: ** domain name. A host name is started by `://'.
2.38 frystyk 503: ** 3) Chop off port if `:80' (http), `:70' (gopher), or `:21' (ftp)
2.31 frystyk 504: **
505: ** Return: OK The position of the current path part of the URL
2.44 ! frystyk 506: ** which might be the old one or a new one.
2.31 frystyk 507: */
508: PUBLIC char *HTCanon ARGS2 (char **, filename, char *, host)
509: {
2.32 frystyk 510: char *newname = NULL;
2.31 frystyk 511: char *port;
512: char *strptr;
513: char *path;
2.36 frystyk 514: char *access = host-3;
2.31 frystyk 515:
2.36 frystyk 516: while (access>*filename && *(access-1)!='/') /* Find access method */
517: access--;
2.31 frystyk 518: if ((path = strchr(host, '/')) == NULL) /* Find path */
519: path = host + strlen(host);
520: if ((strptr = strchr(host, '@')) != NULL && strptr<path) /* UserId */
521: host = strptr;
2.39 frystyk 522: if ((port = strchr(host, ':')) != NULL && port>path) /* Port number */
523: port = NULL;
2.31 frystyk 524:
525: strptr = host; /* Convert to lower-case */
526: while (strptr<path) {
527: *strptr = TOLOWER(*strptr);
528: strptr++;
529: }
530:
531: /* Does the URL contain a full domain name? This also works for a
532: numerical host name. The domain name is already made lower-case
533: and without a trailing dot. */
2.35 frystyk 534: if (((strptr = strchr(host, '.')) == NULL || strptr >= path) &&
535: strncasecomp(host, "localhost", 9)) {
2.31 frystyk 536: CONST char *domain = HTGetDomainName();
537: if (domain) {
2.32 frystyk 538: if ((newname = (char *) calloc(1, strlen(*filename) +
2.31 frystyk 539: strlen(domain)+2)) == NULL)
540: outofmem(__FILE__, "HTCanon");
541: if (port)
2.32 frystyk 542: strncpy(newname, *filename, (int) (port-*filename));
2.31 frystyk 543: else
2.32 frystyk 544: strncpy(newname, *filename, (int) (path-*filename));
545: strcat(newname, ".");
546: strcat(newname, domain);
2.31 frystyk 547: }
548: } else { /* Look for a trailing dot */
549: char *dot = port ? port : path;
550: if (dot > *filename && *--dot=='.') {
551: char *orig=dot, *dest=dot+1;
552: while((*orig++ = *dest++));
553: if (port) port--;
554: path--;
555: }
556: }
2.36 frystyk 557: /* Chop off port if `:80' (http), `:70' (gopher), or `:21' (ftp) */
558: if (port) {
559: if ((!strncmp(access, "http", 4) &&
560: (*(port+1)=='8'&&*(port+2)=='0'&&(*(port+3)=='/'||!*(port+3)))) ||
561: (!strncmp(access, "gopher", 6) &&
562: (*(port+1)=='7'&&*(port+2)=='0'&&(*(port+3)=='/'||!*(port+3)))) ||
563: (!strncmp(access, "ftp", 3) &&
564: (*(port+1)=='2'&&*(port+2)=='1'&&(*(port+3)=='/'||!*(port+3))))) {
565: if (!newname) {
566: char *orig=port, *dest=port+3;
567: while((*orig++ = *dest++));
568: }
569: } else if (newname)
570: strncat(newname, port, (int) (path-port));
571: }
572:
2.32 frystyk 573: if (newname) {
574: char *newpath = newname+strlen(newname);
575: strcat(newname, path);
2.31 frystyk 576: path = newpath;
577: free(*filename); /* Free old copy */
2.32 frystyk 578: *filename = newname;
2.31 frystyk 579: }
580: return path;
581: }
2.1 timbl 582:
583:
2.24 luotonen 584: /* HTCleanTelnetString()
585: * Make sure that the given string doesn't contain characters that
586: * could cause security holes, such as newlines in ftp, gopher,
587: * news or telnet URLs; more specifically: allows everything between
2.26 frystyk 588: * ASCII 20-7E, and also A0-FE, inclusive. Also TAB ('\t') allowed!
2.24 luotonen 589: *
590: * On entry,
591: * str the string that is *modified* if necessary. The
592: * string will be truncated at the first illegal
593: * character that is encountered.
594: * On exit,
595: * returns YES, if the string was modified.
596: * NO, otherwise.
597: */
598: PUBLIC BOOL HTCleanTelnetString ARGS1(char *, str)
599: {
600: char * cur = str;
601:
602: if (!str) return NO;
603:
604: while (*cur) {
605: int a = TOASCII(*cur);
2.26 frystyk 606: if (a != 0x9 && (a < 0x20 || (a > 0x7E && a < 0xA0) || a > 0xFE)) {
2.31 frystyk 607: if (URI_TRACE)
2.44 ! frystyk 608: fprintf(TDEST, "Illegal..... character in URL: \"%s\"\n",str);
2.24 luotonen 609: *cur = 0;
2.31 frystyk 610: if (URI_TRACE)
2.44 ! frystyk 611: fprintf(TDEST, "Truncated... \"%s\"\n",str);
2.24 luotonen 612: return YES;
613: }
614: cur++;
615: }
616: return NO;
617: }
618:
Webmaster