]> andersk Git - moira.git/blob - server/qvalidate.dc
fixed lint problem; moved prefetch_value to qaccess.dc
[moira.git] / server / qvalidate.dc
1 /*
2  *      $Source$
3  *      $Author$
4  *      $Header$
5  *
6  *      Copyright (C) 1987 by the Massachusetts Institute of Technology
7  *      For copying and distribution information, please see the file
8  *      <mit-copyright.h>.
9  *
10  */
11
12 #ifndef lint
13 static char *rcsid_qsupport_dc = "$Header$";
14 #endif lint
15
16 #include <mit-copyright.h>
17 #include <unistd.h>
18 #include "query.h"
19 #include "mr_server.h"
20 #include <ctype.h>
21 EXEC SQL INCLUDE sqlca;
22 EXEC SQL INCLUDE sqlda;
23 #include "qrtn.h"
24
25 extern char *whoami;
26 extern int ingres_errno, mr_errcode;
27
28 EXEC SQL BEGIN DECLARE SECTION;
29 extern char stmt_buf[];
30 EXEC SQL END DECLARE SECTION;
31
32 EXEC SQL WHENEVER SQLERROR CALL ingerr;
33
34
35 /* Validation Routines */
36
37 validate_row(q, argv, v)
38     register struct query *q;
39     char *argv[];
40     register struct validate *v;
41 {
42     EXEC SQL BEGIN DECLARE SECTION;
43     char *name;
44     char qual[128];
45     int rowcount;
46     EXEC SQL END DECLARE SECTION;
47
48     /* build where clause */
49     build_qual(v->qual, v->argc, argv, qual);
50
51     if (log_flags & LOG_VALID)
52         /* tell the logfile what we're doing */
53         com_err(whoami, 0, "validating row: %s", qual);
54
55     /* look for the record */
56     sprintf(stmt_buf,"SELECT COUNT (*) FROM %s WHERE %s",q->rtable,qual);
57     EXEC SQL PREPARE stmt INTO :SQLDA USING NAMES FROM :stmt_buf;
58     if(sqlca.sqlcode)
59       return(MR_INTERNAL);
60     EXEC SQL DECLARE csr126 CURSOR FOR stmt;
61     EXEC SQL OPEN csr126;
62     EXEC SQL FETCH csr126 USING DESCRIPTOR :SQLDA;
63     EXEC SQL CLOSE csr126;
64     rowcount = *(int *)SQLDA->sqlvar[0].sqldata;
65
66     if (ingres_errno) return(mr_errcode);
67     if (rowcount == 0) return(MR_NO_MATCH);
68     if (rowcount > 1) return(MR_NOT_UNIQUE);
69     return(MR_EXISTS);
70 }
71
72 validate_fields(q, argv, vo, n)
73     struct query *q;
74     register char *argv[];
75     register struct valobj *vo;
76     register int n;
77 {
78     register int status;
79
80     while (--n >= 0) {
81         switch (vo->type) {
82         case V_NAME:
83             if (log_flags & LOG_VALID)
84                 com_err(whoami, 0, "validating %s in %s: %s",
85                     vo->namefield, vo->table, argv[vo->index]);
86             status = validate_name(argv, vo);
87             break;
88
89         case V_ID:
90             if (log_flags & LOG_VALID)
91                 com_err(whoami, 0, "validating %s in %s: %s",
92                     vo->idfield, vo->table, argv[vo->index]);
93             status = validate_id(q, argv, vo);
94             break;
95
96         case V_DATE:
97             if (log_flags & LOG_VALID)
98                 com_err(whoami, 0, "validating date: %s", argv[vo->index]);
99             status = validate_date(argv, vo);
100             break;
101
102         case V_TYPE:
103             if (log_flags & LOG_VALID)
104                 com_err(whoami, 0, "validating %s type: %s",
105                     vo->table, argv[vo->index]);
106             status = validate_type(argv, vo);
107             break;
108
109         case V_TYPEDATA:
110             if (log_flags & LOG_VALID)
111                 com_err(whoami, 0, "validating typed data (%s): %s",
112                     argv[vo->index - 1], argv[vo->index]);
113             status = validate_typedata(q, argv, vo);
114             break;
115
116         case V_RENAME:
117             if (log_flags & LOG_VALID)
118                 com_err(whoami, 0, "validating rename %s in %s",
119                         argv[vo->index], vo->table);
120             status = validate_rename(argv, vo);
121             break;
122
123         case V_CHAR:
124             if (log_flags & LOG_VALID)
125               com_err(whoami, 0, "validating chars: %s", argv[vo->index]);
126             status = validate_chars(argv[vo->index]);
127             break;
128
129         case V_SORT:
130             status = MR_EXISTS;
131             break;
132
133         case V_LOCK:
134             status = lock_table(vo);
135             break;
136
137         case V_WILD:
138             status = convert_wildcards(argv[vo->index]);
139             break;
140
141         case V_UPWILD:
142             status = convert_wildcards_uppercase(argv[vo->index]);
143             break;
144
145         }
146
147         if (status != MR_EXISTS) return(status);
148         vo++;
149     }
150
151     if (ingres_errno) return(mr_errcode);
152     return(MR_SUCCESS);
153 }
154
155
156 /* validate_chars: verify that there are no illegal characters in
157  * the string.  Legal characters are printing chars other than
158  * ", *, ?, \, [ and ].
159  */
160 static int illegalchars[] = {
161     1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /* ^@ - ^O */
162     1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /* ^P - ^_ */
163     0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, /* SPACE - / */
164     0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, /* 0 - ? */
165     0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* : - O */
166     0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, /* P - _ */
167     0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* ` - o */
168     0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, /* p - ^? */
169     1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
170     1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
171     1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
172     1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
173     1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
174     1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
175     1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
176     1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
177 };
178
179 validate_chars(s)
180 register char *s;
181 {
182     while (*s)
183       if (illegalchars[*s++])
184         return(MR_BAD_CHAR);
185     return(MR_EXISTS);
186 }
187
188
189 validate_id(q, argv, vo)
190     struct query *q;
191     char *argv[];
192     register struct valobj *vo;
193 {
194     EXEC SQL BEGIN DECLARE SECTION;
195     char *name, *tbl, *namefield, *idfield;
196     int id, rowcount;
197     EXEC SQL END DECLARE SECTION;
198     int status;
199     register char *c;
200
201     name = argv[vo->index];
202     tbl = vo->table;
203     namefield = vo->namefield;
204     idfield = vo->idfield;
205
206     if ((!strcmp(tbl, "users") && !strcmp(namefield, "login")) ||
207         !strcmp(tbl, "machine") ||
208         !strcmp(tbl, "subnet") ||
209         !strcmp(tbl, "filesys") ||
210         !strcmp(tbl, "list") ||
211         !strcmp(tbl, "cluster") ||
212         !strcmp(tbl, "strings")) {
213         if (!strcmp(tbl, "machine") || !strcmp(tbl, "subnet"))
214           for (c = name; *c; c++) if (islower(*c)) *c = toupper(*c);
215         status = name_to_id(name, tbl, &id);
216         if (status == 0) {
217             *(int *)argv[vo->index] = id;
218             return(MR_EXISTS);
219         } else if (status == MR_NO_MATCH && !strcmp(tbl, "strings") &&
220                    (q->type == APPEND || q->type == UPDATE)) {
221             id=add_string(name);
222             cache_entry(name, "STRING", id);
223             *(int *)argv[vo->index] = id;
224             return(MR_EXISTS);
225         } else if (status == MR_NO_MATCH || status == MR_NOT_UNIQUE)
226           return(vo->error);
227         else
228           return(status);
229     }
230
231     if (!strcmp(namefield, "uid")) {
232         sprintf(stmt_buf,"SELECT %s FROM %s WHERE %s = %s",idfield,tbl,namefield,name);
233     } else {
234         sprintf(stmt_buf,"SELECT %s FROM %s WHERE %s = '%s'",idfield,tbl,namefield,name);
235     }
236     EXEC SQL PREPARE stmt INTO :SQLDA USING NAMES FROM :stmt_buf;
237     if(sqlca.sqlcode)
238       return(MR_INTERNAL);
239     EXEC SQL DECLARE csr127 CURSOR FOR stmt;
240     EXEC SQL OPEN csr127;
241     rowcount=0;
242     EXEC SQL FETCH csr127 USING DESCRIPTOR :SQLDA;
243     if(sqlca.sqlcode == 0) {
244         rowcount++;
245         EXEC SQL FETCH csr127 USING DESCRIPTOR :SQLDA;
246         if(sqlca.sqlcode == 0) rowcount++;
247     }
248     EXEC SQL CLOSE csr127;
249     if (ingres_errno)
250       return(mr_errcode);
251
252     if (rowcount != 1) return(vo->error);
253     bcopy(SQLDA->sqlvar[0].sqldata,argv[vo->index],sizeof(int));
254     return(MR_EXISTS);
255 }
256
257 validate_name(argv, vo)
258     char *argv[];
259     register struct valobj *vo;
260 {
261     EXEC SQL BEGIN DECLARE SECTION;
262     char *name, *tbl, *namefield;
263     int rowcount;
264     EXEC SQL END DECLARE SECTION;
265     register char *c;
266
267     name = argv[vo->index];
268     tbl = vo->table;
269     namefield = vo->namefield;
270     if (!strcmp(tbl, "servers") && !strcmp(namefield, "name")) {
271         for (c = name; *c; c++)
272           if (islower(*c))
273             *c = toupper(*c);
274     }
275     sprintf(stmt_buf,"SELECT DISTINCT COUNT(*) FROM %s WHERE %s.%s = '%s'",
276             tbl,tbl,namefield,name);
277     EXEC SQL PREPARE stmt INTO :SQLDA USING NAMES FROM :stmt_buf;
278     if(sqlca.sqlcode)
279       return(MR_INTERNAL);
280     EXEC SQL DECLARE csr128 CURSOR FOR stmt;
281     EXEC SQL OPEN csr128;
282     EXEC SQL FETCH csr128 USING DESCRIPTOR :SQLDA;
283     rowcount = *(int *)SQLDA->sqlvar[0].sqldata;
284     EXEC SQL CLOSE csr128;
285
286     if (ingres_errno) return(mr_errcode);
287     return ((rowcount == 1) ? MR_EXISTS : vo->error);
288 }
289
290 validate_date(argv, vo)
291     char *argv[];
292     struct valobj *vo;
293 {
294     EXEC SQL BEGIN DECLARE SECTION;
295     char *idate;
296     double dd;
297     int errorno;
298     EXEC SQL END DECLARE SECTION;
299
300     idate = argv[vo->index];
301     EXEC SQL SELECT interval('years',date(:idate)-date('today')) INTO :dd;
302
303     if (sqlca.sqlcode != 0 || dd > 5.0) return(MR_DATE);
304     return(MR_EXISTS);
305 }
306
307
308 validate_rename(argv, vo)
309 char *argv[];
310 struct valobj *vo;
311 {
312     EXEC SQL BEGIN DECLARE SECTION;
313     char *name, *tbl, *namefield, *idfield;
314     int id;
315     EXEC SQL END DECLARE SECTION;
316     int status;
317     register char *c;
318
319     c = name = argv[vo->index];
320     while (*c)
321       if (illegalchars[*c++])
322         return(MR_BAD_CHAR);
323     tbl = vo->table;
324     /* minor kludge to upcasify machine names */
325     if (!strcmp(tbl, "machine"))
326         for (c = name; *c; c++) if (islower(*c)) *c = toupper(*c);
327     namefield = vo->namefield;
328     idfield = vo->idfield;
329     id = -1;
330     if (idfield == 0) {
331         if (!strcmp(argv[vo->index], argv[vo->index - 1]))
332           return(MR_EXISTS);
333         sprintf(stmt_buf,"SELECT %s FROM %s WHERE %s = LEFT('%s',SIZE(%s))",
334                 namefield,tbl,namefield,name,namefield);
335         EXEC SQL PREPARE stmt INTO :SQLDA USING NAMES FROM :stmt_buf;
336         if(sqlca.sqlcode)
337           return(MR_INTERNAL);
338         EXEC SQL DECLARE csr129 CURSOR FOR stmt;
339         EXEC SQL OPEN csr129;
340         EXEC SQL FETCH csr129 USING DESCRIPTOR :SQLDA;
341         if(sqlca.sqlcode == 0) id=1; else id=0;
342         EXEC SQL CLOSE csr129;
343
344         if (ingres_errno) return(mr_errcode);
345         if (id)
346           return(vo->error);
347         else
348           return(MR_EXISTS);
349     }
350     status = name_to_id(name, tbl, &id);
351     if (status == MR_NO_MATCH || id == *(int *)argv[vo->index - 1])
352       return(MR_EXISTS);
353     else
354       return(vo->error);
355 }
356
357
358 validate_type(argv, vo)
359     char *argv[];
360     register struct valobj *vo;
361 {
362     EXEC SQL BEGIN DECLARE SECTION;
363     char *typename;
364     char *val;
365     int cnt;
366     EXEC SQL END DECLARE SECTION;
367     register char *c;
368
369     typename = vo->table;
370     c = val = argv[vo->index];
371     while (*c) {
372         if (illegalchars[*c++])
373           return(MR_BAD_CHAR);
374     }
375
376     /* uppercase type fields */
377     for (c = val; *c; c++) if (islower(*c)) *c = toupper(*c);
378
379     EXEC SQL SELECT COUNT(trans) INTO :cnt FROM alias
380       WHERE name = :typename AND type='TYPE' AND trans = :val;
381     if (ingres_errno) return(mr_errcode);
382     return (cnt ? MR_EXISTS : vo->error);
383 }
384
385 /* validate member or type-specific data field */
386
387 validate_typedata(q, argv, vo)
388     register struct query *q;
389     register char *argv[];
390     register struct valobj *vo;
391 {
392     EXEC SQL BEGIN DECLARE SECTION;
393     char *name;
394     char *field_type;
395     char data_type[129];
396     int id;
397     EXEC SQL END DECLARE SECTION;
398     int status;
399     char *index();
400     register char *c;
401
402     /* get named object */
403     name = argv[vo->index];
404
405     /* get field type string (known to be at index-1) */
406     field_type = argv[vo->index-1];
407
408     /* get corresponding data type associated with field type name */
409     EXEC SQL SELECT trans INTO :data_type FROM alias
410       WHERE name = :field_type AND type='TYPEDATA';
411     if (ingres_errno) return(mr_errcode);
412     if (sqlca.sqlerrd[2] != 1) return(MR_TYPE);
413
414     /* now retrieve the record id corresponding to the named object */
415     if (index(data_type, ' '))
416         *index(data_type, ' ') = 0;
417     if (!strcmp(data_type, "user")) {
418         /* USER */
419         if (index(name, '@'))
420           return(MR_USER);
421         status = name_to_id(name, data_type, &id);
422         if (status && (status == MR_NO_MATCH || status == MR_NOT_UNIQUE))
423           return(MR_USER);
424         if (status) return(status);
425     } else if (!strcmp(data_type, "list")) {
426         /* LIST */
427         status = name_to_id(name, data_type, &id);
428         if (status && status == MR_NOT_UNIQUE)
429           return(MR_LIST);
430         if (status == MR_NO_MATCH) {
431             /* if idfield is non-zero, then if argv[0] matches the string
432              * that we're trying to resolve, we should get the value of
433              * numvalues.[idfield] for the id.
434              */
435             if (vo->idfield && !strcmp(argv[0], argv[vo->index])) {
436                 set_next_object_id(q->validate->object_id, q->rtable, 0);
437                 name = vo->idfield;
438                 EXEC SQL REPEATED SELECT value INTO :id FROM numvalues
439                   WHERE name = :name;
440                 if (sqlca.sqlerrd[2] != 1) return(MR_LIST);
441             } else
442               return(MR_LIST);
443         } else if (status) return(status);
444     } else if (!strcmp(data_type, "machine")) {
445         /* MACHINE */
446         for (c = name; *c; c++) if (islower(*c)) *c = toupper(*c);
447         status = name_to_id(name, data_type, &id);
448         if (status && (status == MR_NO_MATCH || status == MR_NOT_UNIQUE))
449           return(MR_MACHINE);
450         if (status) return(status);
451     } else if (!strcmp(data_type, "string")) {
452         /* STRING */
453         status = name_to_id(name, data_type, &id);
454         if (status && status == MR_NOT_UNIQUE)
455           return(MR_STRING);
456         if (status == MR_NO_MATCH) {
457             if (q->type != APPEND && q->type != UPDATE) return(MR_STRING);
458             id=add_string(name);
459             cache_entry(name, "STRING", id);
460         } else if (status) return(status);
461     } else if (!strcmp(data_type, "none")) {
462         id = 0;
463     } else {
464         return(MR_TYPE);
465     }
466
467     /* now set value in argv */
468     *(int *)argv[vo->index] = id;
469
470     return (MR_EXISTS);
471 }
472
473
474 /* Lock the table named by the validation object */
475
476 lock_table(vo)
477 struct valobj *vo;
478 {
479     sprintf(stmt_buf,"UPDATE %s SET modtime='now' WHERE %s.%s = 0",
480             vo->table,vo->table,vo->idfield);
481     EXEC SQL EXECUTE IMMEDIATE :stmt_buf;
482     if (ingres_errno) return(mr_errcode);
483     if (sqlca.sqlerrd[2] != 1)
484       return(vo->error);
485     else
486       return(MR_EXISTS);
487 }
488
489
490 /* Check the database at startup time.  For now this just resets the
491  * inprogress flags that the DCM uses.
492  */
493
494 sanity_check_database()
495 {
496 }
497
498
499 /* Dynamic SQL support routines */
500 MR_SQLDA_T *mr_alloc_SQLDA()
501 {
502     MR_SQLDA_T *it;
503     short *null_indicators;
504     register int j;
505
506     if((it=(MR_SQLDA_T *)malloc(sizeof(MR_SQLDA_T)))==NULL) {
507         com_err(whoami, MR_NO_MEM, "setting up SQLDA");
508         exit(1);
509     }
510
511     if((null_indicators=(short *)calloc(QMAXARGS,sizeof(short)))==NULL) {
512         com_err(whoami, MR_NO_MEM, "setting up SQLDA null indicators");
513         exit(1);
514     }
515
516     for(j=0; j<QMAXARGS; j++) {
517         if((it->sqlvar[j].sqldata=(char *)malloc(sizeof(short)+ARGLEN))==NULL) {
518             com_err(whoami, MR_NO_MEM, "setting up SQLDA variables");
519             exit(1);
520         }
521         it->sqlvar[j].sqllen=ARGLEN;
522         it->sqlvar[j].sqlind=null_indicators+j;
523         null_indicators[j]=0;
524     }
525     it->sqln=QMAXARGS;
526     return it;
527 }
528
529
530 /* Use this after FETCH USING DESCRIPTOR one or more
531  * result columns may contain NULLs.  This routine is
532  * not currently needed, since db/schema creates all
533  * columns with a NOT NULL WITH DEFAULT clause.
534  *
535  * This is currently dead flesh, since no Moira columns
536  * allow null values; all use default values.
537  */
538 mr_fix_nulls_in_SQLDA(da)
539     MR_SQLDA_T *da;
540 {
541     register IISQLVAR *var;
542     register int j;
543     int *intp;
544
545     for(j=0, var=da->sqlvar; j<da->sqld; j++, var++) {
546         switch(var->sqltype) {
547           case -IISQ_CHA_TYPE:
548             if(*var->sqlind)
549               *var->sqldata='\0';
550             break;
551           case -IISQ_INT_TYPE:
552             if(*var->sqlind) {
553                 intp=(int *)var->sqldata;
554                 *intp=0;
555             }
556             break;
557         }
558     }
559 }
560
561 /* Convert normal Unix-style wildcards to SQL voodoo */
562 convert_wildcards(arg)
563     char *arg;
564 {
565     static char buffer[ARGLEN];
566     register char *s, *d;
567
568     for(d=buffer,s=arg;*s;s++) {
569         switch(*s) {
570           case '*': *d++='%'; *d++='%'; break;
571           case '?': *d++='_'; break;
572           case '_':
573           case '[':
574           case ']': *d++='*'; *d++ = *s; break;
575           case '%': *d++='*'; *d++='%'; *d++='%'; break;
576           default: *d++ = *s; break;
577         }
578     }
579     *d='\0';
580
581     /* Copy back into argv */
582     strcpy(arg,buffer);
583
584     return(MR_EXISTS);
585 }
586
587 /* This version includes uppercase conversion, for things like gmac.
588  * This is necessary because "LIKE" doesn't work with "uppercase()".
589  * Including it in a wildcard routine saves making two passes over
590  * the argument string.
591  */
592 convert_wildcards_uppercase(arg)
593     char *arg;
594 {
595     static char buffer[ARGLEN];
596     register char *s, *d;
597
598     for(d=buffer,s=arg;*s;s++) {
599         switch(*s) {
600           case '*': *d++='%'; *d++='%'; break;
601           case '?': *d++='_'; break;
602           case '_':
603           case '[':
604           case ']': *d++='*'; *d++ = *s; break;
605           case '%': *d++='*'; *d++='%'; *d++='%'; break;
606           default: *d++=toupper(*s); break;       /* This is the only diff. */
607         }
608     }
609     *d='\0';
610
611     /* Copy back into argv */
612     strcpy(arg,buffer);
613
614     return(MR_EXISTS);
615 }
616
617
618 /* Looks like it's time to build an abstraction barrier, Yogi */
619 mr_select_any(stmt)
620     EXEC SQL BEGIN DECLARE SECTION; 
621     char *stmt;
622     EXEC SQL END DECLARE SECTION; 
623 {
624     int result=0;
625
626     EXEC SQL PREPARE stmt FROM :stmt;
627     EXEC SQL DESCRIBE stmt INTO :SQLDA;
628     if(SQLDA->sqld==0)                       /* Not a SELECT */
629         return(MR_INTERNAL);        
630     EXEC SQL DECLARE csr CURSOR FOR stmt;
631     EXEC SQL OPEN csr;
632     EXEC SQL FETCH csr USING DESCRIPTOR :SQLDA;
633     if(sqlca.sqlcode==0) 
634         result=MR_EXISTS;
635     else if((sqlca.sqlcode<0) && mr_errcode)
636         result=mr_errcode;
637     else
638         result=0;
639     EXEC SQL CLOSE csr;
640     return(result);
641
642
643
644
645 /*  Adds a string to the string table.  Returns the id number.
646  * 
647  */
648 int add_string(name)
649     EXEC SQL BEGIN DECLARE SECTION; 
650     char *name;
651     EXEC SQL END DECLARE SECTION;
652 {
653     EXEC SQL BEGIN DECLARE SECTION;
654     char buf[256];
655     int id;
656     EXEC SQL END DECLARE SECTION; 
657
658     EXEC SQL SELECT value INTO :id FROM numvalues WHERE name = 'strings_id';
659     id++;
660     EXEC SQL UPDATE numvalues SET value = :id WHERE name = 'strings_id';
661     
662     /* Use sprintf to get around problem with doubled single quotes */
663     sprintf(buf,"INSERT INTO strings (string_id, string) VALUES (%d, '%s')",id,name);
664     EXEC SQL EXECUTE IMMEDIATE :buf;
665  
666     return(id);
667 }
668
This page took 0.089504 seconds and 5 git commands to generate.