From 19177de98c536297f7efcb45d306a1759cf27ea6 Mon Sep 17 00:00:00 2001 From: zodiac Date: Sat, 4 Jul 2009 08:16:07 +0000 Subject: [PATCH] Added --linkify option. Default settings cause the terminal to recognize fully qualified URLs and to make them clickable. git-svn-id: https://shellinabox.googlecode.com/svn/trunk@134 0da03de8-d603-11dd-86c2-0f8696b7b6f9 --- ChangeLog | 5 ++ config.h | 2 +- configure | 2 +- configure.ac | 2 +- demo/styles.css | 9 +++ demo/vt100.js | 138 +++++++++++++++++++++++++++++++- shellinabox/shell_in_a_box.js | 2 +- shellinabox/shellinaboxd.c | 21 ++++- shellinabox/shellinaboxd.man.in | 12 +++ shellinabox/styles.css | 9 +++ shellinabox/vt100.js | 138 +++++++++++++++++++++++++++++++- shellinabox/vt100.jspp | 136 +++++++++++++++++++++++++++++++ 12 files changed, 468 insertions(+), 8 deletions(-) diff --git a/ChangeLog b/ChangeLog index 0e35abb..7a76994 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,8 @@ +2009-07-03 Markus Gutschke + + * Added --linkify option. Default settings cause the terminal to + recognize fully qualified URLs and to make them clickable. + 2009-06-28 Markus Gutschke * Added support for keyboards that have a dedicated "<" / ">" key. diff --git a/config.h b/config.h index 035fcc6..d21c7b7 100644 --- a/config.h +++ b/config.h @@ -132,7 +132,7 @@ #define STDC_HEADERS 1 /* Most recent revision number in the version control system */ -#define VCS_REVISION "133" +#define VCS_REVISION "134" /* Version number of package */ #define VERSION "2.8" diff --git a/configure b/configure index 62bf430..f589d16 100755 --- a/configure +++ b/configure @@ -2037,7 +2037,7 @@ ac_link='$CC -o conftest$ac_exeext $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $ ac_compiler_gnu=$ac_cv_c_compiler_gnu -VCS_REVISION=133 +VCS_REVISION=134 cat >>confdefs.h <<_ACEOF diff --git a/configure.ac b/configure.ac index cc42ad4..d5a125e 100644 --- a/configure.ac +++ b/configure.ac @@ -2,7 +2,7 @@ AC_PREREQ(2.57) dnl This is the one location where the authoritative version number is stored AC_INIT(shellinabox, 2.8, markus@shellinabox.com) -VCS_REVISION=133 +VCS_REVISION=134 AC_SUBST(VCS_REVISION) AC_DEFINE_UNQUOTED(VCS_REVISION, "${VCS_REVISION}", [Most recent revision number in the version control system]) diff --git a/demo/styles.css b/demo/styles.css index 1db9f19..f9caad7 100644 --- a/demo/styles.css +++ b/demo/styles.css @@ -1,3 +1,12 @@ +#vt100 a { + text-decoration: none; + color: inherit; +} + +#vt100 a:hover { + text-decoration: underline; +} + #vt100 #reconnect { position: absolute; z-index: 2; diff --git a/demo/vt100.js b/demo/vt100.js index 603d1ae..c698345 100644 --- a/demo/vt100.js +++ b/demo/vt100.js @@ -96,6 +96,82 @@ // #define MOUSE_CLICK 2 function VT100(container) { + if (typeof linkifyURLs == 'undefined' && linkifyURLs > 0) { + this.urlRE = null; + } else { + this.urlRE = new RegExp( + // Known URL protocol are "http", "https", and "ftp". + '(?:http|https|ftp)://' + + + // Optionally allow username and passwords. + '(?:[^:@/ ]*(?::[^@/ ]*)?@)?' + + + // Hostname. + '(?:[1-9][0-9]{0,2}(?:[.][1-9][0-9]{0,2}){3}|' + + '[0-9a-fA-F]{0,4}(?::{1,2}[0-9a-fA-F]{1,4})+|' + + '(?!-)[^[!"#$%&\'()*+,/:;<=>?@\\^_`{|}~\u0000- \u009F]+)' + + + // Port + '(?::[1-9][0-9]*)?' + + + // Path. + '(?:/[^/,.) ]*)*|' + + + (linkifyURLs <= 1 ? '' : + // Also support URLs without a protocol (assume "http"). + // Optional username and password. + '(?:[^:@/ ]*(?::[^@/ ]*)?@)?' + + + // Hostnames must end with a well-known top-level domain or must be + // numeric. + '(?:[1-9][0-9]{0,2}(?:[.][1-9][0-9]{0,2}){3}|' + + 'localhost|' + + '(?:(?!-)[^.[!"#$%&\'()*+,/:;<=>?@\\^_`{|}~\u0000- \u009F]+[.]){2,}' + + '(?:com|net|org|edu|gov|aero|asia|biz|cat|coop|info|int|jobs|mil|mobi|' + + 'museum|name|pro|tel|travel|ac|ad|ae|af|ag|ai|al|am|an|ao|aq|ar|as|at|' + + 'au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|' + + 'ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|co|cr|cu|cv|cx|cy|cz|de|dj|dk|dm|do|' + + 'dz|ec|ee|eg|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|' + + 'gm|gn|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|in|io|iq|' + + 'ir|is|it|je|jm|jo|jp|ke|kg|kh|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|' + + 'lr|ls|lt|lu|lv|ly|ma|mc|md|me|mg|mh|mk|ml|mm|mn|mo|mp|mq|mr|ms|mt|mu|' + + 'mv|mw|mx|my|mz|na|nc|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|pa|pe|pf|pg|ph|' + + 'pk|pl|pm|pn|pr|ps|pt|pw|py|qa|re|ro|rs|ru|rw|sa|sb|sc|sd|se|sg|sh|si|' + + 'sj|sk|sl|sm|sn|so|sr|st|su|sv|sy|sz|tc|td|tf|tg|th|tj|tk|tl|tm|tn|to|' + + 'tp|tr|tt|tv|tw|tz|ua|ug|uk|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|ye|yt|' + + 'yu|za|zm|zw|arpa|[Xx][Nn]--[-a-zA-Z0-9]+))' + + + // Port + '(?::[1-9][0-9]{0,4})?' + + + // Path. + '(?:/[^/,.) ]*)*|') + + + // In addition, support e-mail address. Optionally, recognize "mailto:" + '(?:mailto:)' + (linkifyURLs <= 1 ? '' : '?') + + + // Username: + '[-_.+a-zA-Z0-9]+@' + + + // Hostname. + '(?!-)[-a-zA-Z0-9]+(?:[.](?!-)[-a-zA-Z0-9]+)?[.]' + + '(?:com|net|org|edu|gov|aero|asia|biz|cat|coop|info|int|jobs|mil|mobi|' + + 'museum|name|pro|tel|travel|ac|ad|ae|af|ag|ai|al|am|an|ao|aq|ar|as|at|' + + 'au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|' + + 'ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|co|cr|cu|cv|cx|cy|cz|de|dj|dk|dm|do|' + + 'dz|ec|ee|eg|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|' + + 'gm|gn|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|in|io|iq|' + + 'ir|is|it|je|jm|jo|jp|ke|kg|kh|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|' + + 'lr|ls|lt|lu|lv|ly|ma|mc|md|me|mg|mh|mk|ml|mm|mn|mo|mp|mq|mr|ms|mt|mu|' + + 'mv|mw|mx|my|mz|na|nc|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|pa|pe|pf|pg|ph|' + + 'pk|pl|pm|pn|pr|ps|pt|pw|py|qa|re|ro|rs|ru|rw|sa|sb|sc|sd|se|sg|sh|si|' + + 'sj|sk|sl|sm|sn|so|sr|st|su|sv|sy|sz|tc|td|tf|tg|th|tj|tk|tl|tm|tn|to|' + + 'tp|tr|tt|tv|tw|tz|ua|ug|uk|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|ye|yt|' + + 'yu|za|zm|zw|arpa|[Xx][Nn]--[-a-zA-Z0-9]+)' + + + // Optional arguments + '(?:[?][^/,.) ]+)?'); + } this.initializeElements(container); this.initializeAnsiColors(); this.maxScrollbackLines = 500; @@ -695,12 +771,72 @@ VT100.prototype.mouseEvent = function(event, type) { return true; }; +VT100.prototype.replaceChar = function(s, ch, repl) { + for (var i = -1;;) { + i = s.indexOf(ch, i + 1); + if (i < 0) { + break; + } + s = s.substr(0, i) + repl + s.substr(i + 1); + } + return s; +}; + +VT100.prototype.htmlEscape = function(s) { + return this.replaceChar(this.replaceChar(this.replaceChar( + s, '&', '&'), '<', '<'), '"', '"'); +}; + VT100.prototype.getTextContent = function(elem) { return elem.textContent || (typeof elem.textContent == 'undefined' ? elem.innerText : ''); }; VT100.prototype.setTextContent = function(elem, s) { + // Check if we find any URLs in the text. If so, automatically convert them + // to links. + if (this.urlRE && this.urlRE.test(s)) { + var inner = ''; + for (;;) { + var consumed = 0; + if (RegExp.leftContext != null) { + inner += this.htmlEscape(RegExp.leftContext); + consumed += RegExp.leftContext.length; + } + var url = this.htmlEscape(RegExp.lastMatch); + var fullUrl = url; + + // If no protocol was specified, try to guess a reasonable one. + if (url.indexOf('http://') < 0 && url.indexOf('https://') < 0 && + url.indexOf('ftp://') < 0 && url.indexOf('mailto:') < 0) { + var slash = url.indexOf('/'); + var at = url.indexOf('@'); + var question = url.indexOf('?'); + if (at > 0 && + (at < question || question < 0) && + (slash < 0 || (question > 0 && slash > question))) { + fullUrl = 'mailto:' + url; + } else { + fullUrl = (url.indexOf('ftp.') == 0 ? 'ftp://' : 'http://') + + url; + } + } + + inner += '' + url + ''; + consumed += RegExp.lastMatch.length; + s = s.substr(consumed); + if (!this.urlRE.test(s)) { + if (RegExp.rightContext != null) { + inner += this.htmlEscape(RegExp.rightContext); + } + break; + } + } + elem.innerHTML = inner; + return; + } + // Updating the content of an element is an expensive operation. It actually // pays off to first check whether the element is still unchanged. if (typeof elem.textContent == 'undefined') { @@ -1500,7 +1636,7 @@ VT100.prototype.toggleBell = function() { }; VT100.prototype.about = function() { - alert("VT100 Terminal Emulator " + "2.8 (revision 133)" + + alert("VT100 Terminal Emulator " + "2.8 (revision 134)" + "\nCopyright 2008-2009 by Markus Gutschke\n" + "For more information check http://shellinabox.com"); }; diff --git a/shellinabox/shell_in_a_box.js b/shellinabox/shell_in_a_box.js index a248167..f618894 100644 --- a/shellinabox/shell_in_a_box.js +++ b/shellinabox/shell_in_a_box.js @@ -355,7 +355,7 @@ ShellInABox.prototype.extendContextMenu = function(entries, actions) { }; ShellInABox.prototype.about = function() { - alert("Shell In A Box version " + "2.8 (revision 133)" + + alert("Shell In A Box version " + "2.8 (revision 134)" + "\nCopyright 2008-2009 by Markus Gutschke\n" + "For more information check http://shellinabox.com" + (typeof serverSupportsSSL != 'undefined' && serverSupportsSSL ? diff --git a/shellinabox/shellinaboxd.c b/shellinabox/shellinaboxd.c index 5c00b9d..853fe3c 100644 --- a/shellinabox/shellinaboxd.c +++ b/shellinabox/shellinaboxd.c @@ -84,6 +84,7 @@ static int noBeep = 0; static int numericHosts = 0; static int enableSSL = 1; static int enableSSLMenu = 1; +static int linkifyURLs = 1; static char *certificateDir; static int certificateFd = -1; static HashMap *externalFiles; @@ -522,10 +523,12 @@ static int shellInABoxHttpHandler(HttpConnection *http, void *arg, char *stateVars = stringPrintf(NULL, "serverSupportsSSL = %s;\n" "disableSSLMenu = %s;\n" - "suppressAllAudio = %s;\n\n", + "suppressAllAudio = %s;\n" + "linkifyURLs = %d;\n\n", enableSSL ? "true" : "false", !enableSSLMenu ? "true" : "false", - noBeep ? "true" : "false"); + noBeep ? "true" : "false", + linkifyURLs); int stateVarsLength = strlen(stateVars); int contentLength = stateVarsLength + (addr(vt100End) - addr(vt100Start)) + @@ -596,6 +599,7 @@ static void usage(void) { " -f, --static-file=URL:FILE serve static file from URL path\n" " -g, --group=GID switch to this group (default: %s)\n" " -h, --help print this message\n" + " --linkify=[none|normal|agressive] default is \"normal\"\n" " --localhost-only only listen on 127.0.0.1\n" " --no-beep suppress all audio output\n" " -n, --numeric do not resolve hostnames\n" @@ -666,6 +670,7 @@ static void parseArgs(int argc, char * const argv[]) { { "debug", 0, 0, 'd' }, { "static-file", 1, 0, 'f' }, { "group", 1, 0, 'g' }, + { "linkify", 1, 0, 0 }, { "localhost-only", 0, 0, 0 }, { "no-beep", 0, 0, 0 }, { "numeric", 0, 0, 'n' }, @@ -784,6 +789,18 @@ static void parseArgs(int argc, char * const argv[]) { fatal("Duplicate --group option."); } runAsGroup = parseGroup(optarg, NULL); + } else if (!idx--) { + // Linkify + if (!strcmp(optarg, "none")) { + linkifyURLs = 0; + } else if (!strcmp(optarg, "normal")) { + linkifyURLs = 1; + } else if (!strcmp(optarg, "aggressive")) { + linkifyURLs = 2; + } else { + fatal("Invalid argument for --linkify. Must be " + "\"none\", \"normal\", or \"aggressive\"."); + } } else if (!idx--) { // Localhost Only localhostOnly = 1; diff --git a/shellinabox/shellinaboxd.man.in b/shellinabox/shellinaboxd.man.in index 18af92c..54691c6 100644 --- a/shellinabox/shellinaboxd.man.in +++ b/shellinabox/shellinaboxd.man.in @@ -58,6 +58,7 @@ shellinaboxd \- publish command line shell through AJAX interface [\ \fB-f\fP\ | \fB--static-file=\fP\fIurl\fP:\fIfile\fP\ ] [\ \fB-g\fP\ | \fB--group=\fP\fIgid\fP\ ] [\ \fB-h\fP\ | \fB--help\fP\ ] +[\ \fB--linkify\fP=[\fBnone\fP|\fBnormal\fP|\fBaggressive\fP]\ ] [\ \fB--localhost-only\fP\ ] [\ \fB--no-beep\fP\ ] [\ \fB-n\fP\ | \fB--numeric\fP\ ] @@ -252,6 +253,17 @@ runs as. \fB-h\fP\ |\ \fB--help\fP Display a brief usage message showing the valid command line parameters. .TP +\fB--linkify\fP=[\fBnone\fP|\fBnormal\fP|\fBaggressive\fP] +the daemon attempts to recognize URLs in the terminal output and makes them +clickable. This is not neccessarily a fool-proof process and both false +negatives and false positives are possible. By default, only URLs starting +with a well known protocol of +.BR http:// ,\ https:// ,\ ftp:// ,\ or\ mailto: +are recognized. In +.B aggressive +mode, anything that looks like a hostname, URL or e-mail address is +recognized, even if not preceded by a protocol. +.TP \fB--localhost-only\fP Normally, .B shellinaboxd diff --git a/shellinabox/styles.css b/shellinabox/styles.css index 1db9f19..f9caad7 100644 --- a/shellinabox/styles.css +++ b/shellinabox/styles.css @@ -1,3 +1,12 @@ +#vt100 a { + text-decoration: none; + color: inherit; +} + +#vt100 a:hover { + text-decoration: underline; +} + #vt100 #reconnect { position: absolute; z-index: 2; diff --git a/shellinabox/vt100.js b/shellinabox/vt100.js index 603d1ae..c698345 100644 --- a/shellinabox/vt100.js +++ b/shellinabox/vt100.js @@ -96,6 +96,82 @@ // #define MOUSE_CLICK 2 function VT100(container) { + if (typeof linkifyURLs == 'undefined' && linkifyURLs > 0) { + this.urlRE = null; + } else { + this.urlRE = new RegExp( + // Known URL protocol are "http", "https", and "ftp". + '(?:http|https|ftp)://' + + + // Optionally allow username and passwords. + '(?:[^:@/ ]*(?::[^@/ ]*)?@)?' + + + // Hostname. + '(?:[1-9][0-9]{0,2}(?:[.][1-9][0-9]{0,2}){3}|' + + '[0-9a-fA-F]{0,4}(?::{1,2}[0-9a-fA-F]{1,4})+|' + + '(?!-)[^[!"#$%&\'()*+,/:;<=>?@\\^_`{|}~\u0000- \u009F]+)' + + + // Port + '(?::[1-9][0-9]*)?' + + + // Path. + '(?:/[^/,.) ]*)*|' + + + (linkifyURLs <= 1 ? '' : + // Also support URLs without a protocol (assume "http"). + // Optional username and password. + '(?:[^:@/ ]*(?::[^@/ ]*)?@)?' + + + // Hostnames must end with a well-known top-level domain or must be + // numeric. + '(?:[1-9][0-9]{0,2}(?:[.][1-9][0-9]{0,2}){3}|' + + 'localhost|' + + '(?:(?!-)[^.[!"#$%&\'()*+,/:;<=>?@\\^_`{|}~\u0000- \u009F]+[.]){2,}' + + '(?:com|net|org|edu|gov|aero|asia|biz|cat|coop|info|int|jobs|mil|mobi|' + + 'museum|name|pro|tel|travel|ac|ad|ae|af|ag|ai|al|am|an|ao|aq|ar|as|at|' + + 'au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|' + + 'ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|co|cr|cu|cv|cx|cy|cz|de|dj|dk|dm|do|' + + 'dz|ec|ee|eg|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|' + + 'gm|gn|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|in|io|iq|' + + 'ir|is|it|je|jm|jo|jp|ke|kg|kh|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|' + + 'lr|ls|lt|lu|lv|ly|ma|mc|md|me|mg|mh|mk|ml|mm|mn|mo|mp|mq|mr|ms|mt|mu|' + + 'mv|mw|mx|my|mz|na|nc|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|pa|pe|pf|pg|ph|' + + 'pk|pl|pm|pn|pr|ps|pt|pw|py|qa|re|ro|rs|ru|rw|sa|sb|sc|sd|se|sg|sh|si|' + + 'sj|sk|sl|sm|sn|so|sr|st|su|sv|sy|sz|tc|td|tf|tg|th|tj|tk|tl|tm|tn|to|' + + 'tp|tr|tt|tv|tw|tz|ua|ug|uk|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|ye|yt|' + + 'yu|za|zm|zw|arpa|[Xx][Nn]--[-a-zA-Z0-9]+))' + + + // Port + '(?::[1-9][0-9]{0,4})?' + + + // Path. + '(?:/[^/,.) ]*)*|') + + + // In addition, support e-mail address. Optionally, recognize "mailto:" + '(?:mailto:)' + (linkifyURLs <= 1 ? '' : '?') + + + // Username: + '[-_.+a-zA-Z0-9]+@' + + + // Hostname. + '(?!-)[-a-zA-Z0-9]+(?:[.](?!-)[-a-zA-Z0-9]+)?[.]' + + '(?:com|net|org|edu|gov|aero|asia|biz|cat|coop|info|int|jobs|mil|mobi|' + + 'museum|name|pro|tel|travel|ac|ad|ae|af|ag|ai|al|am|an|ao|aq|ar|as|at|' + + 'au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|' + + 'ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|co|cr|cu|cv|cx|cy|cz|de|dj|dk|dm|do|' + + 'dz|ec|ee|eg|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|' + + 'gm|gn|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|in|io|iq|' + + 'ir|is|it|je|jm|jo|jp|ke|kg|kh|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|' + + 'lr|ls|lt|lu|lv|ly|ma|mc|md|me|mg|mh|mk|ml|mm|mn|mo|mp|mq|mr|ms|mt|mu|' + + 'mv|mw|mx|my|mz|na|nc|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|pa|pe|pf|pg|ph|' + + 'pk|pl|pm|pn|pr|ps|pt|pw|py|qa|re|ro|rs|ru|rw|sa|sb|sc|sd|se|sg|sh|si|' + + 'sj|sk|sl|sm|sn|so|sr|st|su|sv|sy|sz|tc|td|tf|tg|th|tj|tk|tl|tm|tn|to|' + + 'tp|tr|tt|tv|tw|tz|ua|ug|uk|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|ye|yt|' + + 'yu|za|zm|zw|arpa|[Xx][Nn]--[-a-zA-Z0-9]+)' + + + // Optional arguments + '(?:[?][^/,.) ]+)?'); + } this.initializeElements(container); this.initializeAnsiColors(); this.maxScrollbackLines = 500; @@ -695,12 +771,72 @@ VT100.prototype.mouseEvent = function(event, type) { return true; }; +VT100.prototype.replaceChar = function(s, ch, repl) { + for (var i = -1;;) { + i = s.indexOf(ch, i + 1); + if (i < 0) { + break; + } + s = s.substr(0, i) + repl + s.substr(i + 1); + } + return s; +}; + +VT100.prototype.htmlEscape = function(s) { + return this.replaceChar(this.replaceChar(this.replaceChar( + s, '&', '&'), '<', '<'), '"', '"'); +}; + VT100.prototype.getTextContent = function(elem) { return elem.textContent || (typeof elem.textContent == 'undefined' ? elem.innerText : ''); }; VT100.prototype.setTextContent = function(elem, s) { + // Check if we find any URLs in the text. If so, automatically convert them + // to links. + if (this.urlRE && this.urlRE.test(s)) { + var inner = ''; + for (;;) { + var consumed = 0; + if (RegExp.leftContext != null) { + inner += this.htmlEscape(RegExp.leftContext); + consumed += RegExp.leftContext.length; + } + var url = this.htmlEscape(RegExp.lastMatch); + var fullUrl = url; + + // If no protocol was specified, try to guess a reasonable one. + if (url.indexOf('http://') < 0 && url.indexOf('https://') < 0 && + url.indexOf('ftp://') < 0 && url.indexOf('mailto:') < 0) { + var slash = url.indexOf('/'); + var at = url.indexOf('@'); + var question = url.indexOf('?'); + if (at > 0 && + (at < question || question < 0) && + (slash < 0 || (question > 0 && slash > question))) { + fullUrl = 'mailto:' + url; + } else { + fullUrl = (url.indexOf('ftp.') == 0 ? 'ftp://' : 'http://') + + url; + } + } + + inner += '' + url + ''; + consumed += RegExp.lastMatch.length; + s = s.substr(consumed); + if (!this.urlRE.test(s)) { + if (RegExp.rightContext != null) { + inner += this.htmlEscape(RegExp.rightContext); + } + break; + } + } + elem.innerHTML = inner; + return; + } + // Updating the content of an element is an expensive operation. It actually // pays off to first check whether the element is still unchanged. if (typeof elem.textContent == 'undefined') { @@ -1500,7 +1636,7 @@ VT100.prototype.toggleBell = function() { }; VT100.prototype.about = function() { - alert("VT100 Terminal Emulator " + "2.8 (revision 133)" + + alert("VT100 Terminal Emulator " + "2.8 (revision 134)" + "\nCopyright 2008-2009 by Markus Gutschke\n" + "For more information check http://shellinabox.com"); }; diff --git a/shellinabox/vt100.jspp b/shellinabox/vt100.jspp index cf02eca..f68f2c8 100644 --- a/shellinabox/vt100.jspp +++ b/shellinabox/vt100.jspp @@ -96,6 +96,82 @@ #define MOUSE_CLICK 2 function VT100(container) { + if (typeof linkifyURLs == 'undefined' && linkifyURLs > 0) { + this.urlRE = null; + } else { + this.urlRE = new RegExp( + // Known URL protocol are "http", "https", and "ftp". + '(?:http|https|ftp)://' + + + // Optionally allow username and passwords. + '(?:[^:@/ ]*(?::[^@/ ]*)?@)?' + + + // Hostname. + '(?:[1-9][0-9]{0,2}(?:[.][1-9][0-9]{0,2}){3}|' + + '[0-9a-fA-F]{0,4}(?::{1,2}[0-9a-fA-F]{1,4})+|' + + '(?!-)[^[!"#$%&\'()*+,/:;<=>?@\\^_`{|}~\u0000- \u009F]+)' + + + // Port + '(?::[1-9][0-9]*)?' + + + // Path. + '(?:/[^/,.) ]*)*|' + + + (linkifyURLs <= 1 ? '' : + // Also support URLs without a protocol (assume "http"). + // Optional username and password. + '(?:[^:@/ ]*(?::[^@/ ]*)?@)?' + + + // Hostnames must end with a well-known top-level domain or must be + // numeric. + '(?:[1-9][0-9]{0,2}(?:[.][1-9][0-9]{0,2}){3}|' + + 'localhost|' + + '(?:(?!-)[^.[!"#$%&\'()*+,/:;<=>?@\\^_`{|}~\u0000- \u009F]+[.]){2,}' + + '(?:com|net|org|edu|gov|aero|asia|biz|cat|coop|info|int|jobs|mil|mobi|' + + 'museum|name|pro|tel|travel|ac|ad|ae|af|ag|ai|al|am|an|ao|aq|ar|as|at|' + + 'au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|' + + 'ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|co|cr|cu|cv|cx|cy|cz|de|dj|dk|dm|do|' + + 'dz|ec|ee|eg|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|' + + 'gm|gn|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|in|io|iq|' + + 'ir|is|it|je|jm|jo|jp|ke|kg|kh|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|' + + 'lr|ls|lt|lu|lv|ly|ma|mc|md|me|mg|mh|mk|ml|mm|mn|mo|mp|mq|mr|ms|mt|mu|' + + 'mv|mw|mx|my|mz|na|nc|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|pa|pe|pf|pg|ph|' + + 'pk|pl|pm|pn|pr|ps|pt|pw|py|qa|re|ro|rs|ru|rw|sa|sb|sc|sd|se|sg|sh|si|' + + 'sj|sk|sl|sm|sn|so|sr|st|su|sv|sy|sz|tc|td|tf|tg|th|tj|tk|tl|tm|tn|to|' + + 'tp|tr|tt|tv|tw|tz|ua|ug|uk|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|ye|yt|' + + 'yu|za|zm|zw|arpa|[Xx][Nn]--[-a-zA-Z0-9]+))' + + + // Port + '(?::[1-9][0-9]{0,4})?' + + + // Path. + '(?:/[^/,.) ]*)*|') + + + // In addition, support e-mail address. Optionally, recognize "mailto:" + '(?:mailto:)' + (linkifyURLs <= 1 ? '' : '?') + + + // Username: + '[-_.+a-zA-Z0-9]+@' + + + // Hostname. + '(?!-)[-a-zA-Z0-9]+(?:[.](?!-)[-a-zA-Z0-9]+)?[.]' + + '(?:com|net|org|edu|gov|aero|asia|biz|cat|coop|info|int|jobs|mil|mobi|' + + 'museum|name|pro|tel|travel|ac|ad|ae|af|ag|ai|al|am|an|ao|aq|ar|as|at|' + + 'au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|' + + 'ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|co|cr|cu|cv|cx|cy|cz|de|dj|dk|dm|do|' + + 'dz|ec|ee|eg|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|' + + 'gm|gn|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|in|io|iq|' + + 'ir|is|it|je|jm|jo|jp|ke|kg|kh|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|' + + 'lr|ls|lt|lu|lv|ly|ma|mc|md|me|mg|mh|mk|ml|mm|mn|mo|mp|mq|mr|ms|mt|mu|' + + 'mv|mw|mx|my|mz|na|nc|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|pa|pe|pf|pg|ph|' + + 'pk|pl|pm|pn|pr|ps|pt|pw|py|qa|re|ro|rs|ru|rw|sa|sb|sc|sd|se|sg|sh|si|' + + 'sj|sk|sl|sm|sn|so|sr|st|su|sv|sy|sz|tc|td|tf|tg|th|tj|tk|tl|tm|tn|to|' + + 'tp|tr|tt|tv|tw|tz|ua|ug|uk|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|ye|yt|' + + 'yu|za|zm|zw|arpa|[Xx][Nn]--[-a-zA-Z0-9]+)' + + + // Optional arguments + '(?:[?][^/,.) ]+)?'); + } this.initializeElements(container); this.initializeAnsiColors(); this.maxScrollbackLines = 500; @@ -695,12 +771,72 @@ VT100.prototype.mouseEvent = function(event, type) { return true; }; +VT100.prototype.replaceChar = function(s, ch, repl) { + for (var i = -1;;) { + i = s.indexOf(ch, i + 1); + if (i < 0) { + break; + } + s = s.substr(0, i) + repl + s.substr(i + 1); + } + return s; +}; + +VT100.prototype.htmlEscape = function(s) { + return this.replaceChar(this.replaceChar(this.replaceChar( + s, '&', '&'), '<', '<'), '"', '"'); +}; + VT100.prototype.getTextContent = function(elem) { return elem.textContent || (typeof elem.textContent == 'undefined' ? elem.innerText : ''); }; VT100.prototype.setTextContent = function(elem, s) { + // Check if we find any URLs in the text. If so, automatically convert them + // to links. + if (this.urlRE && this.urlRE.test(s)) { + var inner = ''; + for (;;) { + var consumed = 0; + if (RegExp.leftContext != null) { + inner += this.htmlEscape(RegExp.leftContext); + consumed += RegExp.leftContext.length; + } + var url = this.htmlEscape(RegExp.lastMatch); + var fullUrl = url; + + // If no protocol was specified, try to guess a reasonable one. + if (url.indexOf('http://') < 0 && url.indexOf('https://') < 0 && + url.indexOf('ftp://') < 0 && url.indexOf('mailto:') < 0) { + var slash = url.indexOf('/'); + var at = url.indexOf('@'); + var question = url.indexOf('?'); + if (at > 0 && + (at < question || question < 0) && + (slash < 0 || (question > 0 && slash > question))) { + fullUrl = 'mailto:' + url; + } else { + fullUrl = (url.indexOf('ftp.') == 0 ? 'ftp://' : 'http://') + + url; + } + } + + inner += '' + url + ''; + consumed += RegExp.lastMatch.length; + s = s.substr(consumed); + if (!this.urlRE.test(s)) { + if (RegExp.rightContext != null) { + inner += this.htmlEscape(RegExp.rightContext); + } + break; + } + } + elem.innerHTML = inner; + return; + } + // Updating the content of an element is an expensive operation. It actually // pays off to first check whether the element is still unchanged. if (typeof elem.textContent == 'undefined') {