1 #include "url-mode.h"
2 
3 #include <stdlib.h>
4 #include <string.h>
5 #include <wctype.h>
6 #include <unistd.h>
7 
8 #include <sys/stat.h>
9 #include <fcntl.h>
10 
11 #define LOG_MODULE "url-mode"
12 #define LOG_ENABLE_DBG 0
13 #include "log.h"
14 #include "grid.h"
15 #include "render.h"
16 #include "selection.h"
17 #include "spawn.h"
18 #include "terminal.h"
19 #include "uri.h"
20 #include "util.h"
21 #include "xmalloc.h"
22 
23 static void url_destroy(struct url *url);
24 
25 static bool
execute_binding(struct seat * seat,struct terminal * term,enum bind_action_url action,uint32_t serial)26 execute_binding(struct seat *seat, struct terminal *term,
27                 enum bind_action_url action, uint32_t serial)
28 {
29     switch (action) {
30     case BIND_ACTION_URL_NONE:
31         return false;
32 
33     case BIND_ACTION_URL_CANCEL:
34         urls_reset(term);
35         return true;
36 
37     case BIND_ACTION_URL_TOGGLE_URL_ON_JUMP_LABEL:
38          term->urls_show_uri_on_jump_label = !term->urls_show_uri_on_jump_label;
39         render_refresh_urls(term);
40         return true;
41 
42     case BIND_ACTION_URL_COUNT:
43         return false;
44 
45     }
46     return true;
47 }
48 
49 static void
activate_url(struct seat * seat,struct terminal * term,const struct url * url)50 activate_url(struct seat *seat, struct terminal *term, const struct url *url)
51 {
52     char *url_string = NULL;
53 
54     char *scheme, *host, *path;
55     if (uri_parse(url->url, strlen(url->url), &scheme, NULL, NULL,
56                   &host, NULL, &path, NULL, NULL))
57     {
58         if (strcmp(scheme, "file") == 0 && hostname_is_localhost(host)) {
59             /*
60              * This is a file in *this* computer. Pass only the
61              * filename to the URL-launcher.
62              *
63              * I.e. strip the ‘file://user@host/’ prefix.
64              */
65             url_string = path;
66         } else
67             free(path);
68 
69         free(scheme);
70         free(host);
71     }
72 
73     if (url_string == NULL)
74         url_string = xstrdup(url->url);
75 
76     switch (url->action) {
77     case URL_ACTION_COPY:
78         if (text_to_clipboard(seat, term, url_string, seat->kbd.serial)) {
79             /* Now owned by our clipboard “manager” */
80             url_string = NULL;
81         }
82         break;
83 
84     case URL_ACTION_LAUNCH: {
85         size_t argc;
86         char **argv;
87 
88         int dev_null = open("/dev/null", O_RDWR);
89 
90         if (dev_null < 0) {
91             LOG_ERRNO("failed to open /dev/null");
92             break;
93         }
94 
95         if (spawn_expand_template(
96                 &term->conf->url.launch, 1,
97                 (const char *[]){"url"},
98                 (const char *[]){url_string},
99                 &argc, &argv))
100         {
101             spawn(term->reaper, term->cwd, argv, dev_null, dev_null, dev_null);
102 
103             for (size_t i = 0; i < argc; i++)
104                 free(argv[i]);
105             free(argv);
106         }
107 
108         close(dev_null);
109         break;
110     }
111     }
112 
113     free(url_string);
114 }
115 
116 void
urls_input(struct seat * seat,struct terminal * term,uint32_t key,xkb_keysym_t sym,xkb_mod_mask_t mods,xkb_mod_mask_t consumed,const xkb_keysym_t * raw_syms,size_t raw_count,uint32_t serial)117 urls_input(struct seat *seat, struct terminal *term, uint32_t key,
118            xkb_keysym_t sym, xkb_mod_mask_t mods, xkb_mod_mask_t consumed,
119            const xkb_keysym_t *raw_syms, size_t raw_count,
120            uint32_t serial)
121 {
122     /* Key bindings */
123     tll_foreach(seat->kbd.bindings.url, it) {
124         const struct key_binding *bind = &it->item;
125 
126         /* Match translated symbol */
127         if (bind->sym == sym &&
128             bind->mods == (mods & ~consumed))
129         {
130             execute_binding(seat, term, bind->action, serial);
131             return;
132         }
133 
134         if (bind->mods != mods)
135             continue;
136 
137         for (size_t i = 0; i < raw_count; i++) {
138             if (bind->sym == raw_syms[i]) {
139                 execute_binding(seat, term, bind->action, serial);
140                 return;
141             }
142         }
143 
144         /* Match raw key code */
145         tll_foreach(bind->key_codes, code) {
146             if (code->item == key) {
147                 execute_binding(seat, term, bind->action, serial);
148                 return;
149             }
150         }
151     }
152 
153     size_t seq_len = wcslen(term->url_keys);
154 
155     if (sym == XKB_KEY_BackSpace) {
156         if (seq_len > 0) {
157             term->url_keys[seq_len - 1] = L'\0';
158             render_refresh_urls(term);
159         }
160 
161         return;
162     }
163 
164     if (mods & ~consumed)
165         return;
166 
167     wchar_t wc = xkb_state_key_get_utf32(seat->kbd.xkb_state, key);
168 
169     /*
170      * Determine if this is a “valid” key. I.e. if there is an URL
171      * label with a key combo where this key is the next in
172      * sequence.
173      */
174 
175     bool is_valid = false;
176     const struct url *match = NULL;
177 
178     tll_foreach(term->urls, it) {
179         if (it->item.key == NULL)
180             continue;
181 
182         const struct url *url = &it->item;
183         const size_t key_len = wcslen(it->item.key);
184 
185         if (key_len >= seq_len + 1 &&
186             wcsncasecmp(url->key, term->url_keys, seq_len) == 0 &&
187             towlower(url->key[seq_len]) == towlower(wc))
188         {
189             is_valid = true;
190             if (key_len == seq_len + 1) {
191                 match = url;
192                 break;
193             }
194         }
195     }
196 
197     if (match) {
198         activate_url(seat, term, match);
199         urls_reset(term);
200     }
201 
202     else if (is_valid) {
203         xassert(seq_len + 1 <= ALEN(term->url_keys));
204         term->url_keys[seq_len] = wc;
205         render_refresh_urls(term);
206     }
207 }
208 
209 static int
wccmp(const void * _a,const void * _b)210 wccmp(const void *_a, const void *_b)
211 {
212     const wchar_t *a = _a;
213     const wchar_t *b = _b;
214     return *a - *b;
215 }
216 
217 static void
auto_detected(const struct terminal * term,enum url_action action,url_list_t * urls)218 auto_detected(const struct terminal *term, enum url_action action,
219               url_list_t *urls)
220 {
221     const struct config *conf = term->conf;
222 
223     const wchar_t *uri_characters = conf->url.uri_characters;
224     if (uri_characters == NULL)
225         return;
226 
227     const size_t uri_characters_count = wcslen(uri_characters);
228     if (uri_characters_count == 0)
229         return;
230 
231     size_t max_prot_len = conf->url.max_prot_len;
232     wchar_t proto_chars[max_prot_len];
233     struct coord proto_start[max_prot_len];
234     size_t proto_char_count = 0;
235 
236     enum {
237         STATE_PROTOCOL,
238         STATE_URL,
239     } state = STATE_PROTOCOL;
240 
241     struct coord start = {-1, -1};
242     wchar_t url[term->cols * term->rows + 1];
243     size_t len = 0;
244 
245     ssize_t parenthesis = 0;
246     ssize_t brackets = 0;
247     ssize_t ltgts = 0;
248 
249     for (int r = 0; r < term->rows; r++) {
250         const struct row *row = grid_row_in_view(term->grid, r);
251 
252         for (int c = 0; c < term->cols; c++) {
253             const struct cell *cell = &row->cells[c];
254             wchar_t wc = cell->wc;
255 
256             switch (state) {
257             case STATE_PROTOCOL:
258                 for (size_t i = 0; i < max_prot_len - 1; i++) {
259                     proto_chars[i] = proto_chars[i + 1];
260                     proto_start[i] = proto_start[i + 1];
261                 }
262 
263                 if (proto_char_count >= max_prot_len)
264                     proto_char_count = max_prot_len - 1;
265 
266                 proto_chars[max_prot_len - 1] = wc;
267                 proto_start[max_prot_len - 1] = (struct coord){c, r};
268                 proto_char_count++;
269 
270                 for (size_t i = 0; i < conf->url.prot_count; i++) {
271                     size_t prot_len = wcslen(conf->url.protocols[i]);
272 
273                     if (proto_char_count < prot_len)
274                         continue;
275 
276                     const wchar_t *proto = &proto_chars[max_prot_len - prot_len];
277 
278                     if (wcsncasecmp(conf->url.protocols[i], proto, prot_len) == 0) {
279                         state = STATE_URL;
280                         start = proto_start[max_prot_len - prot_len];
281 
282                         wcsncpy(url, proto, prot_len);
283                         len = prot_len;
284 
285                         parenthesis = brackets = ltgts = 0;
286                         break;
287                     }
288                 }
289                 break;
290 
291             case STATE_URL: {
292                 const wchar_t *match = bsearch(
293                     &wc,
294                     uri_characters,
295                     uri_characters_count,
296                     sizeof(uri_characters[0]),
297                     &wccmp);
298 
299                 bool emit_url = false;
300 
301                 if (match == NULL) {
302                     /*
303                      * Character is not a valid URI character. Emit
304                      * the URL we’ve collected so far, *without*
305                      * including _this_ character.
306                      */
307                     emit_url = true;
308                 } else {
309                     xassert(*match == wc);
310 
311                     switch (wc) {
312                     default:
313                         url[len++] = wc;
314                         break;
315 
316                     case L'(':
317                         parenthesis++;
318                         url[len++] = wc;
319                         break;
320 
321                     case L'[':
322                         brackets++;
323                         url[len++] = wc;
324                         break;
325 
326                     case L'<':
327                         ltgts++;
328                         url[len++] = wc;
329                         break;
330 
331                     case L')':
332                         if (--parenthesis < 0)
333                             emit_url = true;
334                         else
335                             url[len++] = wc;
336                         break;
337 
338                     case L']':
339                         if (--brackets < 0)
340                             emit_url = true;
341                         else
342                             url[len++] = wc;
343                         break;
344 
345                     case L'>':
346                         if (--ltgts < 0)
347                             emit_url = true;
348                         else
349                             url[len++] = wc;
350                         break;
351                     }
352                 }
353 
354                 if (c >= term->cols - 1 && row->linebreak) {
355                     /*
356                      * Endpoint is inclusive, and we’ll be subtracting
357                      * 1 from the column when emitting the URL.
358                      */
359                     c++;
360                     emit_url = true;
361                 }
362 
363                 if (emit_url) {
364                     struct coord end = {c, r};
365 
366                     if (--end.col < 0) {
367                         end.row--;
368                         end.col = term->cols - 1;
369                     }
370 
371                     /* Heuristic to remove trailing characters that
372                      * are valid URL characters, but typically not at
373                      * the end of the URL */
374                     bool done = false;
375                     do {
376                         switch (url[len - 1]) {
377                         case L'.': case L',': case L':': case L';': case L'?':
378                         case L'!': case L'"': case L'\'': case L'%':
379                             len--;
380                             end.col--;
381                             if (end.col < 0) {
382                                 end.row--;
383                                 end.col = term->cols - 1;
384                             }
385                             break;
386 
387                         default:
388                             done = true;
389                             break;
390                         }
391                     } while (!done);
392 
393                     url[len] = L'\0';
394 
395                     start.row += term->grid->view;
396                     end.row += term->grid->view;
397 
398                     size_t chars = wcstombs(NULL, url, 0);
399                     if (chars != (size_t)-1) {
400                         char *url_utf8 = xmalloc((chars + 1) * sizeof(wchar_t));
401                         wcstombs(url_utf8, url, chars + 1);
402 
403                         tll_push_back(
404                             *urls,
405                             ((struct url){
406                                 .id = (uint64_t)rand() << 32 | rand(),
407                                 .url = url_utf8,
408                                 .start = start,
409                                 .end = end,
410                                 .action = action,
411                                 .osc8 = false}));
412                     }
413 
414                     state = STATE_PROTOCOL;
415                     len = 0;
416                     parenthesis = brackets = ltgts = 0;
417                 }
418                 break;
419             }
420             }
421         }
422     }
423 }
424 
425 static void
osc8_uris(const struct terminal * term,enum url_action action,url_list_t * urls)426 osc8_uris(const struct terminal *term, enum url_action action, url_list_t *urls)
427 {
428     bool dont_touch_url_attr = false;
429 
430     switch (term->conf->url.osc8_underline) {
431     case OSC8_UNDERLINE_URL_MODE:
432         dont_touch_url_attr = false;
433         break;
434 
435     case OSC8_UNDERLINE_ALWAYS:
436         dont_touch_url_attr = true;
437         break;
438     }
439 
440     for (int r = 0; r < term->rows; r++) {
441         const struct row *row = grid_row_in_view(term->grid, r);
442         const struct row_data *extra = row->extra;
443 
444        if (extra == NULL)
445             continue;
446 
447        for (size_t i = 0; i < extra->uri_ranges.count; i++) {
448            const struct row_uri_range *range = &extra->uri_ranges.v[i];
449 
450            struct coord start = {
451                .col = range->start,
452                .row = r + term->grid->view,
453            };
454            struct coord end = {
455                .col = range->end,
456                .row = r + term->grid->view,
457            };
458            tll_push_back(
459                *urls,
460                ((struct url){
461                    .id = range->id,
462                    .url = xstrdup(range->uri),
463                    .start = start,
464                    .end = end,
465                    .action = action,
466                    .url_mode_dont_change_url_attr = dont_touch_url_attr,
467                    .osc8 = true}));
468        }
469     }
470 }
471 
472 static void
remove_overlapping(url_list_t * urls,int cols)473 remove_overlapping(url_list_t *urls, int cols)
474 {
475     tll_foreach(*urls, outer) {
476         tll_foreach(*urls, inner) {
477             if (outer == inner)
478                 continue;
479 
480             const struct url *out = &outer->item;
481             const struct url *in = &inner->item;
482 
483             uint64_t in_start = in->start.row * cols + in->start.col;
484             uint64_t in_end = in->end.row * cols + in->end.col;
485 
486             uint64_t out_start = out->start.row * cols + out->start.col;
487             uint64_t out_end = out->end.row * cols + out->end.col;
488 
489             if ((in_start <= out_start && in_end >= out_start) ||
490                 (in_start <= out_end && in_end >= out_end) ||
491                 (in_start >= out_start && in_end <= out_end))
492             {
493                 /*
494                  * OSC-8 URLs can’t overlap with each
495                  * other.
496                  *
497                  * Similarly, auto-detected URLs cannot overlap with
498                  * each other.
499                  *
500                  * But OSC-8 URLs can overlap with auto-detected ones.
501                  */
502                 xassert(in->osc8 || out->osc8);
503 
504                 if (in->osc8)
505                     outer->item.duplicate = true;
506                 else
507                     inner->item.duplicate = true;
508             }
509         }
510     }
511 
512     tll_foreach(*urls, it) {
513         if (it->item.duplicate) {
514             url_destroy(&it->item);
515             tll_remove(*urls, it);
516         }
517     }
518 }
519 
520 void
urls_collect(const struct terminal * term,enum url_action action,url_list_t * urls)521 urls_collect(const struct terminal *term, enum url_action action, url_list_t *urls)
522 {
523     xassert(tll_length(term->urls) == 0);
524     osc8_uris(term, action, urls);
525     auto_detected(term, action, urls);
526     remove_overlapping(urls, term->grid->num_cols);
527 }
528 
529 static int
wcscmp_qsort_wrapper(const void * _a,const void * _b)530 wcscmp_qsort_wrapper(const void *_a, const void *_b)
531 {
532     const wchar_t *a = *(const wchar_t **)_a;
533     const wchar_t *b = *(const wchar_t **)_b;
534     return wcscmp(a, b);
535 }
536 
537 static void
generate_key_combos(const struct config * conf,size_t count,wchar_t * combos[static count])538 generate_key_combos(const struct config *conf,
539                     size_t count, wchar_t *combos[static count])
540 {
541     const wchar_t *alphabet = conf->url.label_letters;
542     const size_t alphabet_len = wcslen(alphabet);
543 
544     size_t hints_count = 1;
545     wchar_t **hints = xmalloc(hints_count * sizeof(hints[0]));
546 
547     hints[0] = xwcsdup(L"");
548 
549     size_t offset = 0;
550     do {
551         const wchar_t *prefix = hints[offset++];
552         const size_t prefix_len = wcslen(prefix);
553 
554         hints = xrealloc(hints, (hints_count + alphabet_len) * sizeof(hints[0]));
555 
556         const wchar_t *wc = &alphabet[0];
557         for (size_t i = 0; i < alphabet_len; i++, wc++) {
558             wchar_t *hint = xmalloc((prefix_len + 1 + 1) * sizeof(wchar_t));
559             hints[hints_count + i] = hint;
560 
561             /* Will be reversed later */
562             hint[0] = *wc;
563             wcscpy(&hint[1], prefix);
564         }
565         hints_count += alphabet_len;
566     } while (hints_count - offset < count);
567 
568     xassert(hints_count - offset >= count);
569 
570     /* Copy slice of ‘hints’ array to the caller provided array */
571     for (size_t i = 0; i < hints_count; i++) {
572         if (i >= offset && i < offset + count)
573             combos[i - offset] = hints[i];
574         else
575             free(hints[i]);
576     }
577     free(hints);
578 
579     /* Sorting is a kind of shuffle, since we’re sorting on the
580      * *reversed* strings */
581     qsort(combos, count, sizeof(wchar_t *), &wcscmp_qsort_wrapper);
582 
583     /* Reverse all strings */
584     for (size_t i = 0; i < count; i++) {
585         const size_t len = wcslen(combos[i]);
586         for (size_t j = 0; j < len / 2; j++) {
587             wchar_t tmp = combos[i][j];
588             combos[i][j] = combos[i][len - j - 1];
589             combos[i][len - j - 1] = tmp;
590         }
591     }
592 }
593 
594 void
urls_assign_key_combos(const struct config * conf,url_list_t * urls)595 urls_assign_key_combos(const struct config *conf, url_list_t *urls)
596 {
597     const size_t count = tll_length(*urls);
598     if (count == 0)
599         return;
600 
601     uint64_t seen_ids[count];
602     wchar_t *combos[count];
603     generate_key_combos(conf, count, combos);
604 
605     size_t combo_idx = 0;
606     size_t id_idx = 0;
607 
608     tll_foreach(*urls, it) {
609         bool id_already_seen = false;
610 
611         for (size_t i = 0; i < id_idx; i++) {
612             if (it->item.id == seen_ids[i]) {
613                 id_already_seen = true;
614                 break;
615             }
616         }
617 
618         if (id_already_seen)
619             continue;
620         seen_ids[id_idx++] = it->item.id;
621 
622         /*
623          * Scan previous URLs, and check if *this* URL matches any of
624          * them; if so, re-use the *same* key combo.
625          */
626         bool url_already_seen = false;
627         tll_foreach(*urls, it2) {
628             if (&it->item == &it2->item)
629                 break;
630 
631             if (strcmp(it->item.url, it2->item.url) == 0) {
632                 it->item.key = xwcsdup(it2->item.key);
633                 url_already_seen = true;
634                 break;
635             }
636         }
637 
638         if (!url_already_seen)
639             it->item.key = combos[combo_idx++];
640     }
641 
642     /* Free combos we didn’t use up */
643     for (size_t i = combo_idx; i < count; i++)
644         free(combos[i]);
645 
646 #if defined(_DEBUG) && LOG_ENABLE_DBG
647     tll_foreach(*urls, it) {
648         if (it->item.key == NULL)
649             continue;
650 
651         char key[32];
652         wcstombs(key, it->item.key, sizeof(key) - 1);
653         LOG_DBG("URL: %s (%s)", it->item.url, key);
654     }
655 #endif
656 }
657 
658 static void
tag_cells_for_url(struct terminal * term,const struct url * url,bool value)659 tag_cells_for_url(struct terminal *term, const struct url *url, bool value)
660 {
661     if (url->url_mode_dont_change_url_attr)
662         return;
663 
664     const struct coord *start = &url->start;
665     const struct coord *end = &url->end;
666 
667     size_t end_r = end->row & (term->grid->num_rows - 1);
668 
669     size_t r = start->row & (term->grid->num_rows - 1);
670     size_t c = start->col;
671 
672     struct row *row = term->grid->rows[r];
673     row->dirty = true;
674 
675     while (true) {
676         struct cell *cell = &row->cells[c];
677         cell->attrs.url = value;
678         cell->attrs.clean = 0;
679 
680         if (r == end_r && c == end->col)
681             break;
682 
683         if (++c >= term->cols) {
684             r = (r + 1) & (term->grid->num_rows - 1);
685             c = 0;
686 
687             row = term->grid->rows[r];
688             if (row == NULL) {
689                 /* Un-allocated scrollback. This most likely means a
690                  * runaway OSC-8 URL. */
691                 break;
692             }
693             row->dirty = true;
694         }
695     }
696 }
697 
698 void
urls_render(struct terminal * term)699 urls_render(struct terminal *term)
700 {
701     struct wl_window *win = term->window;
702 
703     if (tll_length(win->term->urls) == 0)
704         return;
705 
706     xassert(tll_length(win->urls) == 0);
707     tll_foreach(win->term->urls, it) {
708         struct wl_url url = {.url = &it->item};
709         wayl_win_subsurface_new(win, &url.surf);
710 
711         tll_push_back(win->urls, url);
712         tag_cells_for_url(term, &it->item, true);
713     }
714 
715     /* Dirty the last cursor, to ensure it is erased */
716     {
717         struct row *cursor_row = term->render.last_cursor.row;
718         if (cursor_row != NULL) {
719             struct cell *cell = &cursor_row->cells[term->render.last_cursor.col];
720             cell->attrs.clean = 0;
721             cursor_row->dirty = true;
722         }
723     }
724     term->render.last_cursor.row = NULL;
725 
726     /* Clear scroll damage, to ensure we don’t apply it twice (once on
727      * the snapshot:ed grid, and then later again on the real grid) */
728     tll_free(term->grid->scroll_damage);
729 
730     /* Damage the entire view, to ensure a full screen redraw, both
731      * now, when entering URL mode, and later, when exiting it. */
732     term_damage_view(term);
733 
734     /* Snapshot the current grid */
735     term->url_grid_snapshot = grid_snapshot(term->grid);
736 
737     render_refresh_urls(term);
738     render_refresh(term);
739 }
740 
741 static void
url_destroy(struct url * url)742 url_destroy(struct url *url)
743 {
744     free(url->url);
745     free(url->key);
746 }
747 
748 void
urls_reset(struct terminal * term)749 urls_reset(struct terminal *term)
750 {
751     if (likely(tll_length(term->urls) == 0)) {
752         xassert(term->url_grid_snapshot == NULL);
753         return;
754     }
755 
756     grid_free(term->url_grid_snapshot);
757     free(term->url_grid_snapshot);
758     term->url_grid_snapshot = NULL;
759 
760     /*
761      * Make sure “last cursor” doesn’t point to a row in the just
762      * free:d snapshot grid.
763      *
764      * Note that it will still be erased properly (if hasn’t already),
765      * since we marked the cell as dirty *before* taking the grid
766      * snapshot.
767      */
768     term->render.last_cursor.row = NULL;
769 
770     if (term->window != NULL) {
771         tll_foreach(term->window->urls, it) {
772             wayl_win_subsurface_destroy(&it->item.surf);
773             tll_remove(term->window->urls, it);
774         }
775     }
776 
777     tll_foreach(term->urls, it) {
778         tag_cells_for_url(term, &it->item, false);
779         url_destroy(&it->item);
780         tll_remove(term->urls, it);
781     }
782 
783     term->urls_show_uri_on_jump_label = false;
784     memset(term->url_keys, 0, sizeof(term->url_keys));
785 
786     render_refresh(term);
787 }
788