]> git.lizzy.rs Git - plan9front.git/blob - sys/src/cmd/mothra/forms.c
mothra: handle <input type=image>
[plan9front.git] / sys / src / cmd / mothra / forms.c
1 #include <u.h>
2 #include <libc.h>
3 #include <draw.h>
4 #include <event.h>
5 #include <panel.h>
6 #include "mothra.h"
7 #include "html.h"
8
9 typedef struct Field Field;
10 typedef struct Option Option;
11 struct Form{
12         int method;
13         char *ctype;
14         char *action;
15         Field *fields, *efields;
16         Form *next;
17 };
18 struct Field{
19         Field *next;
20         Form *form;
21         char *name;
22         char *value;
23         int checked;
24         int size;               /* should be a point, but that feature is deprecated */
25         int maxlength;
26         int type;
27         int rows, cols;
28         Option *options;
29         int multiple;
30         int state;              /* is the button marked? */
31         Panel *p;
32         Panel *pulldown;
33         Panel *textwin;
34 };
35 /*
36  * Field types
37  */
38 enum{
39         TYPEIN=1,
40         CHECK,
41         PASSWD,
42         RADIO,
43         SUBMIT,
44         RESET,
45         BUTTON,
46         SELECT,
47         TEXTWIN,
48         HIDDEN,
49         INDEX,
50         FILE,
51 };
52 struct Option{
53         int selected;
54         int def;
55         char label[NLABEL+1];
56         char *value;
57         Option *next;
58 };
59
60 #define BOUNDARY "nAboJ9uN6ZXsqoVGzLAdjKq97TWDTGjo"
61
62 void h_checkinput(Panel *, int, int);
63 void h_radioinput(Panel *, int, int);
64 void h_submitinput(Panel *, int);
65 void h_buttoninput(Panel *, int);
66 void h_fileinput(Panel *, int);
67 void h_submittype(Panel *, char *);
68 void h_submitindex(Panel *, char *);
69 void h_resetinput(Panel *, int);
70 void h_select(Panel *, int, int);
71 char *selgen(Panel *, int);
72 char *nullgen(Panel *, int);
73 Field *newfield(Form *form){
74         Field *f;
75         f=emalloc(sizeof(Field));
76         if(form->efields==0)
77                 form->fields=f;
78         else
79                 form->efields->next=f;
80         form->efields=f;
81         f->next=0;
82         f->form=form;
83         return f;
84 }
85 /*
86  * Called by rdhtml on seeing a forms-related tag
87  */
88 void rdform(Hglob *g){
89         char *s;
90         Field *f;
91         Option *o, **op;
92         Form *form;
93         switch(g->tag){
94         default:
95                 fprint(2, "Bad tag <%s> in rdform (Can't happen!)\n", g->token);
96                 return;
97         case Tag_form:
98                 if(g->form){
99                         htmlerror(g->name, g->lineno, "nested forms illegal\n");
100                         break;
101                 }
102                 g->form=emalloc(sizeof(Form));
103                 s=pl_getattr(g->attr, "action");
104                 g->form->action=strdup((s && *s) ? s : g->dst->url->fullname);
105                 s=pl_getattr(g->attr, "method");
106                 if(s==0 || *s==0)
107                         g->form->method=GET;
108                 else if(cistrcmp(s, "post")==0)
109                         g->form->method=POST;
110                 else{
111                         if(cistrcmp(s, "get")!=0)
112                                 htmlerror(g->name, g->lineno,
113                                         "unknown form method %s\n", s);
114                         g->form->method=GET;
115                 }
116                 s=pl_getattr(g->attr, "enctype");
117                 if(s && cistrcmp(s, "multipart/form-data")==0)
118                         g->form->ctype = "multipart/form-data; boundary=" BOUNDARY;
119                 g->form->fields=0;
120
121                 g->form->next = g->dst->form;
122                 g->dst->form = g->form;
123                 break;
124         case Tag_input:
125         case Tag_button:
126                 if(g->form==0){
127                         /* no form, assume link button */
128                         form = emalloc(sizeof(Form));   
129                         form->method = 0;
130                         form->fields = 0;
131                         form->efields = 0;
132                         if(g->state->link[0])
133                                 form->action = strdup(g->state->link);
134                         form->next = g->dst->form;
135                         g->dst->form = form;
136                         f=newfield(form);
137                 } else
138                         f=newfield(g->form);
139                 s=pl_getattr(g->attr, "name");
140                 if(s==0 || *s == 0)
141                         f->name=0;
142                 else
143                         f->name=strdup(s);
144                 s=pl_getattr(g->attr, "value");
145                 if(s==0)
146                         f->value=strdup("");
147                 else
148                         f->value=strdup(s);
149                 f->checked=pl_hasattr(g->attr, "checked");
150                 s=pl_getattr(g->attr, "size");
151                 if(s==0 || *s==0)
152                         f->size=20;
153                 else
154                         f->size=atoi(s);
155                 s=pl_getattr(g->attr, "maxlength");
156                 if(s==0 || *s==0)
157                         f->maxlength=0x3fffffff;
158                 else
159                         f->maxlength=atoi(s);
160                 s=pl_getattr(g->attr, "type");
161                 if((g->tag == Tag_button) && 
162                    (s==0 || cistrcmp(s, "reset") || cistrcmp(s, "button")))
163                         s="submit";
164                 else if(s==0)
165                         s="text";
166                 if(cistrcmp(s, "checkbox")==0)
167                         f->type=CHECK;
168                 else if(cistrcmp(s, "radio")==0)
169                         f->type=RADIO;
170                 else if(cistrcmp(s, "submit")==0)
171                         f->type=SUBMIT;
172                 else if(cistrcmp(s, "image")==0){
173                         f->type=SUBMIT;
174                         s=pl_getattr(g->attr, "src");
175                         if(s && *s)
176                                 nstrcpy(g->state->image, s, sizeof(g->state->image));
177                         s=pl_getattr(g->attr, "width");
178                         if(s && *s)
179                                 g->state->width=strtolength(g, HORIZ, s);
180                         s=pl_getattr(g->attr, "height");
181                         if(s && *s)
182                                 g->state->height=strtolength(g, VERT, s);
183                         s=pl_getattr(g->attr, "alt");
184                         if(s==0 || *s == 0) s = f->value;
185                         pl_htmloutput(g, g->nsp, s, f);
186                         g->state->image[0] = 0;
187                         g->state->width=0;
188                         g->state->height=0;
189                         break;
190                 }
191                 else if(cistrcmp(s, "button")==0)
192                         f->type=BUTTON;
193                 else if(cistrcmp(s, "file")==0)
194                         f->type=FILE;
195                 else if(cistrcmp(s, "reset")==0)
196                         f->type=RESET;
197                 else if(cistrcmp(s, "hidden")==0)
198                         f->type=HIDDEN;
199                 else{
200                         f->type=TYPEIN;
201                         if(cistrcmp(s, "password")==0)
202                                 f->type=PASSWD;
203                         s=f->name;
204                         if(s && cistrcmp(s, "isindex")==0)
205                                 f->type=INDEX;
206
207                         /*
208                          * If there's exactly one attribute, use its value as the name,
209                          * regardless of the attribute name.  This makes
210                          * http://linus.att.com/ias/puborder.html work.
211                          */
212                         if(s==0){
213                                 if(g->attr[0].name && g->attr[1].name==0)
214                                         f->name=strdup(g->attr[0].value);
215                                 else
216                                         f->name=strdup("no-name");
217                         }
218                 }
219                 if((f->type==CHECK || f->type==RADIO) && !pl_hasattr(g->attr, "value")){
220                         free(f->value);
221                         f->value=strdup("on");
222                 }
223                 if(f->type!=HIDDEN)
224                         pl_htmloutput(g, g->nsp, f->value[0]?f->value:"blank field", f);
225                 break;
226         case Tag_select:
227                 if(g->form==0){
228                 BadTag:
229                         htmlerror(g->name, g->lineno, "<%s> not in form, ignored\n",
230                                 tag[g->tag].name);
231                         break;
232                 }
233                 f=newfield(g->form);
234                 s=pl_getattr(g->attr, "name");
235                 if(s==0 || *s==0){
236                         f->name=strdup("select");
237                         htmlerror(g->name, g->lineno, "select has no name=\n");
238                 }
239                 else
240                         f->name=strdup(s);
241                 f->multiple=pl_hasattr(g->attr, "multiple");
242                 f->type=SELECT;
243                 f->size=0;
244                 f->options=0;
245                 g->text=g->token;
246                 g->tp=g->text;
247                 g->etext=g->text;
248                 break;
249         case Tag_option:
250                 if(g->form==0) goto BadTag;
251                 if((f=g->form->efields)==0) goto BadTag;
252                 if(f->size<8)
253                         f->size++;
254                 o=emalloc(sizeof(Option));
255                 for(op=&f->options;*op;op=&(*op)->next);
256                 *op=o;
257                 o->next=0;
258                 g->text=o->label;
259                 g->tp=o->label;
260                 g->etext=o->label+NLABEL;
261                 memset(o->label, 0, NLABEL+1);
262                 *g->tp++=' ';
263                 o->def=pl_hasattr(g->attr, "selected");
264                 o->selected=o->def;
265                 if(pl_hasattr(g->attr, "disabled"))
266                         o->selected=0;
267                 s=pl_getattr(g->attr, "value");
268                 if(s==0)
269                         o->value=o->label+1;
270                 else
271                         o->value=strdup(s);
272                 break;
273         case Tag_textarea:
274                 if(g->form==0) goto BadTag;
275                 f=newfield(g->form);
276                 s=pl_getattr(g->attr, "name");
277                 if(s==0 || *s==0){
278                         f->name=strdup("enter text");
279                         htmlerror(g->name, g->lineno, "select has no name=\n");
280                 }
281                 else
282                         f->name=strdup(s);
283                 s=pl_getattr(g->attr, "rows");
284                 f->rows=(s && *s)?atoi(s):8;
285                 s=pl_getattr(g->attr, "cols");
286                 f->cols=(s && *s)?atoi(s):30;
287                 f->type=TEXTWIN;
288                 /* suck up initial text */
289                 pl_htmloutput(g, g->nsp, f->name, f);
290                 break;
291         case Tag_isindex:
292                 /*
293                  * Make up a form with one tag, of type INDEX
294                  * I have seen a page with <ISINDEX PROMPT="Enter a title here ">,
295                  * which is nonstandard and not handled here.
296                  */
297                 form=emalloc(sizeof(Form));
298                 form->fields=0;
299                 form->efields=0;
300                 s=pl_getattr(g->attr, "action");
301                 form->action=strdup((s && *s) ? s : g->dst->url->fullname);
302                 form->method=GET;
303                 form->fields=0;
304                 f=newfield(form);
305                 f->name=0;
306                 f->value=strdup("");
307                 f->size=20;
308                 f->maxlength=0x3fffffff;
309                 f->type=INDEX;
310                 pl_htmloutput(g, g->nsp, f->value[0]?f->value:"blank field", f);
311                 break;
312         }
313 }
314 /*
315  * Called by rdhtml on seeing a forms-related end tag
316  */
317 void endform(Hglob *g){
318         Field *f;
319
320         switch(g->tag){
321         case Tag_form:
322                 g->form=0;
323                 break;
324         case Tag_select:
325                 if(g->form==0)
326                         htmlerror(g->name, g->lineno, "</select> not in form, ignored\n");
327                 else if((f=g->form->efields)==0)
328                         htmlerror(g->name, g->lineno, "spurious </select>\n");
329                 else
330                         pl_htmloutput(g, g->nsp, f->name, f);
331                 break;
332         case Tag_textarea:
333                 break;
334         }
335 }
336 char *nullgen(Panel *, int ){
337         return 0;
338 }
339 char *selgen(Panel *p, int index){
340         Option *a;
341         Field *f;
342         f=p->userp;
343         if(f==0) return 0;
344         for(a=f->options;index!=0 && a!=0;--index,a=a->next);
345         if(a==0) return 0;
346         a->label[0]=a->selected?'*':' ';
347         return a->label;
348 }
349 char *seloption(Field *f){
350         Option *a;
351         for(a=f->options;a!=0;a=a->next)
352                 if(a->selected)
353                         return a->label+1;
354         return f->name;
355 }
356 void mkfieldpanel(Rtext *t){
357         Action *a;
358         Panel *win, *scrl, *menu, *pop, *button;
359         Field *f;
360
361         if((a = t->user) == nil)
362                 return;
363         if((f = a->field) == nil)
364                 return;
365
366         f->p=0;
367         switch(f->type){
368         case TYPEIN:
369                 f->p=plentry(0, 0, f->size*chrwidth, f->value, h_submittype);
370                 break;
371         case PASSWD:
372                 f->p=plentry(0, USERFL, f->size*chrwidth, f->value, h_submittype);
373                 break;
374         case CHECK:
375                 f->p=plcheckbutton(0, 0, "", h_checkinput);
376                 f->state=f->checked;
377                 plsetbutton(f->p, f->checked);
378                 break;
379         case RADIO:
380                 f->p=plradiobutton(0, 0, "", h_radioinput);
381                 f->state=f->checked;
382                 plsetbutton(f->p, f->checked);
383                 break;
384         case SUBMIT:
385                 f->p=plbutton(0, 0, f->value[0]?f->value:"submit", h_submitinput);
386                 break;
387         case RESET:
388                 f->p=plbutton(0, 0, f->value[0]?f->value:"reset", h_resetinput);
389                 break;
390         case BUTTON:
391                 f->p=plbutton(0, 0, f->value[0]?f->value:"button", h_buttoninput);
392                 break;
393         case FILE:
394                 f->p=plbutton(0, 0, f->value[0]?f->value:"file", h_fileinput);
395                 break;
396         case SELECT:
397                 if(f->size <= 0)
398                         f->size=1;
399                 f->pulldown=plgroup(0,0);
400                 scrl=plscrollbar(f->pulldown, PACKW|FILLY);
401                 win=pllist(f->pulldown, PACKN, nullgen, f->size, h_select);
402                 win->userp=f;
403                 plinitlist(win, PACKN, selgen, f->size, h_select);
404                 plscroll(win, 0, scrl);
405                 plpack(f->pulldown, Rect(0,0,1024,1024));
406                 f->p=plpulldown(0, FIXEDX, seloption(f), f->pulldown, PACKS);
407                 f->p->fixedsize.x=f->pulldown->r.max.x-f->pulldown->r.min.x;
408                 break;
409         case TEXTWIN:
410                 f->p=plframe(0,0);
411                 pllabel(f->p, PACKN|FILLX, f->name);
412                 scrl=plscrollbar(f->p, PACKW|FILLY);
413                 f->textwin=pledit(f->p, EXPAND, Pt(f->cols*chrwidth, f->rows*font->height), 0, 0, 0);
414                 f->textwin->userp=f;
415                 plscroll(f->textwin, 0, scrl);
416                 break;
417         case INDEX:
418                 f->p=plentry(0, 0, f->size*chrwidth, f->value, h_submitindex);
419                 break;
420         }
421         if(f->p){
422                 f->p->userp=f;
423                 free(t->text);
424                 t->text=0;
425                 t->p=f->p;
426                 t->flags|=PL_HOT;
427         }
428 }
429 void h_checkinput(Panel *p, int, int v){
430         ((Field *)p->userp)->state=v;
431 }
432 void h_radioinput(Panel *p, int, int v){
433         Field *f, *me;
434         me=p->userp;
435         me->state=v;
436         if(v){
437                 for(f=me->form->fields;f;f=f->next)
438                         if(f->type==RADIO && f!=me && strcmp(f->name, me->name)==0){
439                                 plsetbutton(f->p, 0);
440                                 f->state=0;
441                                 pldraw(f->p, screen);
442                         }
443         }
444 }
445 void h_select(Panel *p, int, int index){
446         Option *a;
447         Field *f;
448         f=p->userp;
449         if(f==0) return;
450         if(!f->multiple) for(a=f->options;a;a=a->next) a->selected=0;
451         for(a=f->options;index!=0 && a!=0;--index,a=a->next);
452         if(a==0) return;
453         a->selected=!a->selected;
454         plinitpulldown(f->p, FIXEDX, seloption(f), f->pulldown, PACKS);
455         pldraw(f->p, screen);
456 }
457 void h_resetinput(Panel *p, int){
458         Field *f;
459         Option *o;
460         for(f=((Field *)p->userp)->form->fields;f;f=f->next) switch(f->type){
461         case TYPEIN:
462                 plinitentry(f->p, 0, f->size*chrwidth, f->value, 0);
463                 break;
464         case PASSWD:
465                 plinitentry(f->p, USERFL, f->size*chrwidth, f->value, 0);
466                 break;
467         case FILE:
468                 free(f->value);
469                 f->value=strdup("");
470                 if(f->p==nil) break;
471                 f->p->state=0;
472                 pldraw(f->p, screen);
473                 break;
474         case CHECK:
475         case RADIO:
476                 f->state=f->checked;
477                 plsetbutton(f->p, f->checked);
478                 break;
479         case SELECT:
480                 for(o=f->options;o;o=o->next)
481                         o->selected=o->def;
482                 break;
483         }
484         pldraw(text, screen);
485 }
486
487 void h_buttoninput(Panel *p, int){
488         Field *f;
489
490         f = p->userp;
491         if(f && f->form && f->form->method != POST && f->form->action)
492                 geturl(f->form->action, -1, 0, 0);
493 }
494
495 void h_fileinput(Panel *p, int){
496         char name[NNAME];
497         Field *f;
498
499         f = p->userp;
500         nstrcpy(name, f->value, sizeof(name));
501         for(;;){
502                 if(eenter("Upload file", name, sizeof(name), &mouse) <= 0)
503                         break;
504                 if(access(name, AREAD) == 0)
505                         break;
506         }
507         free(f->value);
508         f->value = strdup(name);
509         p->state = name[0] != 0;
510         pldraw(f->p, screen);
511 }
512
513 /*
514  * If there's exactly one button with type=text, then
515  * a CR in the button is supposed to submit the form.
516  */
517 void h_submittype(Panel *p, char *){
518         int ntype;
519         Field *f;
520         ntype=0;
521         for(f=((Field *)p->userp)->form->fields;f;f=f->next)
522                 if(f->type==TYPEIN || f->type==PASSWD)
523                         ntype++;
524         if(ntype==1) h_submitinput(p, 0);
525 }
526 void h_submitindex(Panel *p, char *){
527         h_submitinput(p, 0);
528 }
529
530 void mencodeform(Form *form, int fd){
531         char *b, *p, *sep;
532         int ifd, n, nb;
533         Option *o;
534         Field *f;
535         Rune *rp;
536
537         sep = "--" BOUNDARY;
538         for(f=form->fields;f;f=f->next)switch(f->type){
539         case TYPEIN:
540         case PASSWD:
541                 fprint(fd, "%s\r\nContent-Disposition: form-data; name=\"%s\"\r\n\r\n%s",
542                         sep, f->name, plentryval(f->p));
543                 sep = "\r\n--" BOUNDARY;
544                 break;
545         case CHECK:
546         case RADIO:
547         case SUBMIT:
548                 if(!f->state) break;
549         case HIDDEN:
550                 if(f->name==0 || f->value==0)
551                         break;
552                 fprint(fd, "%s\r\nContent-Disposition: form-data; name=\"%s\"\r\n\r\n%s",
553                         sep, f->name, f->value);
554                 sep = "\r\n--" BOUNDARY;
555                 break;
556         case SELECT:
557                 if(f->name==0) break;
558                 for(o=f->options;o;o=o->next)
559                         if(o->selected && o->value){
560                                 fprint(fd, "%s\r\nContent-Disposition: form-data; name=\"%s\"\r\n\r\n%s",
561                                         sep, f->name, o->value);
562                                 sep = "\r\n--" BOUNDARY;
563                         }
564                 break;
565         case TEXTWIN:
566                 if(f->name==0) break;
567                 n=plelen(f->textwin);
568                 rp=pleget(f->textwin);
569                 p=b=malloc(UTFmax*n+1);
570                 if(b == nil)
571                         break;
572                 while(n > 0){
573                         p += runetochar(p, rp);
574                         rp++;
575                         n--;
576                 }
577                 *p = 0;
578                 fprint(fd, "%s\r\nContent-Disposition: form-data; name=\"%s\"\r\n\r\n%s",
579                         sep, f->name, b);
580                 sep = "\r\n--" BOUNDARY;
581                 free(b);
582                 break;
583         case FILE:
584                 if(f->name==0 || f->value[0]==0)
585                         break;
586                 if(p = strrchr(f->value, '/'))
587                         p++;
588                 if(p == 0 || *p == 0)
589                         p = f->value;
590                 if((b = malloc(nb = 8192)) == nil)
591                         break;
592                 if((ifd = open(f->value, OREAD)) >= 0){
593                         if(filetype(ifd, b, nb) < 0)
594                                 strcpy(b, "application/octet-stream");
595                         fprint(fd, "%s\r\nContent-Disposition: form-data; name=\"%s\"; filename=\"%s\""
596                                 "\r\nContent-Type: %s\r\n\r\n", sep, f->name, p, b);
597                         sep = "\r\n--" BOUNDARY;
598                         while((n = read(ifd, b, nb)) > 0)
599                                 if(write(fd, b, n) != n)
600                                         break;
601                         close(ifd);
602                 }
603                 free(b);
604                 break;
605         }
606         fprint(fd, "%s--\r\n", sep);
607 }
608
609 void uencodeform(Form *form, int fd){
610         char *b, *p, *sep;
611         Option *o;
612         Field *f;
613         Rune *rp;
614         int n;
615
616         sep = "";
617         for(f=form->fields;f;f=f->next) switch(f->type){
618         case TYPEIN:
619         case PASSWD:
620                 fprint(fd, "%s%U=%U", sep, f->name, plentryval(f->p));
621                 sep = "&";
622                 break;
623         case INDEX:
624                 fprint(fd, "%s%U", sep, plentryval(f->p));
625                 sep = "&";
626                 break;
627         case CHECK:
628         case RADIO:
629         case SUBMIT:
630                 if(!f->state) break;
631         case HIDDEN:
632                 if(f->name==0 || f->value==0)
633                         break;
634                 fprint(fd, "%s%U=%U", sep, f->name, f->value);
635                 sep = "&";
636                 break;
637         case SELECT:
638                 if(f->name==0) break;
639                 for(o=f->options;o;o=o->next)
640                         if(o->selected && o->value){
641                                 fprint(fd, "%s%U=%U", sep, f->name, o->value);
642                                 sep = "&";
643                         }
644                 break;
645         case TEXTWIN:
646                 if(f->name==0) break;
647                 n=plelen(f->textwin);
648                 rp=pleget(f->textwin);
649                 p=b=malloc(UTFmax*n+1);
650                 if(b == nil)
651                         break;
652                 while(n > 0){
653                         p += runetochar(p, rp);
654                         rp++;
655                         n--;
656                 }
657                 *p = 0;
658                 fprint(fd, "%s%U=%U", sep, f->name, b);
659                 sep = "&";
660                 free(b);
661                 break;
662         }
663 }
664
665 void h_submitinput(Panel *p, int){
666         char buf[NNAME];
667         Form *form;
668         Field *f;
669         int n, fd;
670
671         f = p->userp;
672         form=f->form;
673         for(f=form->fields;f;f=f->next)
674                 if(f->type==SUBMIT)
675                         f->state = (f->p == p);
676
677         switch(form->method){
678         case GET:
679                 strcpy(buf, "/tmp/mfXXXXXXXXXXX");
680                 fd = create(mktemp(buf), ORDWR|ORCLOSE, 0600);
681                 break;
682         case POST:
683                 fd = urlpost(selurl(form->action), form->ctype);
684                 break;
685         default:
686                 return;
687         }
688
689         if(fd < 0){
690                 message("submit: %r");
691                 return;
692         }
693         if(form->method==GET){
694                 fprint(fd, "%s?", form->action);
695                 uencodeform(form, fd);
696                 seek(fd, 0, 0);
697                 n = readn(fd, buf, sizeof(buf));
698                 close(fd);
699                 if(n < 0 || n >= sizeof(buf)){
700                         message("submit: too large");
701                         return;
702                 }
703                 buf[n] = 0;
704                 if(debug)fprint(2, "GET %s\n", buf);
705                 geturl(buf, -1, 0, 0);
706         } else {
707                 if(debug){
708                         fprint(2, "POST %s ->\n", form->action);
709                         if(form->ctype)
710                                 mencodeform(form, 2);
711                         else
712                                 uencodeform(form, 2);
713                         fprint(2, "\n");
714                 }
715                 /* only set for multipart/form-data */
716                 if(form->ctype)
717                         mencodeform(form, fd);
718                 else
719                         uencodeform(form, fd);
720                 geturl(form->action, fd, 0, 0);
721         }
722 }
723
724 void freeform(void *p)
725 {
726         Form *form;
727         Field *f;
728         Option *o;
729
730         while(form = p){
731                 p = form->next;
732                 free(form->action);
733                 while(f = form->fields){
734                         form->fields = f->next;
735
736                         if(f->p!=0)
737                                 plfree(f->p);
738
739                         free(f->name);
740                         free(f->value);
741
742                         while(o = f->options){
743                                 f->options = o->next;
744                                 if(o->value != o->label+1)
745                                         free(o->value);
746                                 free(o);
747                         }
748
749                         free(f);
750                 }
751                 free(form);
752         }
753 }
754
755 int Ufmt(Fmt *f){
756         char *s = va_arg(f->args, char*);
757         for(; *s; s++){
758                 if(strchr("/$-_@.!*'(),", *s)
759                 || 'a'<=*s && *s<='z'
760                 || 'A'<=*s && *s<='Z'
761                 || '0'<=*s && *s<='9')
762                         fmtprint(f, "%c", *s);
763                 else if(*s==' ')
764                         fmtprint(f, "+");
765                 else
766                         fmtprint(f, "%%%.2X", *s & 0xFF);
767         }
768         return 0;
769 }