1 /*
2 * form.c
3 * vim: expandtab:ts=4:sts=4:sw=4
4 *
5 * Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
6 *
7 * This file is part of Profanity.
8 *
9 * Profanity is free software: you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by
11 * the Free Software Foundation, either version 3 of the License, or
12 * (at your option) any later version.
13 *
14 * Profanity is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU General Public License for more details.
18 *
19 * You should have received a copy of the GNU General Public License
20 * along with Profanity. If not, see <https://www.gnu.org/licenses/>.
21 *
22 * In addition, as a special exception, the copyright holders give permission to
23 * link the code of portions of this program with the OpenSSL library under
24 * certain conditions as described in each individual source file, and
25 * distribute linked combinations including the two.
26 *
27 * You must obey the GNU General Public License in all respects for all of the
28 * code used other than OpenSSL. If you modify file(s) with this exception, you
29 * may extend this exception to your version of the file(s), but you are not
30 * obligated to do so. If you do not wish to do so, delete this exception
31 * statement from your version. If you delete this exception statement from all
32 * source files in the program, then also delete it here.
33 *
34 */
35
36 #include "config.h"
37
38 #include <string.h>
39 #include <stdlib.h>
40
41 #ifdef HAVE_LIBMESODE
42 #include <mesode.h>
43 #endif
44
45 #ifdef HAVE_LIBSTROPHE
46 #include <strophe.h>
47 #endif
48
49 #include <glib.h>
50
51 #include "log.h"
52 #include "xmpp/xmpp.h"
53 #include "xmpp/stanza.h"
54 #include "xmpp/connection.h"
55
56 static gboolean
_is_valid_form_element(xmpp_stanza_t * stanza)57 _is_valid_form_element(xmpp_stanza_t* stanza)
58 {
59 const char* name = xmpp_stanza_get_name(stanza);
60 if (g_strcmp0(name, STANZA_NAME_X) != 0) {
61 log_error("Error parsing form, root element not <x/>.");
62 return FALSE;
63 }
64
65 const char* ns = xmpp_stanza_get_ns(stanza);
66 if (g_strcmp0(ns, STANZA_NS_DATA) != 0) {
67 log_error("Error parsing form, namespace not %s.", STANZA_NS_DATA);
68 return FALSE;
69 }
70
71 const char* type = xmpp_stanza_get_type(stanza);
72 if ((g_strcmp0(type, "form") != 0) && (g_strcmp0(type, "submit") != 0) && (g_strcmp0(type, "cancel") != 0) && (g_strcmp0(type, "result") != 0)) {
73 log_error("Error parsing form, unknown type.");
74 return FALSE;
75 }
76
77 return TRUE;
78 }
79
80 static DataForm*
_form_new(void)81 _form_new(void)
82 {
83 DataForm* form = calloc(1, sizeof(DataForm));
84
85 return form;
86 }
87
88 static FormField*
_field_new(void)89 _field_new(void)
90 {
91 FormField* field = calloc(1, sizeof(FormField));
92
93 return field;
94 }
95
96 static char*
_get_property(xmpp_stanza_t * const stanza,const char * const property)97 _get_property(xmpp_stanza_t* const stanza, const char* const property)
98 {
99 char* result = NULL;
100 xmpp_ctx_t* ctx = connection_get_ctx();
101
102 xmpp_stanza_t* child = xmpp_stanza_get_child_by_name(stanza, property);
103 if (child) {
104 char* child_text = xmpp_stanza_get_text(child);
105 if (child_text) {
106 result = strdup(child_text);
107 xmpp_free(ctx, child_text);
108 }
109 }
110
111 return result;
112 }
113
114 static char*
_get_attr(xmpp_stanza_t * const stanza,const char * const attr)115 _get_attr(xmpp_stanza_t* const stanza, const char* const attr)
116 {
117 const char* result = xmpp_stanza_get_attribute(stanza, attr);
118 if (result) {
119 return strdup(result);
120 } else {
121 return NULL;
122 }
123 }
124
125 static gboolean
_is_required(xmpp_stanza_t * const stanza)126 _is_required(xmpp_stanza_t* const stanza)
127 {
128 xmpp_stanza_t* child = xmpp_stanza_get_child_by_name(stanza, "required");
129 if (child) {
130 return TRUE;
131 } else {
132 return FALSE;
133 }
134 }
135
136 static form_field_type_t
_get_field_type(const char * const type)137 _get_field_type(const char* const type)
138 {
139 if (g_strcmp0(type, "hidden") == 0) {
140 return FIELD_HIDDEN;
141 }
142 if (g_strcmp0(type, "text-single") == 0) {
143 return FIELD_TEXT_SINGLE;
144 }
145 if (g_strcmp0(type, "text-private") == 0) {
146 return FIELD_TEXT_PRIVATE;
147 }
148 if (g_strcmp0(type, "text-multi") == 0) {
149 return FIELD_TEXT_MULTI;
150 }
151 if (g_strcmp0(type, "boolean") == 0) {
152 return FIELD_BOOLEAN;
153 }
154 if (g_strcmp0(type, "list-single") == 0) {
155 return FIELD_LIST_SINGLE;
156 }
157 if (g_strcmp0(type, "list-multi") == 0) {
158 return FIELD_LIST_MULTI;
159 }
160 if (g_strcmp0(type, "jid-single") == 0) {
161 return FIELD_JID_SINGLE;
162 }
163 if (g_strcmp0(type, "jid-multi") == 0) {
164 return FIELD_JID_MULTI;
165 }
166 if (g_strcmp0(type, "fixed") == 0) {
167 return FIELD_FIXED;
168 }
169 return FIELD_UNKNOWN;
170 }
171
172 DataForm*
form_create(xmpp_stanza_t * const form_stanza)173 form_create(xmpp_stanza_t* const form_stanza)
174 {
175 xmpp_ctx_t* ctx = connection_get_ctx();
176
177 if (!_is_valid_form_element(form_stanza)) {
178 return NULL;
179 }
180
181 DataForm* form = _form_new();
182 form->type = _get_attr(form_stanza, "type");
183 form->title = _get_property(form_stanza, "title");
184 form->instructions = _get_property(form_stanza, "instructions");
185 form->var_to_tag = g_hash_table_new_full(g_str_hash, g_str_equal, free, free);
186 form->tag_to_var = g_hash_table_new_full(g_str_hash, g_str_equal, free, free);
187 form->tag_ac = autocomplete_new();
188 form->modified = FALSE;
189
190 int tag_num = 1;
191
192 // get fields
193 xmpp_stanza_t* form_child = xmpp_stanza_get_children(form_stanza);
194 while (form_child) {
195 const char* child_name = xmpp_stanza_get_name(form_child);
196 if (g_strcmp0(child_name, "field") == 0) {
197 xmpp_stanza_t* field_stanza = form_child;
198
199 FormField* field = _field_new();
200 field->label = _get_attr(field_stanza, "label");
201 field->type = _get_attr(field_stanza, "type");
202 field->type_t = _get_field_type(field->type);
203 field->value_ac = autocomplete_new();
204
205 field->var = _get_attr(field_stanza, "var");
206
207 if (field->type_t != FIELD_HIDDEN && field->var) {
208 GString* tag = g_string_new("");
209 g_string_printf(tag, "field%d", tag_num++);
210 g_hash_table_insert(form->var_to_tag, strdup(field->var), strdup(tag->str));
211 g_hash_table_insert(form->tag_to_var, strdup(tag->str), strdup(field->var));
212 autocomplete_add(form->tag_ac, tag->str);
213 g_string_free(tag, TRUE);
214 }
215
216 field->description = _get_property(field_stanza, "desc");
217 field->required = _is_required(field_stanza);
218
219 // handle repeated field children
220 xmpp_stanza_t* field_child = xmpp_stanza_get_children(field_stanza);
221 int value_index = 1;
222 while (field_child) {
223 child_name = xmpp_stanza_get_name(field_child);
224
225 // handle values
226 if (g_strcmp0(child_name, "value") == 0) {
227 char* value = xmpp_stanza_get_text(field_child);
228 if (value) {
229 field->values = g_slist_append(field->values, strdup(value));
230
231 if (field->type_t == FIELD_TEXT_MULTI) {
232 GString* ac_val = g_string_new("");
233 g_string_printf(ac_val, "val%d", value_index++);
234 autocomplete_add(field->value_ac, ac_val->str);
235 g_string_free(ac_val, TRUE);
236 }
237 if (field->type_t == FIELD_JID_MULTI) {
238 autocomplete_add(field->value_ac, value);
239 }
240
241 xmpp_free(ctx, value);
242 }
243
244 // handle options
245 } else if (g_strcmp0(child_name, "option") == 0) {
246 FormOption* option = malloc(sizeof(FormOption));
247 option->label = _get_attr(field_child, "label");
248 option->value = _get_property(field_child, "value");
249
250 if ((field->type_t == FIELD_LIST_SINGLE) || (field->type_t == FIELD_LIST_MULTI)) {
251 autocomplete_add(field->value_ac, option->value);
252 }
253
254 field->options = g_slist_append(field->options, option);
255 }
256
257 field_child = xmpp_stanza_get_next(field_child);
258 }
259
260 form->fields = g_slist_append(form->fields, field);
261 }
262
263 form_child = xmpp_stanza_get_next(form_child);
264 }
265
266 return form;
267 }
268
269 xmpp_stanza_t*
form_create_submission(DataForm * form)270 form_create_submission(DataForm* form)
271 {
272 xmpp_ctx_t* ctx = connection_get_ctx();
273
274 xmpp_stanza_t* x = xmpp_stanza_new(ctx);
275 xmpp_stanza_set_name(x, STANZA_NAME_X);
276 xmpp_stanza_set_ns(x, STANZA_NS_DATA);
277 xmpp_stanza_set_type(x, "submit");
278
279 GSList* curr_field = form->fields;
280 while (curr_field) {
281 FormField* field = curr_field->data;
282
283 if (field->type_t != FIELD_FIXED) {
284 xmpp_stanza_t* field_stanza = xmpp_stanza_new(ctx);
285 xmpp_stanza_set_name(field_stanza, "field");
286 xmpp_stanza_set_attribute(field_stanza, "var", field->var);
287
288 xmpp_stanza_t* value_stanza = NULL;
289 GSList* curr_value = NULL;
290
291 switch (field->type_t) {
292 case FIELD_HIDDEN:
293 case FIELD_TEXT_SINGLE:
294 case FIELD_TEXT_PRIVATE:
295 case FIELD_BOOLEAN:
296 case FIELD_LIST_SINGLE:
297 case FIELD_JID_SINGLE:
298 value_stanza = xmpp_stanza_new(ctx);
299 xmpp_stanza_set_name(value_stanza, "value");
300 if (field->values) {
301 if (field->values->data) {
302 xmpp_stanza_t* text_stanza = xmpp_stanza_new(ctx);
303 xmpp_stanza_set_text(text_stanza, field->values->data);
304 xmpp_stanza_add_child(value_stanza, text_stanza);
305 xmpp_stanza_release(text_stanza);
306 }
307 }
308 xmpp_stanza_add_child(field_stanza, value_stanza);
309 xmpp_stanza_release(value_stanza);
310
311 break;
312
313 case FIELD_TEXT_MULTI:
314 case FIELD_LIST_MULTI:
315 case FIELD_JID_MULTI:
316 curr_value = field->values;
317 while (curr_value) {
318 char* value = curr_value->data;
319
320 value_stanza = xmpp_stanza_new(ctx);
321 xmpp_stanza_set_name(value_stanza, "value");
322 if (value) {
323 xmpp_stanza_t* text_stanza = xmpp_stanza_new(ctx);
324 xmpp_stanza_set_text(text_stanza, value);
325 xmpp_stanza_add_child(value_stanza, text_stanza);
326 xmpp_stanza_release(text_stanza);
327 }
328
329 xmpp_stanza_add_child(field_stanza, value_stanza);
330 xmpp_stanza_release(value_stanza);
331
332 curr_value = g_slist_next(curr_value);
333 }
334 break;
335 case FIELD_FIXED:
336 default:
337 break;
338 }
339
340 xmpp_stanza_add_child(x, field_stanza);
341 xmpp_stanza_release(field_stanza);
342 }
343
344 curr_field = g_slist_next(curr_field);
345 }
346
347 return x;
348 }
349
350 static void
_free_option(FormOption * option)351 _free_option(FormOption* option)
352 {
353 if (option) {
354 free(option->label);
355 free(option->value);
356 free(option);
357 }
358 }
359
360 static void
_free_field(FormField * field)361 _free_field(FormField* field)
362 {
363 if (field) {
364 free(field->label);
365 free(field->type);
366 free(field->var);
367 free(field->description);
368 g_slist_free_full(field->values, free);
369 g_slist_free_full(field->options, (GDestroyNotify)_free_option);
370 autocomplete_free(field->value_ac);
371 free(field);
372 }
373 }
374
375 void
form_destroy(DataForm * form)376 form_destroy(DataForm* form)
377 {
378 if (form) {
379 free(form->type);
380 free(form->title);
381 free(form->instructions);
382 g_slist_free_full(form->fields, (GDestroyNotify)_free_field);
383 g_hash_table_destroy(form->var_to_tag);
384 g_hash_table_destroy(form->tag_to_var);
385 autocomplete_free(form->tag_ac);
386 free(form);
387 }
388 }
389
390 static int
_field_compare_by_var(FormField * a,FormField * b)391 _field_compare_by_var(FormField* a, FormField* b)
392 {
393 return g_strcmp0(a->var, b->var);
394 }
395
396 GSList*
form_get_non_form_type_fields_sorted(DataForm * form)397 form_get_non_form_type_fields_sorted(DataForm* form)
398 {
399 GSList* sorted = NULL;
400 GSList* curr = form->fields;
401 while (curr) {
402 FormField* field = curr->data;
403 if (g_strcmp0(field->var, "FORM_TYPE") != 0) {
404 sorted = g_slist_insert_sorted(sorted, field, (GCompareFunc)_field_compare_by_var);
405 }
406 curr = g_slist_next(curr);
407 }
408
409 return sorted;
410 }
411
412 GSList*
form_get_field_values_sorted(FormField * field)413 form_get_field_values_sorted(FormField* field)
414 {
415 GSList* sorted = NULL;
416 GSList* curr = field->values;
417 while (curr) {
418 char* value = curr->data;
419 if (value) {
420 sorted = g_slist_insert_sorted(sorted, value, (GCompareFunc)g_strcmp0);
421 }
422 curr = g_slist_next(curr);
423 }
424
425 return sorted;
426 }
427
428 char*
form_get_form_type_field(DataForm * form)429 form_get_form_type_field(DataForm* form)
430 {
431 GSList* curr = form->fields;
432 while (curr) {
433 FormField* field = curr->data;
434 if (g_strcmp0(field->var, "FORM_TYPE") == 0) {
435 return field->values->data;
436 }
437 curr = g_slist_next(curr);
438 }
439
440 return NULL;
441 }
442
443 gboolean
form_tag_exists(DataForm * form,const char * const tag)444 form_tag_exists(DataForm* form, const char* const tag)
445 {
446 GList* tags = g_hash_table_get_keys(form->tag_to_var);
447 GList* curr = tags;
448 while (curr) {
449 if (g_strcmp0(curr->data, tag) == 0) {
450 g_list_free(tags);
451 return TRUE;
452 }
453 curr = g_list_next(curr);
454 }
455
456 g_list_free(tags);
457 return FALSE;
458 }
459
460 form_field_type_t
form_get_field_type(DataForm * form,const char * const tag)461 form_get_field_type(DataForm* form, const char* const tag)
462 {
463 char* var = g_hash_table_lookup(form->tag_to_var, tag);
464 if (var) {
465 GSList* curr = form->fields;
466 while (curr) {
467 FormField* field = curr->data;
468 if (g_strcmp0(field->var, var) == 0) {
469 return field->type_t;
470 }
471 curr = g_slist_next(curr);
472 }
473 }
474 return FIELD_UNKNOWN;
475 }
476
477 void
form_set_value(DataForm * form,const char * const tag,char * value)478 form_set_value(DataForm* form, const char* const tag, char* value)
479 {
480 char* var = g_hash_table_lookup(form->tag_to_var, tag);
481 if (var) {
482 GSList* curr = form->fields;
483 while (curr) {
484 FormField* field = curr->data;
485 if (g_strcmp0(field->var, var) == 0) {
486 if (g_slist_length(field->values) == 0) {
487 field->values = g_slist_append(field->values, strdup(value));
488 form->modified = TRUE;
489 return;
490 } else if (g_slist_length(field->values) == 1) {
491 free(field->values->data);
492 field->values->data = strdup(value);
493 form->modified = TRUE;
494 return;
495 }
496 }
497 curr = g_slist_next(curr);
498 }
499 }
500 }
501
502 void
form_add_value(DataForm * form,const char * const tag,char * value)503 form_add_value(DataForm* form, const char* const tag, char* value)
504 {
505 char* var = g_hash_table_lookup(form->tag_to_var, tag);
506 if (var) {
507 GSList* curr = form->fields;
508 while (curr) {
509 FormField* field = curr->data;
510 if (g_strcmp0(field->var, var) == 0) {
511 field->values = g_slist_append(field->values, strdup(value));
512 if (field->type_t == FIELD_TEXT_MULTI) {
513 int total = g_slist_length(field->values);
514 GString* value_index = g_string_new("");
515 g_string_printf(value_index, "val%d", total);
516 autocomplete_add(field->value_ac, value_index->str);
517 g_string_free(value_index, TRUE);
518 }
519 form->modified = TRUE;
520 return;
521 }
522 curr = g_slist_next(curr);
523 }
524 }
525 }
526
527 gboolean
form_add_unique_value(DataForm * form,const char * const tag,char * value)528 form_add_unique_value(DataForm* form, const char* const tag, char* value)
529 {
530 char* var = g_hash_table_lookup(form->tag_to_var, tag);
531 if (var) {
532 GSList* curr = form->fields;
533 while (curr) {
534 FormField* field = curr->data;
535 if (g_strcmp0(field->var, var) == 0) {
536 GSList* curr_value = field->values;
537 while (curr_value) {
538 if (g_strcmp0(curr_value->data, value) == 0) {
539 return FALSE;
540 }
541 curr_value = g_slist_next(curr_value);
542 }
543
544 field->values = g_slist_append(field->values, strdup(value));
545 if (field->type_t == FIELD_JID_MULTI) {
546 autocomplete_add(field->value_ac, value);
547 }
548 form->modified = TRUE;
549 return TRUE;
550 }
551 curr = g_slist_next(curr);
552 }
553 }
554
555 return FALSE;
556 }
557
558 gboolean
form_remove_value(DataForm * form,const char * const tag,char * value)559 form_remove_value(DataForm* form, const char* const tag, char* value)
560 {
561 char* var = g_hash_table_lookup(form->tag_to_var, tag);
562 if (var) {
563 GSList* curr = form->fields;
564 while (curr) {
565 FormField* field = curr->data;
566 if (g_strcmp0(field->var, var) == 0) {
567 GSList* found = g_slist_find_custom(field->values, value, (GCompareFunc)g_strcmp0);
568 if (found) {
569 free(found->data);
570 found->data = NULL;
571 field->values = g_slist_delete_link(field->values, found);
572 if (field->type_t == FIELD_JID_MULTI) {
573 autocomplete_remove(field->value_ac, value);
574 }
575 form->modified = TRUE;
576 return TRUE;
577 } else {
578 return FALSE;
579 }
580 }
581 curr = g_slist_next(curr);
582 }
583 }
584
585 return FALSE;
586 }
587
588 gboolean
form_remove_text_multi_value(DataForm * form,const char * const tag,int index)589 form_remove_text_multi_value(DataForm* form, const char* const tag, int index)
590 {
591 index--;
592 char* var = g_hash_table_lookup(form->tag_to_var, tag);
593 if (var) {
594 GSList* curr = form->fields;
595 while (curr) {
596 FormField* field = curr->data;
597 if (g_strcmp0(field->var, var) == 0) {
598 GSList* item = g_slist_nth(field->values, index);
599 if (item) {
600 free(item->data);
601 item->data = NULL;
602 field->values = g_slist_delete_link(field->values, item);
603 GString* value_index = g_string_new("");
604 g_string_printf(value_index, "val%d", index + 1);
605 autocomplete_remove(field->value_ac, value_index->str);
606 g_string_free(value_index, TRUE);
607 form->modified = TRUE;
608 return TRUE;
609 } else {
610 return FALSE;
611 }
612 }
613 curr = g_slist_next(curr);
614 }
615 }
616
617 return FALSE;
618 }
619
620 int
form_get_value_count(DataForm * form,const char * const tag)621 form_get_value_count(DataForm* form, const char* const tag)
622 {
623 char* var = g_hash_table_lookup(form->tag_to_var, tag);
624 if (var) {
625 GSList* curr = form->fields;
626 while (curr) {
627 FormField* field = curr->data;
628 if (g_strcmp0(field->var, var) == 0) {
629 if ((g_slist_length(field->values) == 1) && (field->values->data == NULL)) {
630 return 0;
631 } else {
632 return g_slist_length(field->values);
633 }
634 }
635 curr = g_slist_next(curr);
636 }
637 }
638
639 return 0;
640 }
641
642 gboolean
form_field_contains_option(DataForm * form,const char * const tag,char * value)643 form_field_contains_option(DataForm* form, const char* const tag, char* value)
644 {
645 char* var = g_hash_table_lookup(form->tag_to_var, tag);
646 if (var) {
647 GSList* curr = form->fields;
648 while (curr) {
649 FormField* field = curr->data;
650 if (g_strcmp0(field->var, var) == 0) {
651 GSList* curr_option = field->options;
652 while (curr_option) {
653 FormOption* option = curr_option->data;
654 if (g_strcmp0(option->value, value) == 0) {
655 return TRUE;
656 }
657 curr_option = g_slist_next(curr_option);
658 }
659 }
660 curr = g_slist_next(curr);
661 }
662 }
663
664 return FALSE;
665 }
666
667 FormField*
form_get_field_by_tag(DataForm * form,const char * const tag)668 form_get_field_by_tag(DataForm* form, const char* const tag)
669 {
670 char* var = g_hash_table_lookup(form->tag_to_var, tag);
671 if (var) {
672 GSList* curr = form->fields;
673 while (curr) {
674 FormField* field = curr->data;
675 if (g_strcmp0(field->var, var) == 0) {
676 return field;
677 }
678 curr = g_slist_next(curr);
679 }
680 }
681 return NULL;
682 }
683
684 Autocomplete
form_get_value_ac(DataForm * form,const char * const tag)685 form_get_value_ac(DataForm* form, const char* const tag)
686 {
687 char* var = g_hash_table_lookup(form->tag_to_var, tag);
688 if (var) {
689 GSList* curr = form->fields;
690 while (curr) {
691 FormField* field = curr->data;
692 if (g_strcmp0(field->var, var) == 0) {
693 return field->value_ac;
694 }
695 curr = g_slist_next(curr);
696 }
697 }
698 return NULL;
699 }
700
701 void
form_reset_autocompleters(DataForm * form)702 form_reset_autocompleters(DataForm* form)
703 {
704 autocomplete_reset(form->tag_ac);
705 GSList* curr_field = form->fields;
706 while (curr_field) {
707 FormField* field = curr_field->data;
708 autocomplete_reset(field->value_ac);
709 curr_field = g_slist_next(curr_field);
710 }
711 }
712