/* X-Chat * Copyright (C) 1998 Peter Zelezny. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <ctype.h> #include "hexchat.h" #include "hexchatc.h" #include "cfgfiles.h" #include "fe.h" #include "tree.h" #include "url.h" #ifdef HAVE_STRINGS_H #include <strings.h> #endif void *url_tree = NULL; GTree *url_btree = NULL; static gboolean regex_match (const GRegex *re, const char *word, int *start, int *end); static const GRegex *re_url (void); static const GRegex *re_url_no_scheme (void); static const GRegex *re_host (void); static const GRegex *re_host6 (void); static const GRegex *re_email (void); static const GRegex *re_nick (void); static const GRegex *re_channel (void); static const GRegex *re_path (void); static gboolean match_nick (const char *word, int *start, int *end); static gboolean match_channel (const char *word, int *start, int *end); static gboolean match_email (const char *word, int *start, int *end); static gboolean match_url (const char *word, int *start, int *end); static gboolean match_host (const char *word, int *start, int *end); static gboolean match_host6 (const char *word, int *start, int *end); static gboolean match_path (const char *word, int *start, int *end); static int url_free (char *url, void *data) { g_free (url); return TRUE; } void url_clear (void) { tree_foreach (url_tree, (tree_traverse_func *)url_free, NULL); tree_destroy (url_tree); url_tree = NULL; g_tree_destroy (url_btree); url_btree = NULL; } static int url_save_cb (char *url, FILE *fd) { fprintf (fd, "%s\n", url); return TRUE; } void url_save_tree (const char *fname, const char *mode, gboolean fullpath) { FILE *fd; if (fullpath) fd = hexchat_fopen_file (fname, mode, XOF_FULLPATH); else fd = hexchat_fopen_file (fname, mode, 0); if (fd == NULL) return; tree_foreach (url_tree, (tree_traverse_func *)url_save_cb, fd); fclose (fd); } static void url_save_node (char* url) { FILE *fd; /* open <config>/url.log in append mode */ fd = hexchat_fopen_file ("url.log", "a", 0); if (fd == NULL) { return; } fprintf (fd, "%s\n", url); fclose (fd); } static int url_find (char *urltext) { return (g_tree_lookup_extended (url_btree, urltext, NULL, NULL)); } static void url_add (char *urltext, int len) { char *data; int size; /* we don't need any URLs if we have neither URL grabbing nor URL logging enabled */ if (!prefs.hex_url_grabber && !prefs.hex_url_logging) { return; } data = g_strndup (urltext, len); if (data[len - 1] == '.') /* chop trailing dot */ { len--; data[len] = 0; } /* chop trailing ) but only if there's no counterpart */ if (data[len - 1] == ')' && strchr (data, '(') == NULL) { data[len - 1] = 0; } if (prefs.hex_url_logging) { url_save_node (data); } /* the URL is saved already, only continue if we need the URL grabber too */ if (!prefs.hex_url_grabber) { g_free (data); return; } if (!url_tree) { url_tree = tree_new ((tree_cmp_func *)strcasecmp, NULL); url_btree = g_tree_new ((GCompareFunc)strcasecmp); } if (url_find (data)) { g_free (data); return; } size = tree_size (url_tree); /* 0 is unlimited */ if (prefs.hex_url_grabber_limit > 0 && size >= prefs.hex_url_grabber_limit) { /* the loop is necessary to handle having the limit lowered while HexChat is running */ size -= prefs.hex_url_grabber_limit; for(; size > 0; size--) { char *pos; pos = tree_remove_at_pos (url_tree, 0); g_tree_remove (url_btree, pos); g_free (pos); } } tree_append (url_tree, data); g_tree_insert (url_btree, data, GINT_TO_POINTER (tree_size (url_tree) - 1)); fe_url_add (data); } /* check if a word is clickable. This is called on mouse motion events, so keep it FAST! This new version was found to be almost 3x faster than 2.4.4 release. */ static int laststart = 0; static int lastend = 0; static int lasttype = 0; #define NICKPRE "~+!@%&" #define CHANPRE "#&!+" int url_check_word (const char *word) { struct { gboolean (*match) (const char *word, int *start, int *end); int type; } m[] = { { match_url, WORD_URL }, { match_email, WORD_EMAIL }, { match_channel, WORD_CHANNEL }, { match_host6, WORD_HOST6 }, { match_host, WORD_HOST }, { match_path, WORD_PATH }, { match_nick, WORD_NICK }, { NULL, 0} }; int i; laststart = lastend = lasttype = 0; for (i = 0; m[i].match; i++) if (m[i].match (word, &laststart, &lastend)) { lasttype = m[i].type; return lasttype; } return 0; } static gboolean match_nick (const char *word, int *start, int *end) { const server *serv = current_sess->server; const char *nick_prefixes = serv ? serv->nick_prefixes : NICKPRE; char *str; if (!regex_match (re_nick (), word, start, end)) return FALSE; /* ignore matches with prefixes that the server doesn't use */ if (strchr (NICKPRE, word[*start]) && !strchr (nick_prefixes, word[*start])) return FALSE; /* nick prefix is not part of the matched word */ if (strchr (nick_prefixes, word[*start])) (*start)++; str = g_strndup (&word[*start], *end - *start); if (!userlist_find (current_sess, str)) { g_free (str); return FALSE; } g_free (str); return TRUE; } static gboolean match_channel (const char *word, int *start, int *end) { const server *serv = current_sess->server; const char *chan_prefixes = serv ? serv->chantypes : CHANPRE; const char *nick_prefixes = serv ? serv->nick_prefixes : NICKPRE; if (!regex_match (re_channel (), word, start, end)) return FALSE; /* Check for +#channel (for example whois output) */ if (strchr (nick_prefixes, word[*start]) != NULL && strchr (chan_prefixes, word[*start + 1]) != NULL) { (*start)++; return TRUE; } /* Or just #channel */ else if (strchr (chan_prefixes, word[*start]) != NULL) return TRUE; return FALSE; } static gboolean match_email (const char *word, int *start, int *end) { return regex_match (re_email (), word, start, end); } static gboolean match_url (const char *word, int *start, int *end) { if (regex_match (re_url (), word, start, end)) return TRUE; return regex_match (re_url_no_scheme (), word, start, end); } static gboolean match_host (const char *word, int *start, int *end) { return regex_match (re_host (), word, start, end); } static gboolean match_host6 (const char *word, int *start, int *end) { return regex_match (re_host6 (), word, start, end); } static gboolean match_path (const char *word, int *start, int *end) { return regex_match (re_path (), word, start, end); } /* List of IRC commands for which contents (and thus possible URLs) * are visible to the user. NOTE: Trailing blank required in each. */ static char *commands[] = { "NOTICE ", "PRIVMSG ", "TOPIC ", "332 ", /* RPL_TOPIC */ "372 " /* RPL_MOTD */ }; #define ARRAY_SIZE(a) (sizeof (a) / sizeof ((a)[0])) void url_check_line (char *buf) { GRegex *re(void); GMatchInfo *gmi; char *po = buf; int i; /* Skip over message prefix */ if (*po == ':') { po = strchr (po, ' '); if (!po) return; po++; } /* Allow only commands from the above list */ for (i = 0; i < ARRAY_SIZE (commands); i++) { char *cmd = commands[i]; int len = strlen (cmd); if (strncmp (cmd, po, len) == 0) { po += len; break; } } if (i == ARRAY_SIZE (commands)) return; /* Skip past the channel name or user nick */ po = strchr (po, ' '); if (!po) return; po++; g_regex_match(re_url(), po, 0, &gmi); while (g_match_info_matches(gmi)) { int start, end; g_match_info_fetch_pos(gmi, 0, &start, &end); while (end > start && (po[end - 1] == '\r' || po[end - 1] == '\n')) end--; url_add(po + start, end - start); g_match_info_next(gmi, NULL); } g_match_info_free(gmi); } int url_last (int *lstart, int *lend) { *lstart = laststart; *lend = lastend; return lasttype; } static gboolean regex_match (const GRegex *re, const char *word, int *start, int *end) { GMatchInfo *gmi; g_regex_match (re, word, 0, &gmi); if (!g_match_info_matches (gmi)) { g_match_info_free (gmi); return FALSE; } while (g_match_info_matches (gmi)) { g_match_info_fetch_pos (gmi, 0, start, end); g_match_info_next (gmi, NULL); } g_match_info_free (gmi); return TRUE; } /* Miscellaneous description --- */ #define DOMAIN "[_\\pL\\pN\\pS][-_\\pL\\pN\\pS]*(\\.[-_\\pL\\pN\\pS]+)*" #define TLD "\\.[\\pL][-\\pL\\pN]*[\\pL]" #define IPADDR "[0-9]{1,3}(\\.[0-9]{1,3}){3}" #define IPV6GROUP "([0-9a-f]{0,4})" #define IPV6ADDR "((" IPV6GROUP "(:" IPV6GROUP "){7})" \ "|(" IPV6GROUP "(:" IPV6GROUP ")*:(:" IPV6GROUP ")+))" /* with :: compression */ #define HOST "(" DOMAIN TLD "|" IPADDR "|" IPV6ADDR ")" /* In urls the IPv6 must be enclosed in square brackets */ #define HOST_URL "(" DOMAIN TLD "|" IPADDR "|" "\\[" IPV6ADDR "\\]" ")" #define HOST_URL_OPT_TLD "(" DOMAIN "|" HOST_URL ")" #define PORT "(:[1-9][0-9]{0,4})" #define OPT_PORT "(" PORT ")?" static GRegex * make_re (const char *grist) { GRegex *ret; GError *err = NULL; ret = g_regex_new (grist, G_REGEX_CASELESS | G_REGEX_OPTIMIZE, 0, &err); return ret; } /* HOST description --- */ /* (see miscellaneous above) */ static const GRegex * re_host (void) { static GRegex *host_ret; if (host_ret) return host_ret; host_ret = make_re ("(" "(" HOST_URL PORT ")|(" HOST ")" ")"); return host_ret; } static const GRegex * re_host6 (void) { static GRegex *host6_ret; if (host6_ret) return host6_ret; host6_ret = make_re ("(" "(" IPV6ADDR ")|(" "\\[" IPV6ADDR "\\]" PORT ")" ")"); return host6_ret; } /* URL description --- */ #define SCHEME "(%s)" #define LPAR "\\(" #define RPAR "\\)" #define NOPARENS "[^() \t]*" #define PATH \ "(" \ "(" LPAR NOPARENS RPAR ")" \ "|" \ "(" NOPARENS ")" \ ")*" /* Zero or more occurrences of either of these */ \ "(?<![.,?!\\]])" /* Not allowed to end with these */ #define USERINFO "([-a-z0-9._~%]+(:[-a-z0-9._~%]*)?@)" /* Flags used to describe URIs (RFC 3986) * * Bellow is an example of what the flags match. * * URI_AUTHORITY - http://example.org:80/foo/bar * ^^^^^^^^^^^^^^^^ * URI_USERINFO/URI_OPT_USERINFO - http://user@example.org:80/foo/bar * ^^^^^ * URI_PATH - http://example.org:80/foo/bar * ^^^^^^^^ */ #define URI_AUTHORITY (1 << 0) #define URI_OPT_USERINFO (1 << 1) #define URI_USERINFO (1 << 2) #define URI_PATH (1 << 3) struct { const char *scheme; /* scheme name. e.g. http */ const char *path_sep; /* string that begins the path */ int flags; /* see above (flag macros) */ } uri[] = { { "irc", "/", URI_PATH }, { "ircs", "/", URI_PATH }, { "rtsp", "/", URI_AUTHORITY | URI_PATH }, { "feed", "/", URI_AUTHORITY | URI_PATH }, { "teamspeak", "?", URI_AUTHORITY | URI_PATH }, { "ftp", "/", URI_AUTHORITY | URI_OPT_USERINFO | URI_PATH }, { "sftp", "/", URI_AUTHORITY | URI_OPT_USERINFO | URI_PATH }, { "ftps", "/", URI_AUTHORITY | URI_OPT_USERINFO | URI_PATH }, { "http", "/", URI_AUTHORITY | URI_OPT_USERINFO | URI_PATH }, { "https", "/", URI_AUTHORITY | URI_OPT_USERINFO | URI_PATH }, { "cvs", "/", URI_AUTHORITY | URI_OPT_USERINFO | URI_PATH }, { "svn", "/", URI_AUTHORITY | URI_OPT_USERINFO | URI_PATH }, { "git", "/", URI_AUTHORITY | URI_OPT_USERINFO | URI_PATH }, { "bzr", "/", URI_AUTHORITY | URI_OPT_USERINFO | URI_PATH }, { "rsync", "/", URI_AUTHORITY | URI_OPT_USERINFO | URI_PATH }, { "mumble", "/", URI_AUTHORITY | URI_OPT_USERINFO | URI_PATH }, { "ventrilo", "/", URI_AUTHORITY | URI_OPT_USERINFO | URI_PATH }, { "xmpp", "/", URI_AUTHORITY | URI_OPT_USERINFO | URI_PATH }, { "h323", ";", URI_AUTHORITY | URI_OPT_USERINFO | URI_PATH }, { "imap", "/", URI_AUTHORITY | URI_OPT_USERINFO | URI_PATH }, { "pop", "/", URI_AUTHORITY | URI_OPT_USERINFO | URI_PATH }, { "nfs", "/", URI_AUTHORITY | URI_OPT_USERINFO | URI_PATH }, { "smb", "/", URI_AUTHORITY | URI_OPT_USERINFO | URI_PATH }, { "ssh", "", URI_AUTHORITY | URI_OPT_USERINFO }, { "sip", "", URI_AUTHORITY | URI_USERINFO }, { "sips", "", URI_AUTHORITY | URI_USERINFO }, { "magnet", "?", URI_PATH }, { "mailto", "", URI_PATH }, { "bitcoin", "", URI_PATH }, { "gtalk", "", URI_PATH }, { "steam", "", URI_PATH }, { "file", "/", URI_PATH }, { "callto", "", URI_PATH }, { "skype", "", URI_PATH }, { "geo", "", URI_PATH }, { "spotify", "", URI_PATH }, { "lastfm", "/", URI_PATH }, { "xfire", "", URI_PATH }, { "ts3server", "", URI_PATH }, { NULL, "", 0} }; static const GRegex * re_url_no_scheme (void) { static GRegex *url_ret = NULL; if (url_ret) return url_ret; url_ret = make_re ("(" HOST_URL OPT_PORT "/" "(" PATH ")?" ")"); return url_ret; } static const GRegex * re_url (void) { static GRegex *url_ret = NULL; GString *grist_gstr; char *grist; int i; if (url_ret) return url_ret; grist_gstr = g_string_new (NULL); for (i = 0; uri[i].scheme; i++) { if (i) g_string_append (grist_gstr, "|"); g_string_append (grist_gstr, "("); g_string_append_printf (grist_gstr, "%s:", uri[i].scheme); if (uri[i].flags & URI_AUTHORITY) g_string_append (grist_gstr, "//"); if (uri[i].flags & URI_USERINFO) g_string_append (grist_gstr, USERINFO); else if (uri[i].flags & URI_OPT_USERINFO) g_string_append (grist_gstr, USERINFO "?"); if (uri[i].flags & URI_AUTHORITY) g_string_append (grist_gstr, HOST_URL_OPT_TLD OPT_PORT); if (uri[i].flags & URI_PATH) { char *sep_escaped = g_regex_escape_string (uri[i].path_sep, strlen(uri[i].path_sep)); g_string_append_printf (grist_gstr, "(" "%s" PATH ")?", sep_escaped); g_free (sep_escaped); } g_string_append (grist_gstr, ")"); } grist = g_string_free (grist_gstr, FALSE); url_ret = make_re (grist); g_free (grist); return url_ret; } /* EMAIL description --- */ #define EMAIL "[a-z][._%+-a-z0-9]+@" "(" HOST_URL ")" static const GRegex * re_email (void) { static GRegex *email_ret; if (email_ret) return email_ret; email_ret = make_re ("(" EMAIL ")"); return email_ret; } /* NICK description --- */ /* For NICKPRE see before url_check_word() */ #define NICKHYP "-" #define NICKLET "a-z" #define NICKDIG "0-9" /* Note for NICKSPE: \\\\ boils down to a single \ */ #define NICKSPE "\\[\\]\\\\`_^{|}" #if 0 #define NICK0 "[" NICKPRE "]?[" NICKLET NICKSPE "]" #else /* Allow violation of rfc 2812 by allowing digit as first char */ /* Rationale is that do_an_re() above will anyway look up what */ /* we find, and that WORD_NICK is the last item in the array */ /* that do_an_re() runs through. */ #define NICK0 "^[" NICKPRE "]?[" NICKLET NICKDIG NICKSPE "]" #endif #define NICK1 "[" NICKHYP NICKLET NICKDIG NICKSPE "]*" #define NICK NICK0 NICK1 static const GRegex * re_nick (void) { static GRegex *nick_ret; if (nick_ret) return nick_ret; nick_ret = make_re ("(" NICK ")"); return nick_ret; } /* CHANNEL description --- */ #define CHANNEL "[" CHANPRE "][^ \t\a,]+(?:,[" CHANPRE "][^ \t\a,]+)*" static const GRegex * re_channel (void) { static GRegex *channel_ret; if (channel_ret) return channel_ret; channel_ret = make_re ("(" CHANNEL ")"); return channel_ret; } /* PATH description --- */ #ifdef WIN32 /* Windows path can be .\ ..\ or C: D: etc */ #define FS_PATH "^(\\.{1,2}\\\\|[a-z]:).*" #else /* Linux path can be / or ./ or ../ etc */ #define FS_PATH "^(/|\\./|\\.\\./).*" #endif static const GRegex * re_path (void) { static GRegex *path_ret; if (path_ret) return path_ret; path_ret = make_re ("(" FS_PATH ")"); return path_ret; }