1 /* HTML forms parser */
2 
3 #ifdef HAVE_CONFIG_H
4 #include "config.h"
5 #endif
6 
7 #include <stdarg.h>
8 #include <stdio.h>
9 #include <stdlib.h>
10 #include <string.h>
11 
12 #include "elinks.h"
13 
14 #include "bfu/listmenu.h"
15 #include "bfu/menu.h"
16 #include "document/html/parser/forms.h"
17 #include "document/html/parser/link.h"
18 #include "document/html/parser/stack.h"
19 #include "document/html/parser/parse.h"
20 #include "document/html/parser.h"
21 #include "document/html/renderer.h"
22 #include "document/forms.h"
23 #include "intl/charsets.h"
24 #include "protocol/protocol.h"
25 #include "protocol/uri.h"
26 #include "util/conv.h"
27 #include "util/error.h"
28 #include "util/memdebug.h"
29 #include "util/memlist.h"
30 #include "util/memory.h"
31 #include "util/string.h"
32 
33 /* Unsafe macros */
34 #include "document/html/internal.h"
35 
36 
37 
38 void
html_form(struct html_context * html_context,unsigned char * a,unsigned char * xxx3,unsigned char * xxx4,unsigned char ** xxx5)39 html_form(struct html_context *html_context, unsigned char *a,
40           unsigned char *xxx3, unsigned char *xxx4, unsigned char **xxx5)
41 {
42 	unsigned char *al;
43 	struct form *form;
44 
45 	html_context->was_br = 1;
46 
47 	form = init_form();
48 	if (!form) return;
49 
50 	form->method = FORM_METHOD_GET;
51 	form->form_num = a - html_context->startf;
52 
53 	al = get_attr_val(a, "method", html_context->options);
54 	if (al) {
55 		if (!c_strcasecmp(al, "post")) {
56 			unsigned char *enctype;
57 
58 			enctype  = get_attr_val(a, "enctype",
59 			                        html_context->options);
60 
61 			form->method = FORM_METHOD_POST;
62 			if (enctype) {
63 				if (!c_strcasecmp(enctype, "multipart/form-data"))
64 					form->method = FORM_METHOD_POST_MP;
65 				else if (!c_strcasecmp(enctype, "text/plain"))
66 					form->method = FORM_METHOD_POST_TEXT_PLAIN;
67 				mem_free(enctype);
68 			}
69 		}
70 		mem_free(al);
71 	}
72 
73 	al = get_attr_val(a, "name", html_context->options);
74 	if (al) form->name = al;
75 
76 	al = get_attr_val(a, "action", html_context->options);
77 	/* The HTML specification at
78 	 * http://www.w3.org/TR/REC-html40/interact/forms.html#h-17.3 states
79 	 * that the behavior of an empty action attribute should be undefined.
80 	 * Mozilla handles action="" as action="<current-URI>" which seems
81 	 * reasonable. (bug 615) */
82 	if (al && *al) {
83 		form->action = join_urls(html_context->base_href, trim_chars(al, ' ', NULL));
84 		mem_free(al);
85 
86 	} else {
87 		enum uri_component components = URI_ORIGINAL;
88 
89 		mem_free_if(al);
90 
91 		/* We have to do following for GET method, because we would end
92 		 * up with two '?' otherwise. */
93 		if (form->method == FORM_METHOD_GET)
94 			components = URI_FORM_GET;
95 
96 		form->action = get_uri_string(html_context->base_href, components);
97 
98 		/* No action URI should contain post data */
99 		assert(!form->action || !strchr(form->action, POST_CHAR));
100 
101 		/* GET method URIs should not have '?'. */
102 		assert(!form->action
103 			|| form->method != FORM_METHOD_GET
104 			|| !strchr(form->action, '?'));
105 	}
106 
107 	al = get_target(html_context->options, a);
108 	form->target = al ? al : stracpy(html_context->base_target);
109 
110 	html_context->special_f(html_context, SP_FORM, form);
111 }
112 
113 
114 static int
get_form_mode(struct html_context * html_context,unsigned char * attr)115 get_form_mode(struct html_context *html_context, unsigned char *attr)
116 {
117 	if (has_attr(attr, "disabled", html_context->options))
118 		return FORM_MODE_DISABLED;
119 
120 	if (has_attr(attr, "readonly", html_context->options))
121 		return FORM_MODE_READONLY;
122 
123 	return FORM_MODE_NORMAL;
124 }
125 
126 static struct form_control *
init_form_control(enum form_type type,unsigned char * attr,struct html_context * html_context)127 init_form_control(enum form_type type, unsigned char *attr,
128                   struct html_context *html_context)
129 {
130 	struct form_control *fc;
131 
132 	fc = mem_calloc(1, sizeof(*fc));
133 	if (!fc) return NULL;
134 
135 	fc->type = type;
136 	fc->position = attr - html_context->startf;
137 	fc->mode = get_form_mode(html_context, attr);
138 
139 	return fc;
140 }
141 
142 void
html_button(struct html_context * html_context,unsigned char * a,unsigned char * xxx3,unsigned char * xxx4,unsigned char ** xxx5)143 html_button(struct html_context *html_context, unsigned char *a,
144             unsigned char *xxx3, unsigned char *xxx4, unsigned char **xxx5)
145 {
146 	unsigned char *al;
147 	struct form_control *fc;
148 	enum form_type type = FC_SUBMIT;
149 
150 	html_focusable(html_context, a);
151 
152 	al = get_attr_val(a, "type", html_context->options);
153 	if (!al) goto no_type_attr;
154 
155 	if (!c_strcasecmp(al, "button")) {
156 		type = FC_BUTTON;
157 	} else if (!c_strcasecmp(al, "reset")) {
158 		type = FC_RESET;
159 	} else if (c_strcasecmp(al, "submit")) {
160 		/* unknown type */
161 		mem_free(al);
162 		return;
163 	}
164 	mem_free(al);
165 
166 no_type_attr:
167 	fc = init_form_control(type, a, html_context);
168 	if (!fc) return;
169 
170 	fc->name = get_attr_val(a, "name", html_context->options);
171 	fc->default_value = get_attr_val(a, "value", html_context->options);
172 	if (!fc->default_value) {
173 		if (fc->type == FC_SUBMIT)
174 			fc->default_value = stracpy("Submit");
175 		else if (fc->type == FC_RESET)
176 			fc->default_value = stracpy("Reset");
177 		else if (fc->type == FC_BUTTON)
178 			fc->default_value = stracpy("Button");
179 	}
180 	if (!fc->default_value)
181 		fc->default_value = stracpy("");
182 
183 	html_context->special_f(html_context, SP_CONTROL, fc);
184 	format.form = fc;
185 	format.style.attr |= AT_BOLD;
186 }
187 
188 static void
html_input_format(struct html_context * html_context,unsigned char * a,struct form_control * fc)189 html_input_format(struct html_context *html_context, unsigned char *a,
190 	   	  struct form_control *fc)
191 {
192 	put_chrs(html_context, " ", 1);
193 	html_stack_dup(html_context, ELEMENT_KILLABLE);
194 	html_focusable(html_context, a);
195 	format.form = fc;
196 	if (format.title) mem_free(format.title);
197 	format.title = get_attr_val(a, "title", html_context->options);
198 	switch (fc->type) {
199 		case FC_TEXT:
200 		case FC_PASSWORD:
201 		case FC_FILE:
202 		{
203 			int i;
204 
205 			format.style.attr |= AT_BOLD;
206 			for (i = 0; i < fc->size; i++)
207 				put_chrs(html_context, "_", 1);
208 			break;
209 		}
210 		case FC_CHECKBOX:
211 			format.style.attr |= AT_BOLD;
212 			put_chrs(html_context, "[&nbsp;]", 8);
213 			break;
214 		case FC_RADIO:
215 			format.style.attr |= AT_BOLD;
216 			put_chrs(html_context, "(&nbsp;)", 8);
217 			break;
218 		case FC_IMAGE:
219 		{
220 			unsigned char *al;
221 
222 			mem_free_set(&format.image, NULL);
223 			al = get_url_val(a, "src", html_context->options);
224 			if (!al)
225 				al = get_url_val(a, "dynsrc",
226 				                 html_context->options);
227 			if (al) {
228 				format.image = join_urls(html_context->base_href, al);
229 				mem_free(al);
230 			}
231 			format.style.attr |= AT_BOLD;
232 			put_chrs(html_context, "[&nbsp;", 7);
233 			if (fc->alt)
234 				put_chrs(html_context, fc->alt, strlen(fc->alt));
235 			else if (fc->name)
236 				put_chrs(html_context, fc->name, strlen(fc->name));
237 			else
238 				put_chrs(html_context, "Submit", 6);
239 
240 			put_chrs(html_context, "&nbsp;]", 7);
241 			break;
242 		}
243 		case FC_SUBMIT:
244 		case FC_RESET:
245 		case FC_BUTTON:
246 			format.style.attr |= AT_BOLD;
247 			put_chrs(html_context, "[&nbsp;", 7);
248 			if (fc->default_value)
249 				put_chrs(html_context, fc->default_value, strlen(fc->default_value));
250 			put_chrs(html_context, "&nbsp;]", 7);
251 			break;
252 		case FC_TEXTAREA:
253 		case FC_SELECT:
254 		case FC_HIDDEN:
255 			INTERNAL("bad control type");
256 	}
257 	kill_html_stack_item(html_context, &html_top);
258 	put_chrs(html_context, " ", 1);
259 }
260 
261 void
html_input(struct html_context * html_context,unsigned char * a,unsigned char * xxx3,unsigned char * xxx4,unsigned char ** xxx5)262 html_input(struct html_context *html_context, unsigned char *a,
263            unsigned char *xxx3, unsigned char *xxx4, unsigned char **xxx5)
264 {
265 	unsigned char *al;
266 	struct form_control *fc;
267 
268 	fc = init_form_control(FC_TEXT, a, html_context);
269 	if (!fc) return;
270 
271 	al = get_attr_val(a, "type", html_context->options);
272 	if (al) {
273 		if (!c_strcasecmp(al, "text")) fc->type = FC_TEXT;
274 		else if (!c_strcasecmp(al, "hidden")) fc->type = FC_HIDDEN;
275 		else if (!c_strcasecmp(al, "button")) fc->type = FC_BUTTON;
276 		else if (!c_strcasecmp(al, "checkbox")) fc->type = FC_CHECKBOX;
277 		else if (!c_strcasecmp(al, "radio")) fc->type = FC_RADIO;
278 		else if (!c_strcasecmp(al, "password")) fc->type = FC_PASSWORD;
279 		else if (!c_strcasecmp(al, "submit")) fc->type = FC_SUBMIT;
280 		else if (!c_strcasecmp(al, "reset")) fc->type = FC_RESET;
281 		else if (!c_strcasecmp(al, "file")) fc->type = FC_FILE;
282 		else if (!c_strcasecmp(al, "image")) fc->type = FC_IMAGE;
283 		/* else unknown type, let it default to FC_TEXT. */
284 		mem_free(al);
285 	}
286 
287 	if (fc->type != FC_FILE)
288 		fc->default_value = get_attr_val(a, "value",
289 		                                 html_context->options);
290 	if (!fc->default_value) {
291 		if (fc->type == FC_CHECKBOX)
292 			fc->default_value = stracpy("on");
293 		else if (fc->type == FC_SUBMIT)
294 			fc->default_value = stracpy("Submit");
295 		else if (fc->type == FC_RESET)
296 			fc->default_value = stracpy("Reset");
297 		else if (fc->type == FC_BUTTON)
298 			fc->default_value = stracpy("Button");
299 	}
300 	if (!fc->default_value)
301 		fc->default_value = stracpy("");
302 
303 	fc->name = get_attr_val(a, "name", html_context->options);
304 
305 	fc->size = get_num(a, "size", html_context->options);
306 	if (fc->size == -1)
307 		fc->size = html_context->options->default_form_input_size;
308 	fc->size++;
309 	if (fc->size > html_context->options->box.width)
310 		fc->size = html_context->options->box.width;
311 	fc->maxlength = get_num(a, "maxlength", html_context->options);
312 	if (fc->maxlength == -1) fc->maxlength = INT_MAX;
313 	if (fc->type == FC_CHECKBOX || fc->type == FC_RADIO)
314 		fc->default_state = has_attr(a, "checked",
315 		                             html_context->options);
316 	if (fc->type == FC_IMAGE)
317 		fc->alt = get_attr_val(a, "alt", html_context->options);
318 
319 	if (fc->type != FC_HIDDEN) {
320 		html_input_format(html_context, a, fc);
321 	}
322 
323 	html_context->special_f(html_context, SP_CONTROL, fc);
324 }
325 
326 static struct list_menu lnk_menu;
327 
328 static void
do_html_select(unsigned char * attr,unsigned char * html,unsigned char * eof,unsigned char ** end,struct html_context * html_context)329 do_html_select(unsigned char *attr, unsigned char *html,
330 	       unsigned char *eof, unsigned char **end,
331 	       struct html_context *html_context)
332 {
333 	struct conv_table *ct = html_context->special_f(html_context, SP_TABLE, NULL);
334 	struct form_control *fc;
335 	struct string lbl = NULL_STRING, orig_lbl = NULL_STRING;
336 	unsigned char **values = NULL;
337 	unsigned char **labels;
338 	unsigned char *name, *t_attr, *en;
339 	int namelen;
340 	int nnmi = 0;
341 	int order = 0;
342 	int preselect = -1;
343 	int group = 0;
344 	int i, max_width;
345 	int closing_tag;
346 
347 	html_focusable(html_context, attr);
348 	init_menu(&lnk_menu);
349 
350 se:
351         en = html;
352 
353 see:
354         html = en;
355 	while (html < eof && *html != '<') html++;
356 
357 	if (html >= eof) {
358 
359 abort:
360 		*end = html;
361 		if (lbl.source) done_string(&lbl);
362 		if (orig_lbl.source) done_string(&orig_lbl);
363 		if (values) {
364 			int j;
365 
366 			for (j = 0; j < order; j++)
367 				mem_free_if(values[j]);
368 			mem_free(values);
369 		}
370 		destroy_menu(&lnk_menu);
371 		*end = en;
372 		return;
373 	}
374 
375 	if (lbl.source) {
376 		unsigned char *q, *s = en;
377 		int l = html - en;
378 
379 		while (l && isspace(s[0])) s++, l--;
380 		while (l && isspace(s[l-1])) l--;
381 		q = convert_string(ct, s, l,
382 		                   html_context->options->cp,
383 		                   CSM_DEFAULT, NULL, NULL, NULL);
384 		if (q) add_to_string(&lbl, q), mem_free(q);
385 		add_bytes_to_string(&orig_lbl, s, l);
386 	}
387 
388 	if (html + 2 <= eof && (html[1] == '!' || html[1] == '?')) {
389 		html = skip_comment(html, eof);
390 		goto se;
391 	}
392 
393 	if (parse_element(html, eof, &name, &namelen, &t_attr, &en)) {
394 		html++;
395 		goto se;
396 	}
397 
398 	if (!namelen) goto see;
399 
400 	if (name[0] == '/') {
401 		namelen--;
402 		if (!namelen) goto see;
403 		name++;
404 		closing_tag = 1;
405 	} else {
406 		closing_tag = 0;
407 	}
408 
409 	if (closing_tag && !c_strlcasecmp(name, namelen, "SELECT", 6)) {
410 		add_select_item(&lnk_menu, &lbl, &orig_lbl, values, order, nnmi);
411 		goto end_parse;
412 	}
413 
414 	if (!c_strlcasecmp(name, namelen, "OPTION", 6)) {
415 		add_select_item(&lnk_menu, &lbl, &orig_lbl, values, order, nnmi);
416 
417 		if (!closing_tag) {
418 			unsigned char *value, *label;
419 
420 			if (has_attr(t_attr, "disabled", html_context->options))
421 				goto see;
422 			if (preselect == -1
423 			    && has_attr(t_attr, "selected", html_context->options))
424 				preselect = order;
425 			value = get_attr_val(t_attr, "value", html_context->options);
426 
427 			if (!mem_align_alloc(&values, order, order + 1, unsigned char *, 0xFF))
428 				goto abort;
429 
430 			values[order++] = value;
431 			label = get_attr_val(t_attr, "label", html_context->options);
432 			if (label) new_menu_item(&lnk_menu, label, order - 1, 0);
433 			if (!value || !label) {
434 				init_string(&lbl);
435 				init_string(&orig_lbl);
436 				nnmi = !!label;
437 			}
438 		}
439 
440 		goto see;
441 	}
442 
443 	if (!c_strlcasecmp(name, namelen, "OPTGROUP", 8)) {
444 		add_select_item(&lnk_menu, &lbl, &orig_lbl, values, order, nnmi);
445 
446 		if (group) new_menu_item(&lnk_menu, NULL, -1, 0), group = 0;
447 
448 		if (!closing_tag) {
449 			unsigned char *label;
450 
451 			label = get_attr_val(t_attr, "label", html_context->options);
452 
453 			if (!label) {
454 				label = stracpy("");
455 				if (!label) goto see;
456 			}
457 			new_menu_item(&lnk_menu, label, -1, 0);
458 			group = 1;
459 		}
460 	}
461 
462 	goto see;
463 
464 
465 end_parse:
466 	*end = en;
467 	if (!order) goto abort;
468 
469 	labels = mem_calloc(order, sizeof(unsigned char *));
470 	if (!labels) goto abort;
471 
472 	fc = init_form_control(FC_SELECT, attr, html_context);
473 	if (!fc) {
474 		mem_free(labels);
475 		goto abort;
476 	}
477 
478 	fc->name = get_attr_val(attr, "name", html_context->options);
479 	fc->default_state = preselect < 0 ? 0 : preselect;
480 	fc->default_value = order ? stracpy(values[fc->default_state]) : stracpy("");
481 	fc->nvalues = order;
482 	fc->values = values;
483 	fc->menu = detach_menu(&lnk_menu);
484 	fc->labels = labels;
485 
486 	menu_labels(fc->menu, "", labels);
487 	put_chrs(html_context, "[", 1);
488 	html_stack_dup(html_context, ELEMENT_KILLABLE);
489 	format.form = fc;
490 	format.style.attr |= AT_BOLD;
491 
492 	max_width = 0;
493 	for (i = 0; i < order; i++) {
494 		if (!labels[i]) continue;
495 		int_lower_bound(&max_width, strlen(labels[i]));
496 	}
497 
498 	for (i = 0; i < max_width; i++)
499 		put_chrs(html_context, "_", 1);
500 
501 	kill_html_stack_item(html_context, &html_top);
502 	put_chrs(html_context, "]", 1);
503 	html_context->special_f(html_context, SP_CONTROL, fc);
504 }
505 
506 
507 static void
do_html_select_multiple(struct html_context * html_context,unsigned char * a,unsigned char * html,unsigned char * eof,unsigned char ** end)508 do_html_select_multiple(struct html_context *html_context, unsigned char *a,
509                         unsigned char *html, unsigned char *eof,
510                         unsigned char **end)
511 {
512 	unsigned char *al = get_attr_val(a, "name", html_context->options);
513 
514 	if (!al) return;
515 	html_focusable(html_context, a);
516 	html_top.type = ELEMENT_DONT_KILL;
517 	mem_free_set(&format.select, al);
518 	format.select_disabled = has_attr(a, "disabled", html_context->options)
519 	                         ? FORM_MODE_DISABLED
520 	                         : FORM_MODE_NORMAL;
521 }
522 
523 void
html_select(struct html_context * html_context,unsigned char * a,unsigned char * html,unsigned char * eof,unsigned char ** end)524 html_select(struct html_context *html_context, unsigned char *a,
525             unsigned char *html, unsigned char *eof, unsigned char **end)
526 {
527 	if (has_attr(a, "multiple", html_context->options))
528 		do_html_select_multiple(html_context, a, html, eof, end);
529 	else
530 		do_html_select(a, html, eof, end, html_context);
531 
532 }
533 
534 void
html_option(struct html_context * html_context,unsigned char * a,unsigned char * xxx3,unsigned char * xxx4,unsigned char ** xxx5)535 html_option(struct html_context *html_context, unsigned char *a,
536             unsigned char *xxx3, unsigned char *xxx4, unsigned char **xxx5)
537 {
538 	struct form_control *fc;
539 	unsigned char *val;
540 
541 	if (!format.select) return;
542 
543 	val = get_attr_val(a, "value", html_context->options);
544 	if (!val) {
545 		struct string str;
546 		unsigned char *p, *r;
547 		unsigned char *name;
548 		int namelen;
549 
550 		for (p = a - 1; *p != '<'; p--);
551 
552 		if (!init_string(&str)) goto end_parse;
553 		if (parse_element(p, html_context->eoff, NULL, NULL, NULL, &p)) {
554 			INTERNAL("parse element failed");
555 			val = str.source;
556 			goto end_parse;
557 		}
558 
559 se:
560 		while (p < html_context->eoff && isspace(*p)) p++;
561 		while (p < html_context->eoff && !isspace(*p) && *p != '<') {
562 
563 sp:
564 			add_char_to_string(&str, *p ? *p : ' '), p++;
565 		}
566 
567 		r = p;
568 		val = str.source; /* Has to be before the possible 'goto end_parse' */
569 
570 		while (r < html_context->eoff && isspace(*r)) r++;
571 		if (r >= html_context->eoff) goto end_parse;
572 		if (r - 2 <= html_context->eoff && (r[1] == '!' || r[1] == '?')) {
573 			p = skip_comment(r, html_context->eoff);
574 			goto se;
575 		}
576 		if (parse_element(r, html_context->eoff, &name, &namelen, NULL, &p)) goto sp;
577 
578 		if (namelen < 6) goto se;
579 		if (name[0] == '/') name++, namelen--;
580 
581 		if (c_strlcasecmp(name, namelen, "OPTION", 6)
582 		    && c_strlcasecmp(name, namelen, "SELECT", 6)
583 		    && c_strlcasecmp(name, namelen, "OPTGROUP", 8))
584 			goto se;
585 	}
586 
587 end_parse:
588 	fc = init_form_control(FC_CHECKBOX, a, html_context);
589 	if (!fc) {
590 		mem_free_if(val);
591 		return;
592 	}
593 
594 	fc->name = null_or_stracpy(format.select);
595 	fc->default_value = val;
596 	fc->default_state = has_attr(a, "selected", html_context->options);
597 	fc->mode = has_attr(a, "disabled", html_context->options)
598 	           ? FORM_MODE_DISABLED
599 	           : format.select_disabled;
600 
601 	put_chrs(html_context, " ", 1);
602 	html_stack_dup(html_context, ELEMENT_KILLABLE);
603 	format.form = fc;
604 	format.style.attr |= AT_BOLD;
605 	put_chrs(html_context, "[ ]", 3);
606 	kill_html_stack_item(html_context, &html_top);
607 	put_chrs(html_context, " ", 1);
608 	html_context->special_f(html_context, SP_CONTROL, fc);
609 }
610 
611 void
html_textarea(struct html_context * html_context,unsigned char * attr,unsigned char * html,unsigned char * eof,unsigned char ** end)612 html_textarea(struct html_context *html_context, unsigned char *attr,
613               unsigned char *html, unsigned char *eof, unsigned char **end)
614 {
615 	struct form_control *fc;
616 	unsigned char *p, *t_name, *wrap_attr;
617 	int t_namelen;
618 	int cols, rows;
619 	int i;
620 
621 	html_focusable(html_context, attr);
622 	while (html < eof && (*html == '\n' || *html == '\r')) html++;
623 	p = html;
624 	while (p < eof && *p != '<') {
625 
626 pp:
627 		p++;
628 	}
629 	if (p >= eof) {
630 		*end = eof;
631 		return;
632 	}
633 	if (parse_element(p, eof, &t_name, &t_namelen, NULL, end)) goto pp;
634 	if (c_strlcasecmp(t_name, t_namelen, "/TEXTAREA", 9)) goto pp;
635 
636 	fc = init_form_control(FC_TEXTAREA, attr, html_context);
637 	if (!fc) return;
638 
639 	fc->name = get_attr_val(attr, "name", html_context->options);
640 	fc->default_value = memacpy(html, p - html);
641 	for (p = fc->default_value; p && p[0]; p++) {
642 		/* FIXME: We don't cope well with entities here. Bugzilla uses
643 		 * &#13; inside of textarea and we fail miserably upon that
644 		 * one.  --pasky */
645 		if (p[0] == '\r') {
646 			if (p[1] == '\n'
647 			    || (p > fc->default_value && p[-1] == '\n')) {
648 				memmove(p, p + 1, strlen(p));
649 				p--;
650 			} else {
651 				p[0] = '\n';
652 			}
653 		}
654 	}
655 
656 	cols = get_num(attr, "cols", html_context->options);
657 	if (cols <= 0)
658 		cols = html_context->options->default_form_input_size;
659 	cols++; /* Add 1 column, other browsers may have different
660 		   behavior here (mozilla adds 2) --Zas */
661 	if (cols > html_context->options->box.width)
662 		cols = html_context->options->box.width;
663 	fc->cols = cols;
664 
665 	rows = get_num(attr, "rows", html_context->options);
666 	if (rows <= 0) rows = 1;
667 	if (rows > html_context->options->box.height)
668 		rows = html_context->options->box.height;
669 	fc->rows = rows;
670 	html_context->options->needs_height = 1;
671 
672 	wrap_attr = get_attr_val(attr, "wrap", html_context->options);
673 	if (wrap_attr) {
674 		if (!c_strcasecmp(wrap_attr, "hard")
675 		    || !c_strcasecmp(wrap_attr, "physical")) {
676 			fc->wrap = FORM_WRAP_HARD;
677 		} else if (!c_strcasecmp(wrap_attr, "soft")
678 			   || !c_strcasecmp(wrap_attr, "virtual")) {
679 			fc->wrap = FORM_WRAP_SOFT;
680 		} else if (!c_strcasecmp(wrap_attr, "none")
681 			   || !c_strcasecmp(wrap_attr, "off")) {
682 			fc->wrap = FORM_WRAP_NONE;
683 		}
684 		mem_free(wrap_attr);
685 
686 	} else if (has_attr(attr, "nowrap", html_context->options)) {
687 		fc->wrap = FORM_WRAP_NONE;
688 
689 	} else {
690 		fc->wrap = FORM_WRAP_SOFT;
691 	}
692 
693 	fc->maxlength = get_num(attr, "maxlength", html_context->options);
694 	if (fc->maxlength == -1) fc->maxlength = INT_MAX;
695 
696 	if (rows > 1) ln_break(html_context, 1);
697 	else put_chrs(html_context, " ", 1);
698 
699 	html_stack_dup(html_context, ELEMENT_KILLABLE);
700 	format.form = fc;
701 	format.style.attr |= AT_BOLD;
702 
703 	for (i = 0; i < rows; i++) {
704 		int j;
705 
706 		for (j = 0; j < cols; j++)
707 			put_chrs(html_context, "_", 1);
708 		if (i < rows - 1)
709 			ln_break(html_context, 1);
710 	}
711 
712 	kill_html_stack_item(html_context, &html_top);
713 	if (rows > 1)
714 		ln_break(html_context, 1);
715 	else
716 		put_chrs(html_context, " ", 1);
717 	html_context->special_f(html_context, SP_CONTROL, fc);
718 }
719