1 /*
2  * Copyright (C) 1997 Michael R. Elkins <me@cs.hmc.edu>
3  * Copyright (C) 2012 Michael R. Elkins <me@sigpipe.org>
4  *
5  *     This program is free software; you can redistribute it and/or modify
6  *     it under the terms of the GNU General Public License as published by
7  *     the Free Software Foundation; either version 2 of the License, or
8  *     (at your option) any later version.
9  *
10  *     This program is distributed in the hope that it will be useful,
11  *     but WITHOUT ANY WARRANTY; without even the implied warranty of
12  *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  *     GNU General Public License for more details.
14  *
15  *     You should have received a copy of the GNU General Public License
16  *     along with this program; if not, write to the Free Software
17  *     Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
18  *
19  *
20  * Created:       Thu Dec  4 02:13:11 PST 1997
21  * Last Modified: Tue Jul  4 11:23:49 CEST 2000
22  */
23 
24 #ifdef USE_SLANG
25 #include <slcurses.h>
26 #include <sys/file.h>
27 #else
28 #ifdef HAVE_NCURSES_H
29 #include <ncurses.h>
30 #else
31 #include <curses.h>
32 #endif
33 #endif /* USE_SLANG */
34 
35 #include <pwd.h>
36 #include <string.h>
37 #include <unistd.h>
38 #include <ctype.h>
39 #include <stdio.h>
40 #include <stdlib.h>
41 #include <fcntl.h>
42 #include <sys/stat.h>
43 
44 #ifdef HAVE_REGEX_H
45 #include <regex.h>
46 #else
47 #include <rx/rxposix.h>
48 #endif
49 
50 #include "quote.h"
51 
52 #define DEFAULT_REGEXP "(((http|https|ftp|gopher)|mailto):(//)?[^ <>\"\t]*|(www|ftp)[0-9]?\\.[-a-z0-9.]+)[^ .,;\t\n\r<\">\\):]?[^, <>\"\t]*[^ .,;\t\n\r<\">\\):]"
53 #define DEFAULT_COMMAND "firefox %s"
54 #define SYSTEM_INITFILE "/usr/local/etc/urlview/system.urlview"
55 
56 #define OFFSET 2
57 #define PAGELEN (LINES - 1 - OFFSET)
58 #define URLSIZE 128
59 
60 enum
61 {
62   FULL = 1,
63   INDEX,
64   MOTION
65 };
66 
67 extern int mutt_enter_string (unsigned char *buf, size_t buflen, int y, int x,
68 		int flags);
69 
search_forward(char * search,int urlcount,char ** url,int * redraw,int * current,int * top)70 void search_forward (char *search, int urlcount, char **url, int *redraw, int *current, int *top)
71 {
72   regex_t rx;
73   int i, j;
74 
75   if (strlen(search) == 0 || *search == '\n')
76   {
77     move (LINES - 1, 0);
78     clrtoeol ();
79     *redraw = MOTION;
80   }
81   else
82   {
83     if ( (j = regcomp (&rx, search, REG_EXTENDED | REG_ICASE | REG_NEWLINE)) )
84     {
85       regerror (j, &rx, search, sizeof (search));
86       regfree (&rx);
87       puts (search);
88     }
89     for (i = *current + 1; i < urlcount; i++)
90     {
91       if (regexec (&rx, url[i], 0, NULL, 0) == 0)
92       {
93 	*current = i;
94 	if (*current < *top || *current > *top + PAGELEN -1)
95 	{
96 	  *top = *current - *current % PAGELEN - 1;
97 	}
98 	i = urlcount;
99       }
100     }
101     move (LINES - 1, 0);
102     clrtoeol ();
103     *redraw = INDEX;
104     regfree (&rx);
105   }
106 }
107 
search_backward(char * search,int urlcount,char ** url,int * redraw,int * current,int * top)108 void search_backward (char *search, int urlcount, char **url, int *redraw, int *current, int *top)
109 {
110   regex_t rx;
111   int i, j;
112 
113   (void)urlcount; /*unused*/
114   if (strlen(search) == 0 || *search == '\n')
115   {
116     move (LINES - 1, 0);
117     clrtoeol ();
118     *redraw = MOTION;
119   }
120   else
121   {
122     if ((j = regcomp (&rx, search, REG_EXTENDED | REG_ICASE | REG_NEWLINE)))
123     {
124       regerror (j, &rx, search, sizeof (search));
125       regfree (&rx);
126       puts (search);
127     }
128     for (i = *current - 1; i >= 0; i--)
129     {
130       if (regexec (&rx, url[i], 0, NULL, 0) == 0)
131       {
132 	*current = i;
133 	if (*current < *top || *current > *top + PAGELEN -1)
134 	{
135 	  *top = *current - *current % PAGELEN - 1;
136 	  if (*top < 0)
137 	    *top = 0;
138 	}
139 	i = 0;
140       }
141     }
142     move (LINES - 1, 0);
143     clrtoeol ();
144     *redraw = INDEX;
145     regfree (&rx);
146   }
147 }
148 
main(int argc,char ** argv)149 int main (int argc, char **argv)
150 {
151   struct passwd *pw;
152   struct stat stat_buf;
153 #ifndef USE_SLANG
154   SCREEN *scr;
155 #endif
156   FILE *fp;
157   regex_t rx;
158   regmatch_t match;
159   char buf[1024];
160   char command[1024];
161   char regexp[1024];
162   char search[1024];
163   char scratch[1024];
164   char wrapchoice[1024];
165   char **url;
166   int urlsize = URLSIZE;
167   char *pc;
168   char *wc;
169   size_t len;
170   size_t offset;
171   int i;
172   int top = 0;
173   int startline = 0;
174   int oldcurrent = 0;
175   int current = 0;
176   int done = 0;
177   int quitonlaunch = 0;
178   int redraw = FULL;
179   int urlcount = 0;
180   int urlcheck = 0;
181   int reopen_tty = 0;
182   int is_filter = 0;
183 
184   int expert = 0;
185 
186   int skip_browser = 0;
187 
188   int menu_wrapping = 0;
189 
190   if (argc == 1)
191     is_filter = 1;
192 
193   strncpy (regexp, DEFAULT_REGEXP, sizeof (regexp) - 1);
194   strncpy (command, DEFAULT_COMMAND, sizeof (command) - 1);
195 
196   /*** read the initialization file ***/
197 
198   pw = getpwuid (getuid ());
199   snprintf (buf, sizeof (buf), "%s/.urlview", pw->pw_dir);
200 
201   /*** Check for users rc-file ***/
202   if (stat (buf,&stat_buf) == -1)
203       snprintf (buf, sizeof(buf), "%s", SYSTEM_INITFILE);
204 
205   if ((fp = fopen (buf, "r")))
206   {
207     while (fgets (buf, sizeof (buf), fp) != NULL)
208     {
209       if (buf[0] == '#' || buf[0] == '\n')
210 	continue;
211       if (strncmp ("REGEXP", buf, 6) == 0 && isspace (buf[6]))
212       {
213 	pc = buf + 6;
214 	while (isspace (*pc))
215 	  pc++;
216 	wc = regexp;
217 	while (*pc && *pc != '\n')
218 	{
219 	  if (*pc == '\\')
220 	  {
221 	    pc++;
222 	    switch (*pc)
223 	    {
224 	      case 'n':
225 		*wc++ = '\n';
226 		break;
227 	      case 'r':
228 		*wc++ = '\r';
229 		break;
230 	      case 't':
231 		*wc++ = '\t';
232 		break;
233 	      case 'f':
234 		*wc++ = '\f';
235 		break;
236 	      default:
237 		*wc++ = '\\';
238 		*wc++ = *pc;
239 		break;
240 	    }
241 	  }
242 	  else
243 	    *wc++ = *pc;
244 	  pc++;
245 	}
246 	*wc = 0;
247       }
248       else if (strncmp ("COMMAND", buf, 7) == 0 && isspace (buf[7]))
249       {
250 	pc = buf + 7;
251 	while (isspace (*pc))
252 	  pc++;
253 	pc[ strlen (pc) - 1 ] = 0; /* kill the trailing newline */
254 	strncpy (command, pc, sizeof (command) - 1);
255 	command[sizeof (command) - 1] = 0;
256 	skip_browser = 1;
257       }
258       else if (strncmp ("WRAP", buf, 4) == 0 && isspace (buf[4]))
259       {
260 	pc = buf + 4;
261 	while (isspace (*pc))
262 	  pc++;
263 	pc[ strlen (pc) - 1 ] = 0; /* kill the trailing newline */
264 	strncpy (wrapchoice, pc, sizeof (wrapchoice) - 1);
265 	wrapchoice[sizeof (wrapchoice) - 1] = 0;
266 	/* checking the value, case insensitive */
267 	if (strcasecmp("YES", wrapchoice) == 0)
268 		menu_wrapping = 1;
269 	else if (!strcasecmp("NO", wrapchoice) == 0)
270 	{
271 		printf ("Unknown value for WRAP: %s. Valid values are: YES, NO\n", wrapchoice);
272 		exit (1);
273 	}
274       }
275       else if (strncmp ("BROWSER", buf, 7) == 0 && isspace (buf[7]))
276       {
277 	skip_browser = 0;
278       }
279       else if (strcmp ("EXPERT\n", buf) == 0)
280       {
281 	expert = 1;
282       }
283 	  else if (strcmp ("QUITONLAUNCH\n", buf) == 0)
284 	  {
285 	quitonlaunch = 1;
286 	  }
287       else
288       {
289 	printf ("Unknown command: %s", buf);
290 	exit (1);
291       }
292     }
293     fclose (fp);
294   }
295 
296   /* Only use the $BROWSER environment variable if
297    * (a) no COMMAND in rc file or
298    * (b) BROWSER in rc file.
299    * If both COMMAND and BROWSER are in the rc file, then the option used
300    * last counts.
301    */
302   if (!skip_browser) {
303     pc = getenv("BROWSER");
304     if (pc)
305     {
306         if (strlen(pc) > 0) {
307           strncpy (command, pc, sizeof (command) - 1);
308         } else {
309           printf("Your $BROWSER is zero-length.  Falling back to COMMAND.");
310         }
311     }
312   }
313 
314   if (!expert && strchr (command, '\''))
315   {
316     puts ("\n\
317 ERROR: Your $BROWSER contains a single\n\
318 quote (') character. This is most likely\n\
319 in error; please read the manual page\n\
320 for details. If you really want to use\n\
321 this command, please put the word EXPERT\n\
322 into a line of its own in your \n\
323 ~/.urlview file.\n\
324 ");
325     exit (1);
326   }
327 
328   /*** compile the regexp ***/
329 
330   if ((i = regcomp (&rx, regexp, REG_EXTENDED | REG_ICASE | REG_NEWLINE)))
331   {
332     regerror (i, &rx, buf, sizeof (buf));
333     regfree (&rx);
334     puts (buf);
335     exit (1);
336   }
337 
338   /*** find matching patterns ***/
339 
340   if ((url = (char **) malloc (urlsize * sizeof (char *))) == NULL)
341   {
342     printf ("Couldn't allocate memory for url list\n");
343     exit (1);
344   }
345 
346   for (; is_filter || argv[optind]; optind++)
347   {
348     if (is_filter || strcmp ("-", argv[optind]) == 0)
349     {
350       fp = stdin;
351       reopen_tty = 1;
352     }
353 	else if (!is_filter && argv[optind][0] == '-') {
354 	  startline = atoi(argv[optind]+1);
355 	  current = -1;
356 	  continue;
357 	}
358     else if (!(fp = fopen (argv[optind], "r")))
359     {
360       perror (argv[optind]);
361       continue;
362     }
363 
364     while (fgets (buf, sizeof (buf) - 1, fp) != NULL)
365     {
366 	  --startline;
367       offset = 0;
368       while (regexec (&rx, buf + offset, 1, &match, offset ? REG_NOTBOL : 0) == 0)
369       {
370 	len = match.rm_eo - match.rm_so;
371         if (urlcount >= urlsize)
372 	{
373 	  void *urltmp;
374 	  urltmp = realloc ((void *) url, (urlsize + URLSIZE) * sizeof (char *));
375 	  if (urltmp == NULL)
376 	  {
377 	    printf ("Couldn't allocate memory for additional urls, "
378 		    "only first %d displayed\n", urlsize);
379 	    goto got_urls;
380 	  }
381 	  else
382 	  {
383 	    urlsize += URLSIZE;
384 	    url = (char **) urltmp;
385 	  }
386 	}
387 	url[urlcount] = malloc (len + 1);
388 	memcpy (url[urlcount], buf + match.rm_so + offset, len);
389 	url[urlcount][len] = 0;
390 	for (urlcheck=0; urlcheck < urlcount; urlcheck++)
391 	{
392 	  if(strcasecmp(url[urlcount],url[urlcheck])==0)
393 	  {
394 	    urlcount--;
395 	    break;
396 	  }
397 	}
398 	if (current < 0 && startline <= 0)
399 	  current = urlcount;
400 	urlcount++;
401 	offset += match.rm_eo;
402       }
403     }
404   got_urls:
405     fclose (fp);
406     if (is_filter)
407       break;
408   }
409 
410   regfree (&rx);
411 
412   if (!urlcount)
413   {
414     puts ("No URLs found.");
415     exit (1);
416   }
417 
418   if (current < 0)
419     current = urlcount - 1;
420 
421   /*** present the URLs to the user ***/
422 
423 #ifdef USE_SLANG
424   if (reopen_tty) {
425     SLang_TT_Read_FD = open ("/dev/tty", O_RDONLY);
426     if(SLang_TT_Read_FD < 0) {
427 	    perror("Can't open /dev/tty");
428 	    exit(1);
429     }
430   initscr ();
431 #else
432   /* if we piped a file we can't use initscr() because it assumes `stdin' */
433   fp = reopen_tty ? fopen ("/dev/tty", "r") : stdin;
434   if(fp == NULL) {
435 	  perror("Can't open /dev/tty");
436 	  exit(1);
437   }
438   scr = newterm (NULL, stdout, fp);
439   set_term (scr);
440 #endif
441 
442   cbreak ();
443   noecho ();
444 #ifdef HAVE_CURS_SET
445   curs_set (1);
446 #endif
447   keypad (stdscr, TRUE);
448 
449   top = current - PAGELEN / 2;
450   if (top < 0)
451     top = 0;
452 
453   while (!done)
454   {
455     if (redraw == FULL)
456     {
457       move (0, 0);
458       clrtobot ();
459       standout ();
460       printw ("UrlView %s: (%d matches) Press Q or Ctrl-C to Quit!", VERSION, urlcount);
461       standend ();
462       redraw = INDEX;
463     }
464 
465     if (redraw == INDEX)
466     {
467       for (i = top; i < urlcount && i < top + PAGELEN; i++)
468       {
469 	mvaddstr (i - top + OFFSET, 0, "   ");
470 	snprintf (buf, sizeof (buf), "%4d ", i + 1);
471 	addstr (buf);
472 	addstr (url[i]);
473 	clrtoeol ();
474       }
475       clrtobot ();
476     }
477     else if (redraw == MOTION)
478       mvaddstr (oldcurrent - top + OFFSET, 0, "  ");
479 
480     standout ();
481     mvaddstr (current - top + OFFSET, 0, "->");
482     standend ();
483 
484     oldcurrent = current;
485 
486     switch (i = getch ())
487     {
488       case 'q':
489       case 'x':
490       case 'h':
491 	done = 1;
492 	break;
493       case KEY_DOWN:
494       case 'j':
495 	if (current < urlcount - 1)
496 	{
497 	  current++;
498 	  if (current >= top + PAGELEN)
499 	  {
500 	    top++;
501 	    redraw = INDEX;
502 	  }
503 	  else
504 	    redraw = MOTION;
505 	}
506 	else
507 	{
508 	  if(menu_wrapping)
509         current = 0;
510 	  if (current < top)
511 	  {
512 	    top = current - current % PAGELEN;
513 		redraw = INDEX;
514 	  }
515 	  else
516 	    redraw = MOTION;
517 	}
518 	break;
519       case KEY_UP:
520       case 'k':
521 	if (current)
522 	{
523 	  current--;
524 	  if (current < top)
525 	  {
526 	    top--;
527 	    redraw = INDEX;
528 	  }
529 	  else
530 	    redraw = MOTION;
531 	}
532 	else
533     {
534       if(menu_wrapping)
535         current = urlcount - 1;
536 	  if (current > top + PAGELEN - 1)
537 	  {
538 	    top = current - current % PAGELEN;
539 	    redraw = INDEX;
540 	  }
541 	  else
542 	    redraw = MOTION;
543 	}
544 	break;
545       case KEY_HOME:
546       case '=':
547 	if (top != 0)
548 	{
549 	  top  = 0;
550 	  redraw = INDEX;
551 	}
552 	else
553 	  redraw = MOTION;
554 	current = 0;
555 	break;
556       case KEY_END:
557       case '*':
558       case 'G':
559 	current = urlcount - 1;
560 	if (current >= top + PAGELEN)
561 	{
562 	  top = current - PAGELEN + 1;
563 	  redraw = INDEX;
564 	}
565 	else
566 	  redraw = MOTION;
567 	break;
568       case KEY_NPAGE:
569       case '\006':
570 	if (top + PAGELEN < urlcount)
571 	{
572 	  current = top = top + PAGELEN;
573 	  redraw = INDEX;
574 	}
575 	else
576 	{
577 	  current = urlcount - 1;
578 	  redraw = INDEX;
579 	}
580 	break;
581       case KEY_PPAGE:
582       case '\002':
583 	if (top)
584 	{
585 	  top -= PAGELEN;
586 	  if (top < 0)
587 	    top = 0;
588 	  if (current >= top + PAGELEN)
589 	  {
590 	    current = top + PAGELEN - 1;
591 	    if (current < 0)
592 	      current = 0;
593 	  }
594 	  redraw = INDEX;
595 	}
596 	else
597 	{
598 	  current = 0;
599 	  redraw = INDEX;
600 	}
601 	break;
602       case '\n':
603       case '\r':
604       case KEY_ENTER:
605       case ' ':
606 	strncpy (buf, url[current], sizeof (buf));
607 	buf[sizeof (buf) - 1] = 0;
608 	mvaddstr (LINES - 1, 0, "URL: ");
609 	if (mutt_enter_string ((unsigned char *)buf, sizeof (buf), LINES - 1, 5, 0) == 0 && buf[0])
610 	{
611 	  char *part, *tmpbuf;
612 
613 	  free (url[current]);
614 	  url[current] = strdup (buf);
615 	  endwin ();
616 
617 	  tmpbuf = strdup(command);
618 	  part = strtok(tmpbuf, ":");
619 	  do {
620               quote (scratch, sizeof (scratch), url[current]);
621 	      if (strstr (part, "%s"))
622 		  snprintf (buf, sizeof (buf), part, scratch);
623 	      else
624 		  snprintf (buf, sizeof (buf), "%s %s", part, scratch);
625 	      printf ("Executing: %s...\n", buf);
626 	      fflush (stdout);
627 	      if (system (buf) == 0)
628 		  break;
629 	  } while
630 	      ((part = strtok(NULL, ":")) != NULL);
631 	  free(tmpbuf);
632 	}
633 	done = quitonlaunch;
634 	move (LINES - 1, 0);
635 	clrtoeol ();
636 	break;
637       case '0':
638       case '1':
639       case '2':
640       case '3':
641       case '4':
642       case '5':
643       case '6':
644       case '7':
645       case '8':
646       case '9':
647 	buf[0] = i; buf[1] = 0;
648 	mvaddstr (LINES - 1, 0, "Jump to url: ");
649 	if (mutt_enter_string ((unsigned char *)buf, sizeof (buf), LINES - 1, 13, 0) == 0 && buf[0])
650 	{
651 	  i = atoi (buf);
652 	  if (i < 1 || i > urlcount)
653 	  {
654 	    mvaddstr (LINES - 1, 0, "No such url number!");
655 	    clrtoeol ();
656 	  }
657 	  else
658 	  {
659 	    move (LINES - 1, 0);
660 	    clrtoeol ();
661 	    current = i - 1;
662 	    redraw = MOTION;
663 	    if (current < top || current > top + PAGELEN - 1)
664 	    {
665 	      top = current - current % PAGELEN;
666 	      redraw = INDEX;
667 	    }
668 	  }
669 	}
670 	break;
671       case '\f':
672 	clearok (stdscr, TRUE);
673 	redraw = FULL;
674 	break;
675       case '/':
676 	mvaddstr (LINES - 1, 0, "Search forwards for string: ");
677 	if (mutt_enter_string ((unsigned char *)search, sizeof (search), LINES - 1, 28, 0) == 0)
678 	  search_forward (search, urlcount, url, &redraw, &current, &top);
679 	break;
680       case '?':
681 	mvaddstr (LINES - 1, 0, "Search backwards for string: ");
682 	if (mutt_enter_string ((unsigned char *)search, sizeof (search), LINES - 1, 29, 0) == 0)
683 	  search_backward (search, urlcount, url, &redraw, &current, &top);
684         break;
685       case 'n':
686 	search_forward (search, urlcount, url, &redraw, &current, &top);
687 	break;
688       case 'N':
689 	search_backward (search, urlcount, url, &redraw, &current, &top);
690 	break;
691       default:
692 	break;
693     }
694   }
695 
696   endwin ();
697   exit (0);
698 }
699