1 /*-
2  * SPDX-License-Identifier: BSD-2-Clause
3  *
4  * Copyright (c) 2021 Alfonso Sabato Siciliano
5  *
6  * Redistribution and use in source and binary forms, with or without
7  * modification, are permitted provided that the following conditions
8  * are met:
9  * 1. Redistributions of source code must retain the above copyright
10  *    notice, this list of conditions and the following disclaimer.
11  * 2. Redistributions in binary form must reproduce the above copyright
12  *    notice, this list of conditions and the following disclaimer in the
13  *    documentation and/or other materials provided with the distribution.
14  *
15  * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
16  * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18  * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
19  * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
21  * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
22  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
23  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
24  * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
25  * SUCH DAMAGE.
26  */
27 
28 #include <sys/param.h>
29 
30 #include <ctype.h>
31 #include <stdlib.h>
32 #include <string.h>
33 
34 #ifdef PORTNCURSES
35 #include <ncurses/form.h>
36 #else
37 #include <form.h>
38 #endif
39 
40 #include "bsddialog.h"
41 #include "lib_util.h"
42 #include "bsddialog_theme.h"
43 
44 #define REDRAWFORM 14021986 /* magic number */
45 #define ISFIELDHIDDEN(item)   (item.flags & BSDDIALOG_FIELDHIDDEN)
46 #define ISFIELDREADONLY(item) (item.flags & BSDDIALOG_FIELDREADONLY)
47 
48 /* "Form": inputbox - passwordbox - form - passwordform - mixedform */
49 
50 extern struct bsddialog_theme t;
51 
52 /* util struct for private buffer and view options */
53 struct myfield {
54 	int len;
55 	char *buf;
56 	int pos;
57 	int size;
58 	bool secure;
59 	int securech;
60 };
61 #define GETMYFIELD(field) ((struct myfield*)field_userptr(field))
62 #define GETMYFIELD2(form) ((struct myfield*)field_userptr(current_field(form)))
63 
insertch(struct myfield * mf,int ch)64 static void insertch(struct myfield *mf, int ch)
65 {
66 	int i;
67 
68 	if (mf->len == mf->size)
69 		return;
70 
71 	for (i=mf->len-1; i>=mf->pos; i--) {
72 		mf->buf[i+1] = mf->buf[i];
73 	}
74 
75 	mf->buf[mf->pos] = ch;
76 	mf->pos += 1;
77 	mf->len += 1;
78 	mf->buf[mf->len] = '\0';
79 }
80 
shiftleft(struct myfield * mf)81 static void shiftleft(struct myfield *mf)
82 {
83 	int i, last;
84 
85 	for (i=mf->pos; i<mf->len; i++) {
86 		mf->buf[i] = mf->buf[i+1];
87 	}
88 
89 	last = mf->len > 0 ? mf->len -1 : 0;
90 	mf->buf[last] = '\0';
91 		mf->len = last;
92 }
93 
94 static int
form_handler(struct bsddialog_conf * conf,WINDOW * widget,int y,int cols,struct buttons bs,WINDOW * formwin,FORM * form,FIELD ** cfield,int nitems,struct bsddialog_formitem * items)95 form_handler(struct bsddialog_conf *conf, WINDOW *widget, int y, int cols,
96     struct buttons bs, WINDOW *formwin, FORM *form, FIELD **cfield, int nitems,
97     struct bsddialog_formitem *items)
98 {
99 	bool loop, buttupdate, informwin = true;
100 	int i, input, output;
101 	struct myfield *mf;
102 
103 	curs_set(2);
104 	pos_form_cursor(form);
105 	loop = buttupdate = true;
106 	bs.curr = -1;
107 	form_driver(form, REQ_END_LINE);
108 	form_driver(form, REQ_END_LINE);
109 	mf = GETMYFIELD2(form);
110 	mf->pos = mf->len;
111 	while(loop) {
112 		if (buttupdate) {
113 			draw_buttons(widget, y, cols, bs, !informwin);
114 			wrefresh(widget);
115 			buttupdate = false;
116 		}
117 		wrefresh(formwin);
118 		input = getch();
119 		switch(input) {
120 		case KEY_ENTER:
121 		case 10: /* Enter */
122 			if (informwin)
123 				break;
124 			output = bs.value[bs.curr];
125 			if (output == BSDDIALOG_YESOK) {
126 				form_driver(form, REQ_NEXT_FIELD);
127 				form_driver(form, REQ_PREV_FIELD);
128 				for (i=0; i<nitems; i++) {
129 					mf = GETMYFIELD(cfield[i]);
130 					items[i].value = strdup(mf->buf);
131 				}
132 			}
133 			loop = false;
134 			break;
135 		case 27: /* Esc */
136 			output = BSDDIALOG_ESC;
137 			loop = false;
138 			break;
139 		case '\t': /* TAB */
140 			if (informwin) {
141 				bs.curr = 0;
142 				informwin = false;
143 				curs_set(0);
144 			} else {
145 				bs.curr++;
146 				informwin = bs.curr >= (int) bs.nbuttons ?
147 				    true : false;
148 				if (informwin) {
149 					curs_set(2);
150 					pos_form_cursor(form);
151 				}
152 			}
153 			buttupdate = true;
154 			break;
155 		case KEY_LEFT:
156 			if (informwin) {
157 				form_driver(form, REQ_PREV_CHAR);
158 				mf = GETMYFIELD2(form);
159 				if (mf->pos > 0)
160 					mf->pos -= 1;
161 			} else {
162 				if (bs.curr > 0) {
163 					bs.curr--;
164 					buttupdate = true;
165 				}
166 			}
167 			break;
168 		case KEY_RIGHT:
169 			if (informwin) {
170 				mf = GETMYFIELD2(form);
171 				if (mf->pos >= mf->len)
172 					break;
173 				mf->pos += 1;
174 				form_driver(form, REQ_NEXT_CHAR);
175 			} else {
176 				if (bs.curr < (int) bs.nbuttons - 1) {
177 					bs.curr++;
178 					buttupdate = true;
179 				}
180 			}
181 			break;
182 		case KEY_UP:
183 			if (nitems < 2)
184 				break;
185 			set_field_fore(current_field(form), t.form.fieldcolor);
186 			set_field_back(current_field(form), t.form.fieldcolor);
187 			form_driver(form, REQ_PREV_FIELD);
188 			form_driver(form, REQ_END_LINE);
189 			mf = GETMYFIELD2(form);
190 			mf->pos = mf->len;
191 			set_field_fore(current_field(form), t.form.f_fieldcolor);
192 			set_field_back(current_field(form), t.form.f_fieldcolor);
193 			break;
194 		case KEY_DOWN:
195 			if (nitems < 2)
196 				break;
197 			set_field_fore(current_field(form), t.form.fieldcolor);
198 			set_field_back(current_field(form), t.form.fieldcolor);
199 			form_driver(form, REQ_NEXT_FIELD);
200 			form_driver(form, REQ_END_LINE);
201 			mf = GETMYFIELD2(form);
202 			mf->pos = mf->len;
203 			set_field_fore(current_field(form), t.form.f_fieldcolor);
204 			set_field_back(current_field(form), t.form.f_fieldcolor);
205 			break;
206 		case KEY_BACKSPACE:
207 		case 127: /* Backspace */
208 			mf = GETMYFIELD2(form);
209 			if (mf->pos <= 0)
210 				break;
211 			form_driver(form, REQ_DEL_PREV);
212 			mf = GETMYFIELD2(form);
213 			mf->pos -= 1;
214 			shiftleft(mf);
215 			break;
216 		case KEY_DC:
217 			form_driver(form, REQ_DEL_CHAR);
218 			mf = GETMYFIELD2(form);
219 			if (mf->len-1 >= mf->pos)
220 				shiftleft(mf);
221 			break;
222 		case KEY_F(1):
223 			if (conf->hfile == NULL)
224 				break;
225 			if (f1help(conf) != 0)
226 				return BSDDIALOG_ERROR;
227 			/* No Break */
228 		case KEY_RESIZE:
229 			output = REDRAWFORM;
230 			loop = false;
231 			break;
232 		default:
233 			/*
234 			 * user input, add unicode chars to "public" buffer
235 			 */
236 			if (informwin) {
237 				mf = GETMYFIELD2(form);
238 				if (mf->secure)
239 					form_driver(form, mf->securech);
240 				else
241 					form_driver(form, input);
242 				insertch(mf, input);
243 			}
244 			else {
245 				for (i = 0; i < (int) bs.nbuttons; i++) {
246 					if (tolower(input) ==
247 					    tolower((bs.label[i])[0])) {
248 						output = bs.value[i];
249 						loop = false;
250 					}
251 				}
252 			}
253 			break;
254 		}
255 	}
256 
257 	curs_set(0);
258 
259 	return output;
260 }
261 
262 static void
form_autosize(struct bsddialog_conf * conf,int rows,int cols,int * h,int * w,char * text,int linelen,unsigned int * formheight,int nitems,struct buttons bs)263 form_autosize(struct bsddialog_conf *conf, int rows, int cols, int *h, int *w,
264     char *text, int linelen, unsigned int *formheight, int nitems,
265     struct buttons bs)
266 {
267 	int textrow, menusize;
268 
269 	textrow = text != NULL && strlen(text) > 0 ? 1 : 0;
270 
271 	if (cols == BSDDIALOG_AUTOSIZE) {
272 		*w = VBORDERS;
273 		/* buttons size */
274 		*w += bs.nbuttons * bs.sizebutton;
275 		*w += bs.nbuttons > 0 ? (bs.nbuttons-1) * t.button.space : 0;
276 		/* line size */
277 		*w = MAX(*w, linelen + 3);
278 		/*
279 		* avoid terminal overflow,
280 		* -1 fix false negative with big menu over the terminal and
281 		* autosize, for example "portconfig /usr/ports/www/apache24/".
282 		*/
283 		*w = MIN(*w, widget_max_width(conf)-1);
284 	}
285 
286 	if (rows == BSDDIALOG_AUTOSIZE) {
287 		*h = HBORDERS + 2 /* buttons */ + textrow;
288 
289 		if (*formheight == 0) {
290 			*h += nitems + 2;
291 			*h = MIN(*h, widget_max_height(conf));
292 			menusize = MIN(nitems + 2, *h - (HBORDERS + 2 + textrow));
293 			menusize -=2;
294 			*formheight = menusize < 0 ? 0 : menusize;
295 		}
296 		else /* h autosize with a fixed formheight */
297 			*h = *h + *formheight + 2;
298 
299 		/* avoid terminal overflow */
300 		*h = MIN(*h, widget_max_height(conf));
301 	}
302 	else {
303 		if (*formheight == 0)
304 			*formheight = MIN(rows-6-textrow, nitems);
305 	}
306 }
307 
308 static int
form_checksize(int rows,int cols,char * text,int formheight,int nitems,struct buttons bs)309 form_checksize(int rows, int cols, char *text, int formheight, int nitems,
310     struct buttons bs)
311 {
312 	int mincols, textrow, formrows;
313 
314 	mincols = VBORDERS;
315 	/* buttons */
316 	mincols += bs.nbuttons * bs.sizebutton;
317 	mincols += bs.nbuttons > 0 ? (bs.nbuttons-1) * t.button.space : 0;
318 	/* line, comment to permet some cols hidden */
319 	/* mincols = MAX(mincols, linelen); */
320 
321 	if (cols < mincols)
322 		RETURN_ERROR("Few cols, width < size buttons or "\
323 		    "labels + forms");
324 
325 	textrow = text != NULL && strlen(text) > 0 ? 1 : 0;
326 
327 	if (nitems > 0 && formheight == 0)
328 		RETURN_ERROR("fields > 0 but formheight == 0, probably "\
329 		    "terminal too small");
330 
331 	formrows = nitems > 0 ? 3 : 0;
332 	if (rows < 2  + 2 + formrows + textrow)
333 		RETURN_ERROR("Few lines for this menus");
334 
335 	return 0;
336 }
337 
338 int
bsddialog_form(struct bsddialog_conf * conf,char * text,int rows,int cols,unsigned int formheight,unsigned int nitems,struct bsddialog_formitem * items)339 bsddialog_form(struct bsddialog_conf *conf, char* text, int rows, int cols,
340     unsigned int formheight, unsigned int nitems,
341     struct bsddialog_formitem *items)
342 {
343 	WINDOW *widget, *formwin, *textpad, *shadow;
344 	int i, output, color, y, x, h, w, htextpad;
345 	FIELD **cfield;
346 	FORM *form;
347 	struct buttons bs;
348 	struct myfield *myfields;
349 	unsigned long maxline;
350 
351 	/* disable form scrolling like dialog */
352 	if (formheight < nitems)
353 		formheight = nitems;
354 
355 	maxline = 0;
356 	myfields = malloc(nitems * sizeof(struct myfield));
357 	cfield = calloc(nitems + 1, sizeof(FIELD*));
358 	for (i=0; i < (int)nitems; i++) {
359 		cfield[i] = new_field(1, items[i].fieldlen, items[i].yfield-1,
360 		    items[i].xfield-1, 0, 0);
361 		field_opts_off(cfield[i], O_STATIC);
362 		set_max_field(cfield[i], items[i].maxvaluelen);
363 		set_field_buffer(cfield[i], 0, items[i].init);
364 
365 		myfields[i].pos  = strlen(items[i].init);
366 		myfields[i].len  = strlen(items[i].init);
367 		myfields[i].size = items[i].maxvaluelen;
368 		myfields[i].buf  = malloc(myfields[i].size);
369 		memset(myfields[i].buf, 0, myfields[i].size);
370 		strcpy(myfields[i].buf, items[i].init);
371 		set_field_userptr(cfield[i], &myfields[i]);
372 
373 		field_opts_off(cfield[i], O_AUTOSKIP);
374 		field_opts_off(cfield[i], O_BLANK);
375 		/* field_opts_off(field[i], O_BS_OVERLOAD); */
376 
377 		if (ISFIELDHIDDEN(items[i])) {
378 			/* field_opts_off(field[i], O_PUBLIC); old hidden */
379 			myfields[i].secure = true;
380 			myfields[i].securech = ' ';
381 			if (conf->form.securech != '\0')
382 				myfields[i].securech = conf->form.securech;
383 		}
384 		else myfields[i].secure = false;
385 
386 		if (ISFIELDREADONLY(items[i])) {
387 			field_opts_off(cfield[i], O_EDIT);
388 			field_opts_off(cfield[i], O_ACTIVE);
389 			color = t.form.readonlycolor;
390 		} else {
391 			color = i == 0 ? t.form.f_fieldcolor : t.form.fieldcolor;
392 		}
393 		set_field_fore(cfield[i], color);
394 		set_field_back(cfield[i], color);
395 
396 		maxline = MAX(maxline, items[i].xlabel + strlen(items[i].label));
397 		maxline = MAX(maxline, items[i].xfield + items[i].fieldlen);
398 	}
399 	cfield[i] = NULL;
400 
401 	 /* disable focus with 1 item (inputbox or passwordbox) */
402 	if (formheight == 1 && nitems == 1 && strlen(items[0].label) == 0 &&
403 	    items[0].xfield == 1 ) {
404 		set_field_fore(cfield[0], t.widget.color);
405 		set_field_back(cfield[0], t.widget.color);
406 	}
407 
408 	get_buttons(conf, &bs, BUTTONLABEL(ok_label), BUTTONLABEL(extra_label),
409 	    BUTTONLABEL(cancel_label), BUTTONLABEL(help_label));
410 
411 	if (set_widget_size(conf, rows, cols, &h, &w) != 0)
412 		return BSDDIALOG_ERROR;
413 	form_autosize(conf, rows, cols, &h, &w, text, maxline, &formheight,
414 	    nitems, bs);
415 	if (form_checksize(h, w, text, formheight, nitems, bs) != 0)
416 		return BSDDIALOG_ERROR;
417 	if (set_widget_position(conf, &y, &x, h, w) != 0)
418 		return BSDDIALOG_ERROR;
419 
420 	if (new_widget_withtextpad(conf, &shadow, &widget, y, x, h, w, RAISED,
421 	    &textpad, &htextpad, text, true) != 0)
422 		return BSDDIALOG_ERROR;
423 
424 	prefresh(textpad, 0, 0, y + 1, x + 1 + t.text.hmargin,
425 	    y + h - formheight, x + 1 + w - t.text.hmargin);
426 
427 	formwin = new_boxed_window(conf, y + h - 3 - formheight -2, x +1,
428 	    formheight+2, w-2, LOWERED);
429 
430 	form = new_form(cfield);
431 	set_form_win(form, formwin);
432 	/* should be formheight */
433 	set_form_sub(form, derwin(formwin, nitems, w-4, 1, 1));
434 	post_form(form);
435 
436 	for (i=0; i < (int)nitems; i++)
437 		mvwaddstr(formwin, items[i].ylabel, items[i].xlabel, items[i].label);
438 
439 	wrefresh(formwin);
440 
441 	do {
442 		output = form_handler(conf, widget, h-2, w, bs, formwin, form,
443 		    cfield, nitems, items);
444 
445 		if(update_widget_withtextpad(conf, shadow, widget, h, w,
446 		    RAISED, textpad, &htextpad, text, true) != 0)
447 		return BSDDIALOG_ERROR;
448 
449 		draw_buttons(widget, h-2, w, bs, true);
450 		wrefresh(widget);
451 
452 		prefresh(textpad, 0, 0, y + 1, x + 1 + t.text.hmargin,
453 		    y + h - formheight, x + 1 + w - t.text.hmargin);
454 
455 		draw_borders(conf, formwin, formheight+2, w-2, LOWERED);
456 		/* wrefresh(formwin); */
457 	} while (output == REDRAWFORM);
458 
459 	unpost_form(form);
460 	free_form(form);
461 	for (i=0; i < (int)nitems; i++) {
462 		free_field(cfield[i]);
463 		free(myfields[i].buf);
464 	}
465 	free(cfield);
466 	free(myfields);
467 
468 	delwin(formwin);
469 	end_widget_withtextpad(conf, widget, h, w, textpad, shadow);
470 
471 	return output;
472 }
473