diff --git a/.gitignore b/.gitignore index fa0fe3f0..745c9dc3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ # Ignore data/ and tmp/ data/ tmp/ +tst/log/ +.settings/ +.buildpath +.project diff --git a/CREDITS.md b/CREDITS.md new file mode 100644 index 00000000..098261b6 --- /dev/null +++ b/CREDITS.md @@ -0,0 +1,8 @@ +Credits +======= +Sébastien Sauvage - original idea and main developer + +Alexey Gladkov - syntax highlighting +Greg Knaddison - robots.txt +MrKooky - HTML5 markup, CSS cleanup +Simon Rupf - MVC refactoring, configuration support and unit tests diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 00000000..ae35c5ce --- /dev/null +++ b/INSTALL.md @@ -0,0 +1,97 @@ +Installation +============ + +For Administrators +------------------ + +In the index.php in the main folder you can define a different PATH. This is +useful if you want to secure your installation and want to move the +configuration, data files, templates and PHP libraries (directories cfg, data, +lib, tpl and tst) outside of your document root. This new location must still +be accessible to your webserver / PHP process. + +> ### PATH Example ### +> Your zerobin installation lives in a subfolder called "paste" inside of your +> document root. The URL looks like this: +> http://example.com/paste/ +> The ZeroBin folder on your webserver is really: +> /home/example.com/htdocs/paste +> +> When setting the path like this: +> define('PATH', '../../secret/zerobin/'); +> ZeroBin will look for your includes here: +> /home/example.com/secret/zerobin + +In the file "cfg/conf.ini" you can configure ZeroBin. The config file is +divided into multiple sections, which are enclosed in square brackets. In the +"[main]" section you can enable or disable the discussion feature, set the +limit of stored pastes and comments in bytes. The "[traffic]" section lets you +set a time limit in seconds. Users may not post more often then this limit to +your ZeroBin. + +Finally the "[model]" and "[model_options]" sections let you configure your +favourite way of storing the pastes and discussions on your server. +"zerobin_data" is the default model, which stores everything in files in the +data folder. This is the recommended setup for low traffic sites. Under high +load, in distributed setups or if you are not allowed to store files locally, +you might want to switch to the "zerobin_db" model. This lets you store your +data in a database. Basically all databases that are supported by PDO (PHP +data objects) may be used. Automatic table creation is provided for pdo_ibm, +pdo_informix, pdo_mssql, pdo_mysql, pdo_oci, pdo_pgsql and pdo_sqlite. You may +want to provide a table prefix, if you have to share the zerobin database with +another application. The table prefix option is called "tbl". + +> ### Note ### +> The "zerobin_db" model has only been tested with SQLite and MySQL, although +> it would not be recommended to use SQLite in a production environment. If you +> gain any experience running ZeroBin on other RDBMS, please let us know. + +For reference or if you want to create the table schema for yourself: + + CREATE TABLE prefix_paste ( + dataid CHAR(16), + data TEXT, + postdate INT, + expiredate INT, + opendiscussion INT, + burnafterreading INT + ); + + CREATE TABLE prefix_comment ( + dataid CHAR(16), + pasteid CHAR(16), + parentid CHAR(16), + data TEXT, + nickname VARCHAR(255), + vizhash TEXT, + postdate INT + ); + +For Developers +-------------- +If you want to create your own data models, you might want to know how the +arrays, that you have to store, look like: + + public function create($pasteid, $paste) + { + $pasteid = substr(hash('md5', $paste['data']), 0, 16); + + $paste['data'] // text + $paste['meta']['postdate'] // int UNIX timestamp + $paste['meta']['expire_date'] // int UNIX timestamp + $paste['meta']['opendiscussion'] // true (if false it is unset) + $paste['meta']['burnafterreading'] // true (if false it is unset; if true, then opendiscussion is unset) + } + + public function createComment($pasteid, $parentid, $commentid, $comment) + { + $pasteid // the id of the paste this comment belongs to + $parentid // the id of the parent of this comment, may be the paste id itself + $commentid = substr(hash('md5', $paste['data']), 0, 16); + + $comment['data'] // text + $comment['meta']['nickname'] // text or null (if anonymous) + $comment['meta']['vizhash'] // text or null (if anonymous) + $comment['meta']['postdate'] // int UNIX timestamp + } + diff --git a/cfg/.htaccess b/cfg/.htaccess new file mode 100644 index 00000000..b584d98c --- /dev/null +++ b/cfg/.htaccess @@ -0,0 +1,2 @@ +Allow from none +Deny from all diff --git a/cfg/conf.ini b/cfg/conf.ini new file mode 100644 index 00000000..74acb0f3 --- /dev/null +++ b/cfg/conf.ini @@ -0,0 +1,47 @@ +; ZeroBin +; +; a zero-knowledge paste bin +; +; @link http://sebsauvage.net/wiki/doku.php?id=php:zerobin +; @copyright 2012 Sébastien SAUVAGE (sebsauvage.net) +; @license http://www.opensource.org/licenses/zlib-license.php The zlib/libpng License +; @version 0.15 + +[main] +; enable or disable discussions +opendiscussion = true + +; size limit per paste or comment in bytes +sizelimit = 2097152 + +[traffic] +; time limit between calls from the same IP address in seconds +limit = 10 +dir = PATH "data" + +[model] +; name of data model class to load and directory for storage +; the default model "zerobin_data" stores everything in the filesystem +class = zerobin_data +[model_options] +dir = PATH "data" + +;[model] +; example of DB configuration for MySQL +;class = zerobin_db +;[model_options] +;dsn = "mysql:host=localhost;dbname=zerobin;charset=UTF8" +;tbl = "zerobin_" ; table prefix +;usr = "zerobin" +;pwd = "Z3r0P4ss" +;opt[12] = true ; PDO::ATTR_PERSISTENT + +;[model] +; example of DB configuration for SQLite +;class = zerobin_db +;[model_options] +;dsn = "sqlite:" PATH "data/db.sq3" +;usr = null +;pwd = null +;opt[12] = true ; PDO::ATTR_PERSISTENT + diff --git a/css/prettify.css b/css/prettify.css new file mode 100644 index 00000000..f2ad6530 --- /dev/null +++ b/css/prettify.css @@ -0,0 +1,64 @@ +/* Pretty printing styles. Used with prettify.js. */ + +/* SPAN elements with the classes below are added by prettyprint. */ +.pln { color: #000 } /* plain text */ + +@media screen { + .str { color: #080 } /* string content */ + .kwd { color: #008 } /* a keyword */ + .com { color: #800 } /* a comment */ + .typ { color: #606 } /* a type name */ + .lit { color: #066 } /* a literal value */ + /* punctuation, lisp open bracket, lisp close bracket */ + .pun, .opn, .clo { color: #660 } + .tag { color: #008 } /* a markup tag name */ + .atn { color: #606 } /* a markup attribute name */ + .atv { color: #080 } /* a markup attribute value */ + .dec, .var { color: #606 } /* a declaration; a variable name */ + .fun { color: red } /* a function name */ +} + +/* Use higher contrast and text-weight for printable form. */ +@media print, projection { + .str { color: #060 } + .kwd { color: #006; font-weight: bold } + .com { color: #600; font-style: italic } + .typ { color: #404; font-weight: bold } + .lit { color: #044 } + .pun, .opn, .clo { color: #440 } + .tag { color: #006; font-weight: bold } + .atn { color: #404 } + .atv { color: #060 } +} + +/* Put a border around prettyprinted code snippets. */ +.prettyprint { + padding: 2px; + border: 1px solid #888; + background-color: white; + white-space: pre-wrap; +} + +/* Specify class=linenums on a pre to get line numbering */ +ol.linenums { + color: black; + margin-top: 0; + margin-bottom: 0; + list-style: decimal outside; +} /* IE indents via margin-left */ +/* +li.L0, +li.L1, +li.L2, +li.L3, +li.L5, +li.L6, +li.L7, +li.L8 { list-style-type: none } +*/ +/* Alternate shading for lines */ +li.L1, +li.L3, +li.L5, +li.L7, +li.L9 { background: #eee } diff --git a/css/zerobin.css b/css/zerobin.css index c4997698..c2372fe0 100644 --- a/css/zerobin.css +++ b/css/zerobin.css @@ -4,354 +4,374 @@ /* CSS Reset from YUI 3.4.1 (build 4118) - Copyright 2011 Yahoo! Inc. All rights reserved. Licensed under the BSD License. - http://yuilibrary.com/license/ */ -html{color:#000;background:#FFF}body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,textarea,p,blockquote,th,td{margin:0;padding:0}table{border-collapse:collapse;border-spacing:0}fieldset,img{border:0}address,caption,cite,code,dfn,em,strong,th,var{font-style:normal;font-weight:normal}ol,ul{list-style:none}caption,th{text-align:left}h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:normal}q:before,q:after{content:''}abbr,acronym{border:0;font-variant:normal}sup{vertical-align:text-top}sub{vertical-align:text-bottom}input,textarea,select{font-family:inherit;font-size:inherit;font-weight:inherit}input,textarea,select{*font-size:100%}legend{color:#000} +html{color:#000;background:#fff}body,div,dl,dt,dd,ul,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,textarea,p,blockquote,th,td{margin:0;padding:0}table{border-collapse:collapse;border-spacing:0}fieldset,img{border:0}address,caption,cite,code,dfn,em,strong,th,var{font-style:normal;font-weight:normal}ol,ul{list-style:none}caption,th{text-align:left}h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:normal}q:before,q:after{content:''}abbr,acronym{border:0;font-variant:normal}sup{vertical-align:text-top}sub{vertical-align:text-bottom}input,textarea,select{font-family:inherit;font-size:inherit;font-weight:inherit}input,textarea,select{font-size:100%;}legend{color:#000} -html { -background-color:#455463; -color:white; -min-height:100%; -background-image: linear-gradient(bottom, #0F1823 0%, #455463 100%); -background-image: -o-linear-gradient(bottom, #0F1823 0%, #455463 100%); -background-image: -moz-linear-gradient(bottom, #0F1823 0%, #455463 100%); -background-image: -webkit-linear-gradient(bottom, #0F1823 0%, #455463 100%); -background-image: -ms-linear-gradient(bottom, #0F1823 0%, #455463 100%); -background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #0F1823), color-stop(1, #455463)); +html { + background-color: #455463; + color: #fff; + min-height: 100%; + background-image: linear-gradient(bottom, #0f1823 0, #455463 100%); + background-image: -o-linear-gradient(bottom, #0f1823 0, #455463 100%); + background-image: -moz-linear-gradient(bottom, #0f1823 0, #455463 100%); + background-image: -webkit-linear-gradient(bottom, #0f1823 0, #455463 100%); + background-image: -ms-linear-gradient(bottom, #0f1823 0, #455463 100%); + background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #0f1823), color-stop(1, #455463)); } body { -font-family: Arial, Helvetica, sans-serif; -font-size: 0.8em; -margin-bottom:15px; -padding-left:60px; padding-right:60px; + font-family: Arial, Helvetica, sans-serif; + font-size: 0.8em; + margin-bottom: 15px; + padding-left: 60px; + padding-right: 60px; } -a { color:#0F388F; } +a { color: #0f388f; } h1 { -font-size:3.5em; -font-weight:700; -color:#000; -position:relative; -display:inline; -cursor:pointer; + font-size: 3.5em; + font-weight: bold; + color: #000; + position: relative; + display: inline; + cursor: pointer; } h1:before { -content:attr(title); -position:absolute; -color:rgba(255,255,255,0.15); -top:1px; -left:1px; -cursor:pointer; + content: attr(title); + position: absolute; + color: rgba(255,255,255,0.15); + top: 1px; + left: 1px; + cursor: pointer; } h2 { -color:#000; -font-size:1em; -display:inline; -font-style:italic; -font-weight:bold; -position:relative; -bottom:8px;} + color: #000; + font-size: 1em; + display: inline; + font-style: italic; + font-weight: bold; + position: relative; + bottom: 8px; +} h3 { -color:#94a3b4; -font-size:0.7em; -display:inline; -position:relative; -bottom:8px;} + color: #94a3b4; + font-size: 0.7em; + display: inline; + margin-top: 10px; + position: relative; + bottom: 8px; +} #aboutbox { -font-size:0.85em; -color: #94a3b4; -padding: 4px 8px 4px 16px; -position:relative; -top:10px; -border-left: 2px solid #94a3b4; -float:right; -width:60%; -} - -div#aboutbox a { color: #94a3b4; } - -textarea#message,div#cleartext,.replymessage { -clear:both; -color:black; -background-color:#fff; -white-space:pre-wrap; -font-family:Consolas,"Lucida Console","DejaVu Sans Mono",Monaco,monospace; -font-size:9pt; -border: 1px solid #28343F; -padding:5px; -box-sizing:border-box; --webkit-box-sizing:border-box; --moz-box-sizing:border-box; --ms-box-sizing:border-box; --o-box-sizing:border-box; -width:100%; -} - -div#status { -clear:both; -padding:5px 10px; -} - - -div#pastelink { -background-color:#1F2833; -color:white; -padding:4px 12px; -clear:both; --moz-box-shadow: inset 0px 2px 2px #000; --webkit-box-shadow: inset 0px 2px 2px #000; -box-shadow: inset 0px 2px 5px #000; -} -div#pastelink a { color:white; } -div#pastelink button { margin-left:11px } -div#toolbar, div#status { margin-bottom:5px; } - -button,.button,div#expiration,div#language { -color:#fff; -background-color:#323B47; -background-repeat:no-repeat; -background-position:center left; -padding:4px 8px; -font-size:1em; -margin-right:5px; -display:inline; -background-image: linear-gradient(bottom, #323B47 0%, #51606E 100%); -background-image: -o-linear-gradient(bottom, #323B47 0%, #51606E 100%); -background-image: -moz-linear-gradient(bottom, #323B47 0%, #51606E 100%); -background-image: -webkit-linear-gradient(bottom, #323B47 0%, #51606E 100%); -background-image: -ms-linear-gradient(bottom, #323B47 0%, #51606E 100%); -background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #323B47), color-stop(1, #51606E)); -border: 1px solid #28343F; --moz-box-shadow: inset 0px 1px 2px #647384; --webkit-box-shadow: inset 0px 1px 2px #647384; -box-shadow: inset 0px 1px 2px #647384; --webkit-border-radius: 3px; --moz-border-radius: 3px; -border-radius: 3px; --moz-background-clip: padding; -webkit-background-clip: padding-box; background-clip: padding-box; + font-size: 0.85em; + color: #94a3b4; + padding: 4px 8px 4px 16px; + position: relative; + top: 10px; + border-left: 2px solid #94a3b4; + float: right; + width: 60%; +} + +#aboutbox a { color: #94a3b4; } + +#message, #cleartext, .replymessage { + clear: both; + color: #000; + background-color: #fff; + white-space: pre-wrap; + font-family: Consolas, "Lucida Console", "DejaVu Sans Mono", Monaco, monospace; + font-size: 9pt; + border: 1px solid #28343F; + padding: 5px; + box-sizing: border-box; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + -ms-box-sizing: border-box; + -o-box-sizing: border-box; + width: 100%; +} + +#status { + clear: both; + padding: 5px 10px; +} + + +#pastelink { + background-color: #1F2833; + color: #fff; + padding: 4px 12px; + clear: both; + -moz-box-shadow: inset 0 2px 2px #000; + -webkit-box-shadow: inset 0 2px 2px #000; + box-shadow: inset 0 2px 2px #000; +} + +#pastelink a { color: #fff; } + +#pastelink button { margin-left: 11px } + +#toolbar, #status { margin-bottom: 5px; } + +button, .button, #expiration, #language { + color: #fff; + background-color: #323b47; + background-repeat: no-repeat; + background-position: center left; + padding: 4px 8px; + font-size: 1em; + margin-right: 5px; + display: inline; + background-image: linear-gradient(bottom, #323b47 0, #51606e 100%); + background-image: -o-linear-gradient(bottom, #323b47 0, #51606e 100%); + background-image: -moz-linear-gradient(bottom, #323b47 0, #51606e 100%); + background-image: -webkit-linear-gradient(bottom, #323b47 0, #51606e 100%); + background-image: -ms-linear-gradient(bottom, #323b47 0, #51606e 100%); + background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #323b47), color-stop(1, #51606e)); + border: 1px solid #28343F; + -moz-box-shadow: inset 0 1px 2px #647384; + -webkit-box-shadow: inset 0 1px 2px #647384; + box-shadow: inset 0 1px 2px #647384; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + -moz-background-clip: padding; + -webkit-background-clip: padding-box; + background-clip: padding-box; } + button:hover { -background-image: linear-gradient(bottom, #424B57 0%, #61707E 100%); -background-image: -o-linear-gradient(bottom, #424B57 0%, #61707E 100%); -background-image: -moz-linear-gradient(bottom, #424B57 0%, #61707E 100%); -background-image: -webkit-linear-gradient(bottom, #424B57 0%, #61707E 100%); -background-image: -ms-linear-gradient(bottom, #424B57 0%, #61707E 100%); -background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #424B57), color-stop(1, #61707E)); + background-image: linear-gradient(bottom, #424b57 0%, #61707e 100%); + background-image: -o-linear-gradient(bottom, #424b57 0%, #61707e 100%); + background-image: -moz-linear-gradient(bottom, #424b57 0%, #61707e 100%); + background-image: -webkit-linear-gradient(bottom, #424b57 0%, #61707e 100%); + background-image: -ms-linear-gradient(bottom, #424b57 0%, #61707e 100%); + background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #424b57), color-stop(1, #61707e)); } + button:active { -background-image: linear-gradient(bottom, #51606E 0%, #323B47 100%); -background-image: -o-linear-gradient(bottom, #51606E 0%, #323B47 100%); -background-image: -moz-linear-gradient(bottom, #51606E 0%, #323B47 100%); -background-image: -webkit-linear-gradient(bottom, #51606E 0%, #323B47 100%); -background-image: -ms-linear-gradient(bottom, #51606E 0%, #323B47 100%); -background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #51606E), color-stop(1, #323B47)); -position:relative; -top:1px; + background-image: linear-gradient(bottom, #51606e 0, #323b47 100%); + background-image: -o-linear-gradient(bottom, #51606e 0, #323b47 100%); + background-image: -moz-linear-gradient(bottom, #51606e 0, #323b47 100%); + background-image: -webkit-linear-gradient(bottom, #51606e 0, #323b47 100%); + background-image: -ms-linear-gradient(bottom, #51606e 0, #323b47 100%); + background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #51606e), color-stop(1, #323b47)); + position:relative; + top:1px; } + button:disabled, .buttondisabled { -background:#ccc; -color:#888; -top:0px; + background: #ccc; + color: #888; + top: 0; } + button img { -margin-right:8px; -position:relative; -top:2px; + margin-right: 8px; + position: relative; + top: 2px; } -div#expiration, div#language, div#opendisc { -background-color:#414D5A; -padding:6px 8px; -margin:0px 5px 0px 0px;; -position: relative; -bottom:1px; /* WTF ? Why is this shifted by 1 pixel ? */ +#expiration, #language, #opendisc { + background-color: #414d5a; + padding: 6px 8px; + margin: 0 5px 0 0; + position: relative; + bottom: 1px; /* WTF ? Why is this shifted by 1 pixel ? */ } -div#expiration select, div#language select { -color:#eee; -background: transparent; -border: none; + +#expiration select, #language select { + color: #eee; + background: transparent; + border: none; } -div#expiration select option, div#language select option { -color:#eee; -background: #414D5A; -background-color:#414D5A; +#expiration select option, #language select option { + color:#eee; + background: #414d5a; } -div#remainingtime { -color: #94a3b4; -display:inline; -font-size:0.85em; +#remainingtime { + color: #94a3b4; + display: inline; + font-size: 0.85em; } -.foryoureyesonly { -color: yellow !important; -font-size: 1em !important; -font-weight:bold !important; +#newbutton { + float: right; + margin-right: 0; + margin-bottom: 5px; + display: inline; } -button#newbutton { float:right; margin-right:0px;margin-bottom:5px; display:inline; } -input { color:#777; font-size:1em; padding:6px; border: 1px solid #28343F; } +input { + color: #777; + font-size: 1em; + padding: 6px; + border: 1px solid #28343f; +} -.nonworking { -background-color:#fff; -color:#000; -width:100%; -text-align:center; -font-weight:bold; -font-size:10pt; --webkit-border-radius:4px; --moz-border-radius:4px; -border-radius:4px; -padding:5px; +.blink { + text-decoration: blink; + font-size: 0.8em; + color: #a4b3c4; } -div#ienotice { -background-color:#7E98AF; -color:#000; -font-size:0.85em; -padding:3px 5px; -text-align:center; --webkit-border-radius:4px; --moz-border-radius:4px; -border-radius:4px; -display:none; +.foryoureyesonly { + color: #ff0 !important; + font-size: 1em !important; + font-weight: bold !important; } -div#ienotice a { -color:black; +.nonworking { + background-color: #fff; + color: #000; + width: 100%; + text-align: center; + font-weight: bold; + font-size: 10pt; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + padding: 5px; } -div#oldienotice { -display:none; +.hidden { display: none !important; } + +#ienotice { + background-color: #7e98af; + color: #000; + font-size: 0.85em; + padding: 3px 5px; + text-align: center; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + display: none; } +#ienotice a { color: #000; } + +#oldienotice { display: none; } + .errorMessage { -background-color:#FF7979 !important; -color:#FF0; + background-color: #f77 !important; + color:#ff0; } - /* --- discussion related CSS ------- */ - -div#discussion { /* Discussion container */ -margin-top:20px; -width:100%; -margin-left:-30px; -min-width:200px; +#discussion { /* Discussion container */ + margin-top: 20px; + width: 100%; + margin-left: -30px; + min-width: 200px; } h4 { -font-size:1.2em; -color: #94A3B4; -font-style:italic; -font-weight:bold; -position:relative; -margin-left:30px; + font-size: 1.2em; + color: #94a3b4; + font-style: italic; + font-weight: bold; + position: relative; + margin-left: 30px; } - -div.comment /* One single reply */ +.comment /* One single reply */ { -background-color:#CECED6; -color:#000; -white-space:pre-wrap; -font-family:Consolas,"Lucida Console","DejaVu Sans Mono",Monaco,monospace; -font-size:9pt; -border-left: 1px solid #859AAE; -border-top: 1px solid #859AAE; -padding:5px 0px 5px 5px; -margin-left:30px; --moz-box-shadow: -3px -3px 5px rgba(0,0,0,0.15); --webkit-box-shadow: -3px -3px 5px rgba(0,0,0,0.15); -box-shadow: -3px -3px 5px rgba(0,0,0,0.15); -min-width:200px; -overflow:auto; -} -/* FIXME: Add min-width */ - -div.reply { -margin: 5px 0px 0px 30px; -} - -div#replystatus { -display:inline; -padding:1px 7px; -font-family: Arial, Helvetica, sans-serif; -} - -div.comment button { -color:#446; -background-color:#aab; -background-repeat:no-repeat; -background-position:center left; -padding:0px 2px; -font-size:0.73em; -margin: 3px 5px 3px 0px; -display:inline; -background-image: linear-gradient(bottom, #aab 0%, #ccc 100%); -background-image: -o-linear-gradient(bottom, #aab 0%, #ccc 100%); -background-image: -moz-linear-gradient(bottom, #aab 0%, #ccc 100%); -background-image: -webkit-linear-gradient(bottom, #aab 0%, #ccc 100%); -background-image: -ms-linear-gradient(bottom, #aab 0%, #ccc 100%); -background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #aab), color-stop(1, #ccc)); -border: 1px solid #ccd; --moz-box-shadow: inset 0px 1px 2px #ddd; --webkit-box-shadow: inset 0px 1px 2px #fff; -box-shadow: inset 0px 1px 2px #eee; --webkit-border-radius: 3px; --moz-border-radius: 3px; -border-radius: 3px; --moz-background-clip: padding; -webkit-background-clip: padding-box; background-clip: padding-box; -} -div.comment button:hover { -background-image: linear-gradient(bottom, #ccd 0%, #fff 100%); -background-image: -o-linear-gradient(bottom, #ccd 0%, #fff 100%); -background-image: -moz-linear-gradient(bottom, #ccd 0%, #fff 100%); -background-image: -webkit-linear-gradient(bottom, #ccd 0%, #fff 100%); -background-image: -ms-linear-gradient(bottom, #ccd 0%, #fff 100%); -background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #ccd), color-stop(1, #fff)); -} -div.comment button:active { -background-image: linear-gradient(bottom, #fff 0%, #889 100%); -background-image: -o-linear-gradient(bottom, #fff 0%, #889 100%); -background-image: -moz-linear-gradient(bottom, #fff 0%, #889 100%); -background-image: -webkit-linear-gradient(bottom, #fff 0%, #889 100%); -background-image: -ms-linear-gradient(bottom, #fff 0%, #889 100%); -background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #fff), color-stop(1, #889)); -position:relative; -top:1px; -} - -div.comment input { -padding:2px; -} - -textarea#replymessage { -margin-top:5px; -} - -div.commentmeta { -color: #fff; -background-color:#8EA0B2; -margin-bottom:3px; -padding:0px 0px 0px 3px; -} - -span.commentdate { -color: #BFCEDE; -} + background-color: #ceced6; + color: #000; + white-space: pre-wrap; + font-family: Consolas,"Lucida Console","DejaVu Sans Mono",Monaco,monospace; + font-size: 9pt; + border-left: 1px solid #859AAE; + border-top: 1px solid #859AAE; + padding: 5px 0px 5px 5px; + margin-left: 30px; + -moz-box-shadow: -3px -3px 5px rgba(0,0,0,0.15); + -webkit-box-shadow: -3px -3px 5px rgba(0,0,0,0.15); + box-shadow: -3px -3px 5px rgba(0,0,0,0.15); + min-width: 200px; + overflow: auto; +} + +.reply { margin: 5px 0 0 30px; } + +#replystatus { + display: inline; + padding: 1px 7px; + font-family: Arial, Helvetica, sans-serif; +} + +.comment button { + color: #446; + background-color: #aab; + background-repeat: no-repeat; + background-position: center left; + padding: 0 2px; + font-size: 0.73em; + margin: 3px 5px 3px 0; + display: inline; + background-image: linear-gradient(bottom, #aab 0, #ccc 100%); + background-image: -o-linear-gradient(bottom, #aab 0, #ccc 100%); + background-image: -moz-linear-gradient(bottom, #aab 0, #ccc 100%); + background-image: -webkit-linear-gradient(bottom, #aab 0, #ccc 100%); + background-image: -ms-linear-gradient(bottom, #aab 0, #ccc 100%); + background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #aab), color-stop(1, #ccc)); + border: 1px solid #ccd; + -moz-box-shadow: inset 0 1px 2px #ddd; + -webkit-box-shadow: inset 0 1px 2px #fff; + box-shadow: inset 0 1px 2px #eee; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + -moz-background-clip: padding; + -webkit-background-clip: padding-box; + background-clip: padding-box; +} + +.comment button:hover { + background-image: linear-gradient(bottom, #ccd 0, #fff 100%); + background-image: -o-linear-gradient(bottom, #ccd 0, #fff 100%); + background-image: -moz-linear-gradient(bottom, #ccd 0, #fff 100%); + background-image: -webkit-linear-gradient(bottom, #ccd 0, #fff 100%); + background-image: -ms-linear-gradient(bottom, #ccd 0, #fff 100%); + background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #ccd), color-stop(1, #fff)); +} + +.comment button:active { + background-image: linear-gradient(bottom, #fff 0, #889 100%); + background-image: -o-linear-gradient(bottom, #fff 0, #889 100%); + background-image: -moz-linear-gradient(bottom, #fff 0, #889 100%); + background-image: -webkit-linear-gradient(bottom, #fff 0, #889 100%); + background-image: -ms-linear-gradient(bottom, #fff 0, #889 100%); + background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #fff), color-stop(1, #889)); + position:relative; + top:1px; +} + +.comment input { padding: 2px; } + +#replymessage { margin-top: 5px; } + +.commentmeta { + color: #fff; + background-color: #8ea0b2; + margin-bottom: 3px; + padding: 0 0 0 3px; +} + +.commentdate { color: #bfcede; } img.vizhash { -width:16px; -height:16px; -position:relative; -top:2px; -left:-3px; + width: 16px; + height: 16px; + position: relative; + top: 2px; + left: -3px; } \ No newline at end of file diff --git a/img/icon_clone.png b/img/icon_clone.png index 94843acc..3d558ca9 100644 Binary files a/img/icon_clone.png and b/img/icon_clone.png differ diff --git a/index.php b/index.php index ddd28f61..42576dc0 100644 --- a/index.php +++ b/index.php @@ -1,339 +1,17 @@ "); - chmod($tfilename,0705); - } - require $tfilename; - $tl=$GLOBALS['trafic_limiter']; - if (!empty($tl[$ip]) && ($tl[$ip]+10>=time())) - { - return false; - // FIXME: purge file of expired IPs to keep it small - } - $tl[$ip]=time(); - file_put_contents($tfilename, ""); - return true; -} - -/* Convert paste id to storage path. - The idea is to creates subdirectories in order to limit the number of files per directory. - (A high number of files in a single directory can slow things down.) - eg. "f468483c313401e8" will be stored in "data/f4/68/f468483c313401e8" - High-trafic websites may want to deepen the directory structure (like Squid does). - - eg. input 'e3570978f9e4aa90' --> output 'data/e3/57/' -*/ -function dataid2path($dataid) -{ - return 'data/'.substr($dataid,0,2).'/'.substr($dataid,2,2).'/'; -} - -/* Convert paste id to discussion storage path. - eg. 'e3570978f9e4aa90' --> 'data/e3/57/e3570978f9e4aa90.discussion/' -*/ -function dataid2discussionpath($dataid) -{ - return dataid2path($dataid).$dataid.'.discussion/'; -} - -// Checks if a json string is a proper SJCL encrypted message. -// False if format is incorrect. -function validSJCL($jsonstring) -{ - $accepted_keys=array('iv','salt','ct'); - - // Make sure content is valid json - $decoded = json_decode($jsonstring); - if ($decoded==null) return false; - $decoded = (array)$decoded; - - // Make sure required fields are present and that they are base64 data. - foreach($accepted_keys as $k) - { - if (!array_key_exists($k,$decoded)) { return false; } - if (base64_decode($decoded[$k],$strict=true)==null) { return false; } - } - - // Make sure no additionnal keys were added. - if (count(array_intersect(array_keys($decoded),$accepted_keys))!=3) { return false; } - - // FIXME: Reject data if entropy is too low ? - - // Make sure some fields have a reasonable size. - if (strlen($decoded['iv'])>24) return false; - if (strlen($decoded['salt'])>14) return false; - return true; -} - -// Delete a paste and its discussion. -// Input: $pasteid : the paste identifier. -function deletePaste($pasteid) -{ - // Delete the paste itself - unlink(dataid2path($pasteid).$pasteid); - - // Delete discussion if it exists. - $discdir = dataid2discussionpath($pasteid); - if (is_dir($discdir)) - { - // Delete all files in discussion directory - $dhandle = opendir($discdir); - while (false !== ($filename = readdir($dhandle))) - { - if (is_file($discdir.$filename)) unlink($discdir.$filename); - } - closedir($dhandle); - - // Delete the discussion directory. - rmdir($discdir); - } -} - -if (!empty($_POST['data'])) // Create new paste/comment -{ - /* POST contains: - data (mandatory) = json encoded SJCL encrypted text (containing keys: iv,salt,ct) - - All optional data will go to meta information: - expire (optional) = expiration delay (never,10min,1hour,1day,1month,1year,burn) (default:never) - opendiscusssion (optional) = is the discussion allowed on this paste ? (0/1) (default:0) - nickname (optional) = son encoded SJCL encrypted text nickname of author of comment (containing keys: iv,salt,ct) - parentid (optional) = in discussion, which comment this comment replies to. - pasteid (optional) = in discussion, which paste this comment belongs to. - */ - - header('Content-type: application/json'); - $error = false; - - // Create storage directory if it does not exist. - if (!is_dir('data')) - { - mkdir('data',0705); - file_put_contents('data/.htaccess',"Allow from none\nDeny from all\n"); - } - - // Make sure last paste from the IP address was more than 10 seconds ago. - if (!trafic_limiter_canPass($_SERVER['REMOTE_ADDR'])) - { echo json_encode(array('status'=>1,'message'=>'Please wait 10 seconds between each post.')); exit; } - - // Make sure content is not too big. - $data = $_POST['data']; - if (strlen($data)>2000000) - { echo json_encode(array('status'=>1,'message'=>'Paste is limited to 2 Mb of encrypted data.')); exit; } - - // Make sure format is correct. - if (!validSJCL($data)) - { echo json_encode(array('status'=>1,'message'=>'Invalid data.')); exit; } - - // Read additional meta-information. - $meta=array(); - - // Read expiration date - if (!empty($_POST['expire'])) - { - $expire=$_POST['expire']; - if ($expire=='10min') $meta['expire_date']=time()+10*60; - elseif ($expire=='1hour') $meta['expire_date']=time()+60*60; - elseif ($expire=='1day') $meta['expire_date']=time()+24*60*60; - elseif ($expire=='1month') $meta['expire_date']=time()+30*24*60*60; // Well this is not *exactly* one month, it's 30 days. - elseif ($expire=='1year') $meta['expire_date']=time()+365*24*60*60; - elseif ($expire=='burn') $meta['burnafterreading']=true; - } - - // Read open discussion flag - if (!empty($_POST['opendiscussion'])) - { - $opendiscussion = $_POST['opendiscussion']; - if ($opendiscussion!='0' && $opendiscussion!='1') { $error=true; } - if ($opendiscussion!='0') { $meta['opendiscussion']=true; } - } - - // You can't have an open discussion on a "Burn after reading" paste: - if (isset($meta['burnafterreading'])) unset($meta['opendiscussion']); - - // Optional nickname for comments - if (!empty($_POST['nickname'])) - { - $nick = $_POST['nickname']; - if (!validSJCL($nick)) - { - $error=true; - } - else - { - $meta['nickname']=$nick; - - // Generation of the anonymous avatar (Vizhash): - // If a nickname is provided, we generate a Vizhash. - // (We assume that if the user did not enter a nickname, he/she wants - // to be anonymous and we will not generate the vizhash.) - $vz = new vizhash16x16(); - $pngdata = $vz->generate($_SERVER['REMOTE_ADDR']); - if ($pngdata!='') $meta['vizhash'] = 'data:image/png;base64,'.base64_encode($pngdata); - // Once the avatar is generated, we do not keep the IP address, nor its hash. - } - } - - if ($error) - { - echo json_encode(array('status'=>1,'message'=>'Invalid data.')); - exit; - } - - // Add post date to meta. - $meta['postdate']=time(); - - // We just want a small hash to avoid collisions: Half-MD5 (64 bits) will do the trick - $dataid = substr(hash('md5',$data),0,16); - - $is_comment = (!empty($_POST['parentid']) && !empty($_POST['pasteid'])); // Is this post a comment ? - $storage = array('data'=>$data); - if (count($meta)>0) $storage['meta'] = $meta; // Add meta-information only if necessary. - - if ($is_comment) // The user posts a comment. - { - $pasteid = $_POST['pasteid']; - $parentid = $_POST['parentid']; - if (!preg_match('/[a-f\d]{16}/',$pasteid)) { echo json_encode(array('status'=>1,'message'=>'Invalid data.')); exit; } - if (!preg_match('/[a-f\d]{16}/',$parentid)) { echo json_encode(array('status'=>1,'message'=>'Invalid data.')); exit; } - - unset($storage['expire_date']); // Comment do not expire (it's the paste that expires) - unset($storage['opendiscussion']); - - // Make sure paste exists. - $storagedir = dataid2path($pasteid); - if (!is_file($storagedir.$pasteid)) { echo json_encode(array('status'=>1,'message'=>'Invalid data.')); exit; } - - // Make sure the discussion is opened in this paste. - $paste=json_decode(file_get_contents($storagedir.$pasteid)); - if (!$paste->meta->opendiscussion) { echo json_encode(array('status'=>1,'message'=>'Invalid data.')); exit; } - - $discdir = dataid2discussionpath($pasteid); - $filename = $pasteid.'.'.$dataid.'.'.$parentid; - if (!is_dir($discdir)) mkdir($discdir,$mode=0705,$recursive=true); - if (is_file($discdir.$filename)) // Oups... improbable collision. - { - echo json_encode(array('status'=>1,'message'=>'You are unlucky. Try again.')); - exit; - } - - file_put_contents($discdir.$filename,json_encode($storage)); - echo json_encode(array('status'=>0,'id'=>$dataid)); // 0 = no error - exit; - } - else // a standard paste. - { - $storagedir = dataid2path($dataid); - if (!is_dir($storagedir)) mkdir($storagedir,$mode=0705,$recursive=true); - if (is_file($storagedir.$dataid)) // Oups... improbable collision. - { - echo json_encode(array('status'=>1,'message'=>'You are unlucky. Try again.')); - exit; - } - // New paste - file_put_contents($storagedir.$dataid,json_encode($storage)); - echo json_encode(array('status'=>0,'id'=>$dataid)); // 0 = no error - exit; - } - -echo json_encode(array('status'=>1,'message'=>'Server error.')); -exit; -} - -$CIPHERDATA=''; -$ERRORMESSAGE=''; -if (!empty($_SERVER['QUERY_STRING'])) // Display an existing paste. -{ - $dataid = $_SERVER['QUERY_STRING']; - if (preg_match('/[a-f\d]{16}/',$dataid)) // Is this a valid paste identifier ? - { - $filename = dataid2path($dataid).$dataid; - if (is_file($filename)) // Check that paste exists. - { - // Get the paste itself. - $paste=json_decode(file_get_contents($filename)); - - // See if paste has expired. - if (isset($paste->meta->expire_date) && $paste->meta->expire_datemeta, 'expire_date')) $paste->meta->remaining_time = $paste->meta->expire_date - time(); - - $messages = array($paste); // The paste itself is the first in the list of encrypted messages. - // If it's a discussion, get all comments. - if (property_exists($paste->meta, 'opendiscussion') && $paste->meta->opendiscussion) - { - $comments=array(); - $datadir = dataid2discussionpath($dataid); - if (!is_dir($datadir)) mkdir($datadir,$mode=0705,$recursive=true); - $dhandle = opendir($datadir); - while (false !== ($filename = readdir($dhandle))) - { - if (is_file($datadir.$filename)) - { - $comment=json_decode(file_get_contents($datadir.$filename)); - // Filename is in the form pasteid.commentid.parentid: - // - pasteid is the paste this reply belongs to. - // - commentid is the comment identifier itself. - // - parentid is the comment this comment replies to (It can be pasteid) - $items=explode('.',$filename); - $comment->meta->commentid=$items[1]; // Add some meta information not contained in file. - $comment->meta->parentid=$items[2]; - $comments[$comment->meta->postdate]=$comment; // Store in table - } - } - closedir($dhandle); - ksort($comments); // Sort comments by date, oldest first. - $messages = array_merge($messages, $comments); - } - $CIPHERDATA = json_encode($messages); - - // If the paste was meant to be read only once, delete it. - if (property_exists($paste->meta, 'burnafterreading') && $paste->meta->burnafterreading) deletePaste($dataid); - } - } - else - { - $ERRORMESSAGE='Paste does not exist or has expired.'; - } - } -} - - -require_once "lib/rain.tpl.class.php"; -header('Content-Type: text/html; charset=utf-8'); -$page = new RainTPL; -$page->assign('CIPHERDATA',htmlspecialchars($CIPHERDATA,ENT_NOQUOTES)); // We escape it here because ENT_NOQUOTES can't be used in RainTPL templates. -$page->assign('VERSION',$VERSION); -$page->assign('ERRORMESSAGE',$ERRORMESSAGE); -$page->draw('page'); -?> +/** + * ZeroBin + * + * a zero-knowledge paste bin + * + * @link http://sebsauvage.net/wiki/doku.php?id=php:zerobin + * @copyright 2012 Sébastien SAUVAGE (sebsauvage.net) + * @license http://www.opensource.org/licenses/zlib-license.php The zlib/libpng License + * @version 0.15 + */ + +// change this, if your php files and data is outside of your webservers document root +define('PATH', ''); + +require PATH . 'lib/auto.php'; +new zerobin; diff --git a/js/prettify.js b/js/prettify.js new file mode 100644 index 00000000..04ed32db --- /dev/null +++ b/js/prettify.js @@ -0,0 +1,1477 @@ +// Copyright (C) 2006 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +/** + * @fileoverview + * some functions for browser-side pretty printing of code contained in html. + * + *

+ * For a fairly comprehensive set of languages see the + * README + * file that came with this source. At a minimum, the lexer should work on a + * number of languages including C and friends, Java, Python, Bash, SQL, HTML, + * XML, CSS, Javascript, and Makefiles. It works passably on Ruby, PHP and Awk + * and a subset of Perl, but, because of commenting conventions, doesn't work on + * Smalltalk, Lisp-like, or CAML-like languages without an explicit lang class. + *

+ * Usage:

    + *
  1. include this source file in an html page via + * {@code } + *
  2. define style rules. See the example page for examples. + *
  3. mark the {@code
    } and {@code } tags in your source with
    + *    {@code class=prettyprint.}
    + *    You can also use the (html deprecated) {@code } tag, but the pretty
    + *    printer needs to do more substantial DOM manipulations to support that, so
    + *    some css styles may not be preserved.
    + * </ol>
    + * That's it.  I wanted to keep the API as simple as possible, so there's no
    + * need to specify which language the code is in, but if you wish, you can add
    + * another class to the {@code <pre>} or {@code <code>} element to specify the
    + * language, as in {@code <pre class="prettyprint lang-java">}.  Any class that
    + * starts with "lang-" followed by a file extension, specifies the file type.
    + * See the "lang-*.js" files in this directory for code that implements
    + * per-language file handlers.
    + * <p>
    + * Change log:<br>
    + * cbeust, 2006/08/22
    + * <blockquote>
    + *   Java annotations (start with "@") are now captured as literals ("lit")
    + * </blockquote>
    + * @requires console
    + */
    +
    +// JSLint declarations
    +/*global console, document, navigator, setTimeout, window */
    +
    +/**
    + * Split {@code prettyPrint} into multiple timeouts so as not to interfere with
    + * UI events.
    + * If set to {@code false}, {@code prettyPrint()} is synchronous.
    + */
    +window['PR_SHOULD_USE_CONTINUATION'] = true;
    +
    +(function () {
    +  // Keyword lists for various languages.
    +  // We use things that coerce to strings to make them compact when minified
    +  // and to defeat aggressive optimizers that fold large string constants.
    +  var FLOW_CONTROL_KEYWORDS = ["break,continue,do,else,for,if,return,while"];
    +  var C_KEYWORDS = [FLOW_CONTROL_KEYWORDS,"auto,case,char,const,default," + 
    +      "double,enum,extern,float,goto,int,long,register,short,signed,sizeof," +
    +      "static,struct,switch,typedef,union,unsigned,void,volatile"];
    +  var COMMON_KEYWORDS = [C_KEYWORDS,"catch,class,delete,false,import," +
    +      "new,operator,private,protected,public,this,throw,true,try,typeof"];
    +  var CPP_KEYWORDS = [COMMON_KEYWORDS,"alignof,align_union,asm,axiom,bool," +
    +      "concept,concept_map,const_cast,constexpr,decltype," +
    +      "dynamic_cast,explicit,export,friend,inline,late_check," +
    +      "mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast," +
    +      "template,typeid,typename,using,virtual,where"];
    +  var JAVA_KEYWORDS = [COMMON_KEYWORDS,
    +      "abstract,boolean,byte,extends,final,finally,implements,import," +
    +      "instanceof,null,native,package,strictfp,super,synchronized,throws," +
    +      "transient"];
    +  var CSHARP_KEYWORDS = [JAVA_KEYWORDS,
    +      "as,base,by,checked,decimal,delegate,descending,dynamic,event," +
    +      "fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock," +
    +      "object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed," +
    +      "stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"];
    +  var COFFEE_KEYWORDS = "all,and,by,catch,class,else,extends,false,finally," +
    +      "for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then," +
    +      "true,try,unless,until,when,while,yes";
    +  var JSCRIPT_KEYWORDS = [COMMON_KEYWORDS,
    +      "debugger,eval,export,function,get,null,set,undefined,var,with," +
    +      "Infinity,NaN"];
    +  var PERL_KEYWORDS = "caller,delete,die,do,dump,elsif,eval,exit,foreach,for," +
    +      "goto,if,import,last,local,my,next,no,our,print,package,redo,require," +
    +      "sub,undef,unless,until,use,wantarray,while,BEGIN,END";
    +  var PYTHON_KEYWORDS = [FLOW_CONTROL_KEYWORDS, "and,as,assert,class,def,del," +
    +      "elif,except,exec,finally,from,global,import,in,is,lambda," +
    +      "nonlocal,not,or,pass,print,raise,try,with,yield," +
    +      "False,True,None"];
    +  var RUBY_KEYWORDS = [FLOW_CONTROL_KEYWORDS, "alias,and,begin,case,class," +
    +      "def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo," +
    +      "rescue,retry,self,super,then,true,undef,unless,until,when,yield," +
    +      "BEGIN,END"];
    +  var SH_KEYWORDS = [FLOW_CONTROL_KEYWORDS, "case,done,elif,esac,eval,fi," +
    +      "function,in,local,set,then,until"];
    +  var ALL_KEYWORDS = [
    +      CPP_KEYWORDS, CSHARP_KEYWORDS, JSCRIPT_KEYWORDS, PERL_KEYWORDS +
    +      PYTHON_KEYWORDS, RUBY_KEYWORDS, SH_KEYWORDS];
    +  var C_TYPES = /^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/;
    +
    +  // token style names.  correspond to css classes
    +  /**
    +   * token style for a string literal
    +   * @const
    +   */
    +  var PR_STRING = 'str';
    +  /**
    +   * token style for a keyword
    +   * @const
    +   */
    +  var PR_KEYWORD = 'kwd';
    +  /**
    +   * token style for a comment
    +   * @const
    +   */
    +  var PR_COMMENT = 'com';
    +  /**
    +   * token style for a type
    +   * @const
    +   */
    +  var PR_TYPE = 'typ';
    +  /**
    +   * token style for a literal value.  e.g. 1, null, true.
    +   * @const
    +   */
    +  var PR_LITERAL = 'lit';
    +  /**
    +   * token style for a punctuation string.
    +   * @const
    +   */
    +  var PR_PUNCTUATION = 'pun';
    +  /**
    +   * token style for a punctuation string.
    +   * @const
    +   */
    +  var PR_PLAIN = 'pln';
    +
    +  /**
    +   * token style for an sgml tag.
    +   * @const
    +   */
    +  var PR_TAG = 'tag';
    +  /**
    +   * token style for a markup declaration such as a DOCTYPE.
    +   * @const
    +   */
    +  var PR_DECLARATION = 'dec';
    +  /**
    +   * token style for embedded source.
    +   * @const
    +   */
    +  var PR_SOURCE = 'src';
    +  /**
    +   * token style for an sgml attribute name.
    +   * @const
    +   */
    +  var PR_ATTRIB_NAME = 'atn';
    +  /**
    +   * token style for an sgml attribute value.
    +   * @const
    +   */
    +  var PR_ATTRIB_VALUE = 'atv';
    +
    +  /**
    +   * A class that indicates a section of markup that is not code, e.g. to allow
    +   * embedding of line numbers within code listings.
    +   * @const
    +   */
    +  var PR_NOCODE = 'nocode';
    +
    +
    +
    +/**
    + * A set of tokens that can precede a regular expression literal in
    + * javascript
    + * http://web.archive.org/web/20070717142515/http://www.mozilla.org/js/language/js20/rationale/syntax.html
    + * has the full list, but I've removed ones that might be problematic when
    + * seen in languages that don't support regular expression literals.
    + *
    + * <p>Specifically, I've removed any keywords that can't precede a regexp
    + * literal in a syntactically legal javascript program, and I've removed the
    + * "in" keyword since it's not a keyword in many languages, and might be used
    + * as a count of inches.
    + *
    + * <p>The link a above does not accurately describe EcmaScript rules since
    + * it fails to distinguish between (a=++/b/i) and (a++/b/i) but it works
    + * very well in practice.
    + *
    + * @private
    + * @const
    + */
    +var REGEXP_PRECEDER_PATTERN = '(?:^^\\.?|[+-]|\\!|\\!=|\\!==|\\#|\\%|\\%=|&|&&|&&=|&=|\\(|\\*|\\*=|\\+=|\\,|\\-=|\\->|\\/|\\/=|:|::|\\;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|\\?|\\@|\\[|\\^|\\^=|\\^\\^|\\^\\^=|\\{|\\||\\|=|\\|\\||\\|\\|=|\\~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*';
    +
    +// CAVEAT: this does not properly handle the case where a regular
    +// expression immediately follows another since a regular expression may
    +// have flags for case-sensitivity and the like.  Having regexp tokens
    +// adjacent is not valid in any language I'm aware of, so I'm punting.
    +// TODO: maybe style special characters inside a regexp as punctuation.
    +
    +
    +  /**
    +   * Given a group of {@link RegExp}s, returns a {@code RegExp} that globally
    +   * matches the union of the sets of strings matched by the input RegExp.
    +   * Since it matches globally, if the input strings have a start-of-input
    +   * anchor (/^.../), it is ignored for the purposes of unioning.
    +   * @param {Array.<RegExp>} regexs non multiline, non-global regexs.
    +   * @return {RegExp} a global regex.
    +   */
    +  function combinePrefixPatterns(regexs) {
    +    var capturedGroupIndex = 0;
    +  
    +    var needToFoldCase = false;
    +    var ignoreCase = false;
    +    for (var i = 0, n = regexs.length; i < n; ++i) {
    +      var regex = regexs[i];
    +      if (regex.ignoreCase) {
    +        ignoreCase = true;
    +      } else if (/[a-z]/i.test(regex.source.replace(
    +                     /\\u[0-9a-f]{4}|\\x[0-9a-f]{2}|\\[^ux]/gi, ''))) {
    +        needToFoldCase = true;
    +        ignoreCase = false;
    +        break;
    +      }
    +    }
    +  
    +    var escapeCharToCodeUnit = {
    +      'b': 8,
    +      't': 9,
    +      'n': 0xa,
    +      'v': 0xb,
    +      'f': 0xc,
    +      'r': 0xd
    +    };
    +  
    +    function decodeEscape(charsetPart) {
    +      var cc0 = charsetPart.charCodeAt(0);
    +      if (cc0 !== 92 /* \\ */) {
    +        return cc0;
    +      }
    +      var c1 = charsetPart.charAt(1);
    +      cc0 = escapeCharToCodeUnit[c1];
    +      if (cc0) {
    +        return cc0;
    +      } else if ('0' <= c1 && c1 <= '7') {
    +        return parseInt(charsetPart.substring(1), 8);
    +      } else if (c1 === 'u' || c1 === 'x') {
    +        return parseInt(charsetPart.substring(2), 16);
    +      } else {
    +        return charsetPart.charCodeAt(1);
    +      }
    +    }
    +  
    +    function encodeEscape(charCode) {
    +      if (charCode < 0x20) {
    +        return (charCode < 0x10 ? '\\x0' : '\\x') + charCode.toString(16);
    +      }
    +      var ch = String.fromCharCode(charCode);
    +      if (ch === '\\' || ch === '-' || ch === '[' || ch === ']') {
    +        ch = '\\' + ch;
    +      }
    +      return ch;
    +    }
    +  
    +    function caseFoldCharset(charSet) {
    +      var charsetParts = charSet.substring(1, charSet.length - 1).match(
    +          new RegExp(
    +              '\\\\u[0-9A-Fa-f]{4}'
    +              + '|\\\\x[0-9A-Fa-f]{2}'
    +              + '|\\\\[0-3][0-7]{0,2}'
    +              + '|\\\\[0-7]{1,2}'
    +              + '|\\\\[\\s\\S]'
    +              + '|-'
    +              + '|[^-\\\\]',
    +              'g'));
    +      var groups = [];
    +      var ranges = [];
    +      var inverse = charsetParts[0] === '^';
    +      for (var i = inverse ? 1 : 0, n = charsetParts.length; i < n; ++i) {
    +        var p = charsetParts[i];
    +        if (/\\[bdsw]/i.test(p)) {  // Don't muck with named groups.
    +          groups.push(p);
    +        } else {
    +          var start = decodeEscape(p);
    +          var end;
    +          if (i + 2 < n && '-' === charsetParts[i + 1]) {
    +            end = decodeEscape(charsetParts[i + 2]);
    +            i += 2;
    +          } else {
    +            end = start;
    +          }
    +          ranges.push([start, end]);
    +          // If the range might intersect letters, then expand it.
    +          // This case handling is too simplistic.
    +          // It does not deal with non-latin case folding.
    +          // It works for latin source code identifiers though.
    +          if (!(end < 65 || start > 122)) {
    +            if (!(end < 65 || start > 90)) {
    +              ranges.push([Math.max(65, start) | 32, Math.min(end, 90) | 32]);
    +            }
    +            if (!(end < 97 || start > 122)) {
    +              ranges.push([Math.max(97, start) & ~32, Math.min(end, 122) & ~32]);
    +            }
    +          }
    +        }
    +      }
    +  
    +      // [[1, 10], [3, 4], [8, 12], [14, 14], [16, 16], [17, 17]]
    +      // -> [[1, 12], [14, 14], [16, 17]]
    +      ranges.sort(function (a, b) { return (a[0] - b[0]) || (b[1]  - a[1]); });
    +      var consolidatedRanges = [];
    +      var lastRange = [NaN, NaN];
    +      for (var i = 0; i < ranges.length; ++i) {
    +        var range = ranges[i];
    +        if (range[0] <= lastRange[1] + 1) {
    +          lastRange[1] = Math.max(lastRange[1], range[1]);
    +        } else {
    +          consolidatedRanges.push(lastRange = range);
    +        }
    +      }
    +  
    +      var out = ['['];
    +      if (inverse) { out.push('^'); }
    +      out.push.apply(out, groups);
    +      for (var i = 0; i < consolidatedRanges.length; ++i) {
    +        var range = consolidatedRanges[i];
    +        out.push(encodeEscape(range[0]));
    +        if (range[1] > range[0]) {
    +          if (range[1] + 1 > range[0]) { out.push('-'); }
    +          out.push(encodeEscape(range[1]));
    +        }
    +      }
    +      out.push(']');
    +      return out.join('');
    +    }
    +  
    +    function allowAnywhereFoldCaseAndRenumberGroups(regex) {
    +      // Split into character sets, escape sequences, punctuation strings
    +      // like ('(', '(?:', ')', '^'), and runs of characters that do not
    +      // include any of the above.
    +      var parts = regex.source.match(
    +          new RegExp(
    +              '(?:'
    +              + '\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]'  // a character set
    +              + '|\\\\u[A-Fa-f0-9]{4}'  // a unicode escape
    +              + '|\\\\x[A-Fa-f0-9]{2}'  // a hex escape
    +              + '|\\\\[0-9]+'  // a back-reference or octal escape
    +              + '|\\\\[^ux0-9]'  // other escape sequence
    +              + '|\\(\\?[:!=]'  // start of a non-capturing group
    +              + '|[\\(\\)\\^]'  // start/emd of a group, or line start
    +              + '|[^\\x5B\\x5C\\(\\)\\^]+'  // run of other characters
    +              + ')',
    +              'g'));
    +      var n = parts.length;
    +  
    +      // Maps captured group numbers to the number they will occupy in
    +      // the output or to -1 if that has not been determined, or to
    +      // undefined if they need not be capturing in the output.
    +      var capturedGroups = [];
    +  
    +      // Walk over and identify back references to build the capturedGroups
    +      // mapping.
    +      for (var i = 0, groupIndex = 0; i < n; ++i) {
    +        var p = parts[i];
    +        if (p === '(') {
    +          // groups are 1-indexed, so max group index is count of '('
    +          ++groupIndex;
    +        } else if ('\\' === p.charAt(0)) {
    +          var decimalValue = +p.substring(1);
    +          if (decimalValue && decimalValue <= groupIndex) {
    +            capturedGroups[decimalValue] = -1;
    +          }
    +        }
    +      }
    +  
    +      // Renumber groups and reduce capturing groups to non-capturing groups
    +      // where possible.
    +      for (var i = 1; i < capturedGroups.length; ++i) {
    +        if (-1 === capturedGroups[i]) {
    +          capturedGroups[i] = ++capturedGroupIndex;
    +        }
    +      }
    +      for (var i = 0, groupIndex = 0; i < n; ++i) {
    +        var p = parts[i];
    +        if (p === '(') {
    +          ++groupIndex;
    +          if (capturedGroups[groupIndex] === undefined) {
    +            parts[i] = '(?:';
    +          }
    +        } else if ('\\' === p.charAt(0)) {
    +          var decimalValue = +p.substring(1);
    +          if (decimalValue && decimalValue <= groupIndex) {
    +            parts[i] = '\\' + capturedGroups[groupIndex];
    +          }
    +        }
    +      }
    +  
    +      // Remove any prefix anchors so that the output will match anywhere.
    +      // ^^ really does mean an anchored match though.
    +      for (var i = 0, groupIndex = 0; i < n; ++i) {
    +        if ('^' === parts[i] && '^' !== parts[i + 1]) { parts[i] = ''; }
    +      }
    +  
    +      // Expand letters to groups to handle mixing of case-sensitive and
    +      // case-insensitive patterns if necessary.
    +      if (regex.ignoreCase && needToFoldCase) {
    +        for (var i = 0; i < n; ++i) {
    +          var p = parts[i];
    +          var ch0 = p.charAt(0);
    +          if (p.length >= 2 && ch0 === '[') {
    +            parts[i] = caseFoldCharset(p);
    +          } else if (ch0 !== '\\') {
    +            // TODO: handle letters in numeric escapes.
    +            parts[i] = p.replace(
    +                /[a-zA-Z]/g,
    +                function (ch) {
    +                  var cc = ch.charCodeAt(0);
    +                  return '[' + String.fromCharCode(cc & ~32, cc | 32) + ']';
    +                });
    +          }
    +        }
    +      }
    +  
    +      return parts.join('');
    +    }
    +  
    +    var rewritten = [];
    +    for (var i = 0, n = regexs.length; i < n; ++i) {
    +      var regex = regexs[i];
    +      if (regex.global || regex.multiline) { throw new Error('' + regex); }
    +      rewritten.push(
    +          '(?:' + allowAnywhereFoldCaseAndRenumberGroups(regex) + ')');
    +    }
    +  
    +    return new RegExp(rewritten.join('|'), ignoreCase ? 'gi' : 'g');
    +  }
    +
    +
    +  /**
    +   * Split markup into a string of source code and an array mapping ranges in
    +   * that string to the text nodes in which they appear.
    +   *
    +   * <p>
    +   * The HTML DOM structure:</p>
    +   * <pre>
    +   * (Element   "p"
    +   *   (Element "b"
    +   *     (Text  "print "))       ; #1
    +   *   (Text    "'Hello '")      ; #2
    +   *   (Element "br")            ; #3
    +   *   (Text    "  + 'World';")) ; #4
    +   * </pre>
    +   * <p>
    +   * corresponds to the HTML
    +   * {@code <p><b>print </b>'Hello '<br>  + 'World';</p>}.</p>
    +   *
    +   * <p>
    +   * It will produce the output:</p>
    +   * <pre>
    +   * {
    +   *   sourceCode: "print 'Hello '\n  + 'World';",
    +   *   //                 1         2
    +   *   //       012345678901234 5678901234567
    +   *   spans: [0, #1, 6, #2, 14, #3, 15, #4]
    +   * }
    +   * </pre>
    +   * <p>
    +   * where #1 is a reference to the {@code "print "} text node above, and so
    +   * on for the other text nodes.
    +   * </p>
    +   *
    +   * <p>
    +   * The {@code} spans array is an array of pairs.  Even elements are the start
    +   * indices of substrings, and odd elements are the text nodes (or BR elements)
    +   * that contain the text for those substrings.
    +   * Substrings continue until the next index or the end of the source.
    +   * </p>
    +   *
    +   * @param {Node} node an HTML DOM subtree containing source-code.
    +   * @return {Object} source code and the text nodes in which they occur.
    +   */
    +  function extractSourceSpans(node) {
    +    var nocode = /(?:^|\s)nocode(?:\s|$)/;
    +  
    +    var chunks = [];
    +    var length = 0;
    +    var spans = [];
    +    var k = 0;
    +  
    +    var whitespace;
    +    if (node.currentStyle) {
    +      whitespace = node.currentStyle.whiteSpace;
    +    } else if (window.getComputedStyle) {
    +      whitespace = document.defaultView.getComputedStyle(node, null)
    +          .getPropertyValue('white-space');
    +    }
    +    var isPreformatted = whitespace && 'pre' === whitespace.substring(0, 3);
    +  
    +    function walk(node) {
    +      switch (node.nodeType) {
    +        case 1:  // Element
    +          if (nocode.test(node.className)) { return; }
    +          for (var child = node.firstChild; child; child = child.nextSibling) {
    +            walk(child);
    +          }
    +          var nodeName = node.nodeName;
    +          if ('BR' === nodeName || 'LI' === nodeName) {
    +            chunks[k] = '\n';
    +            spans[k << 1] = length++;
    +            spans[(k++ << 1) | 1] = node;
    +          }
    +          break;
    +        case 3: case 4:  // Text
    +          var text = node.nodeValue;
    +          if (text.length) {
    +            if (!isPreformatted) {
    +              text = text.replace(/[ \t\r\n]+/g, ' ');
    +            } else {
    +              text = text.replace(/\r\n?/g, '\n');  // Normalize newlines.
    +            }
    +            // TODO: handle tabs here?
    +            chunks[k] = text;
    +            spans[k << 1] = length;
    +            length += text.length;
    +            spans[(k++ << 1) | 1] = node;
    +          }
    +          break;
    +      }
    +    }
    +  
    +    walk(node);
    +  
    +    return {
    +      sourceCode: chunks.join('').replace(/\n$/, ''),
    +      spans: spans
    +    };
    +  }
    +
    +
    +  /**
    +   * Apply the given language handler to sourceCode and add the resulting
    +   * decorations to out.
    +   * @param {number} basePos the index of sourceCode within the chunk of source
    +   *    whose decorations are already present on out.
    +   */
    +  function appendDecorations(basePos, sourceCode, langHandler, out) {
    +    if (!sourceCode) { return; }
    +    var job = {
    +      sourceCode: sourceCode,
    +      basePos: basePos
    +    };
    +    langHandler(job);
    +    out.push.apply(out, job.decorations);
    +  }
    +
    +  var notWs = /\S/;
    +
    +  /**
    +   * Given an element, if it contains only one child element and any text nodes
    +   * it contains contain only space characters, return the sole child element.
    +   * Otherwise returns undefined.
    +   * <p>
    +   * This is meant to return the CODE element in {@code <pre><code ...>} when
    +   * there is a single child element that contains all the non-space textual
    +   * content, but not to return anything where there are multiple child elements
    +   * as in {@code <pre><code>...</code><code>...</code></pre>} or when there
    +   * is textual content.
    +   */
    +  function childContentWrapper(element) {
    +    var wrapper = undefined;
    +    for (var c = element.firstChild; c; c = c.nextSibling) {
    +      var type = c.nodeType;
    +      wrapper = (type === 1)  // Element Node
    +          ? (wrapper ? element : c)
    +          : (type === 3)  // Text Node
    +          ? (notWs.test(c.nodeValue) ? element : wrapper)
    +          : wrapper;
    +    }
    +    return wrapper === element ? undefined : wrapper;
    +  }
    +
    +  /** Given triples of [style, pattern, context] returns a lexing function,
    +    * The lexing function interprets the patterns to find token boundaries and
    +    * returns a decoration list of the form
    +    * [index_0, style_0, index_1, style_1, ..., index_n, style_n]
    +    * where index_n is an index into the sourceCode, and style_n is a style
    +    * constant like PR_PLAIN.  index_n-1 <= index_n, and style_n-1 applies to
    +    * all characters in sourceCode[index_n-1:index_n].
    +    *
    +    * The stylePatterns is a list whose elements have the form
    +    * [style : string, pattern : RegExp, DEPRECATED, shortcut : string].
    +    *
    +    * Style is a style constant like PR_PLAIN, or can be a string of the
    +    * form 'lang-FOO', where FOO is a language extension describing the
    +    * language of the portion of the token in $1 after pattern executes.
    +    * E.g., if style is 'lang-lisp', and group 1 contains the text
    +    * '(hello (world))', then that portion of the token will be passed to the
    +    * registered lisp handler for formatting.
    +    * The text before and after group 1 will be restyled using this decorator
    +    * so decorators should take care that this doesn't result in infinite
    +    * recursion.  For example, the HTML lexer rule for SCRIPT elements looks
    +    * something like ['lang-js', /<[s]cript>(.+?)<\/script>/].  This may match
    +    * '<script>foo()<\/script>', which would cause the current decorator to
    +    * be called with '<script>' which would not match the same rule since
    +    * group 1 must not be empty, so it would be instead styled as PR_TAG by
    +    * the generic tag rule.  The handler registered for the 'js' extension would
    +    * then be called with 'foo()', and finally, the current decorator would
    +    * be called with '<\/script>' which would not match the original rule and
    +    * so the generic tag rule would identify it as a tag.
    +    *
    +    * Pattern must only match prefixes, and if it matches a prefix, then that
    +    * match is considered a token with the same style.
    +    *
    +    * Context is applied to the last non-whitespace, non-comment token
    +    * recognized.
    +    *
    +    * Shortcut is an optional string of characters, any of which, if the first
    +    * character, gurantee that this pattern and only this pattern matches.
    +    *
    +    * @param {Array} shortcutStylePatterns patterns that always start with
    +    *   a known character.  Must have a shortcut string.
    +    * @param {Array} fallthroughStylePatterns patterns that will be tried in
    +    *   order if the shortcut ones fail.  May have shortcuts.
    +    *
    +    * @return {function (Object)} a
    +    *   function that takes source code and returns a list of decorations.
    +    */
    +  function createSimpleLexer(shortcutStylePatterns, fallthroughStylePatterns) {
    +    var shortcuts = {};
    +    var tokenizer;
    +    (function () {
    +      var allPatterns = shortcutStylePatterns.concat(fallthroughStylePatterns);
    +      var allRegexs = [];
    +      var regexKeys = {};
    +      for (var i = 0, n = allPatterns.length; i < n; ++i) {
    +        var patternParts = allPatterns[i];
    +        var shortcutChars = patternParts[3];
    +        if (shortcutChars) {
    +          for (var c = shortcutChars.length; --c >= 0;) {
    +            shortcuts[shortcutChars.charAt(c)] = patternParts;
    +          }
    +        }
    +        var regex = patternParts[1];
    +        var k = '' + regex;
    +        if (!regexKeys.hasOwnProperty(k)) {
    +          allRegexs.push(regex);
    +          regexKeys[k] = null;
    +        }
    +      }
    +      allRegexs.push(/[\0-\uffff]/);
    +      tokenizer = combinePrefixPatterns(allRegexs);
    +    })();
    +
    +    var nPatterns = fallthroughStylePatterns.length;
    +
    +    /**
    +     * Lexes job.sourceCode and produces an output array job.decorations of
    +     * style classes preceded by the position at which they start in
    +     * job.sourceCode in order.
    +     *
    +     * @param {Object} job an object like <pre>{
    +     *    sourceCode: {string} sourceText plain text,
    +     *    basePos: {int} position of job.sourceCode in the larger chunk of
    +     *        sourceCode.
    +     * }</pre>
    +     */
    +    var decorate = function (job) {
    +      var sourceCode = job.sourceCode, basePos = job.basePos;
    +      /** Even entries are positions in source in ascending order.  Odd enties
    +        * are style markers (e.g., PR_COMMENT) that run from that position until
    +        * the end.
    +        * @type {Array.<number|string>}
    +        */
    +      var decorations = [basePos, PR_PLAIN];
    +      var pos = 0;  // index into sourceCode
    +      var tokens = sourceCode.match(tokenizer) || [];
    +      var styleCache = {};
    +
    +      for (var ti = 0, nTokens = tokens.length; ti < nTokens; ++ti) {
    +        var token = tokens[ti];
    +        var style = styleCache[token];
    +        var match = void 0;
    +
    +        var isEmbedded;
    +        if (typeof style === 'string') {
    +          isEmbedded = false;
    +        } else {
    +          var patternParts = shortcuts[token.charAt(0)];
    +          if (patternParts) {
    +            match = token.match(patternParts[1]);
    +            style = patternParts[0];
    +          } else {
    +            for (var i = 0; i < nPatterns; ++i) {
    +              patternParts = fallthroughStylePatterns[i];
    +              match = token.match(patternParts[1]);
    +              if (match) {
    +                style = patternParts[0];
    +                break;
    +              }
    +            }
    +
    +            if (!match) {  // make sure that we make progress
    +              style = PR_PLAIN;
    +            }
    +          }
    +
    +          isEmbedded = style.length >= 5 && 'lang-' === style.substring(0, 5);
    +          if (isEmbedded && !(match && typeof match[1] === 'string')) {
    +            isEmbedded = false;
    +            style = PR_SOURCE;
    +          }
    +
    +          if (!isEmbedded) { styleCache[token] = style; }
    +        }
    +
    +        var tokenStart = pos;
    +        pos += token.length;
    +
    +        if (!isEmbedded) {
    +          decorations.push(basePos + tokenStart, style);
    +        } else {  // Treat group 1 as an embedded block of source code.
    +          var embeddedSource = match[1];
    +          var embeddedSourceStart = token.indexOf(embeddedSource);
    +          var embeddedSourceEnd = embeddedSourceStart + embeddedSource.length;
    +          if (match[2]) {
    +            // If embeddedSource can be blank, then it would match at the
    +            // beginning which would cause us to infinitely recurse on the
    +            // entire token, so we catch the right context in match[2].
    +            embeddedSourceEnd = token.length - match[2].length;
    +            embeddedSourceStart = embeddedSourceEnd - embeddedSource.length;
    +          }
    +          var lang = style.substring(5);
    +          // Decorate the left of the embedded source
    +          appendDecorations(
    +              basePos + tokenStart,
    +              token.substring(0, embeddedSourceStart),
    +              decorate, decorations);
    +          // Decorate the embedded source
    +          appendDecorations(
    +              basePos + tokenStart + embeddedSourceStart,
    +              embeddedSource,
    +              langHandlerForExtension(lang, embeddedSource),
    +              decorations);
    +          // Decorate the right of the embedded section
    +          appendDecorations(
    +              basePos + tokenStart + embeddedSourceEnd,
    +              token.substring(embeddedSourceEnd),
    +              decorate, decorations);
    +        }
    +      }
    +      job.decorations = decorations;
    +    };
    +    return decorate;
    +  }
    +
    +  /** returns a function that produces a list of decorations from source text.
    +    *
    +    * This code treats ", ', and ` as string delimiters, and \ as a string
    +    * escape.  It does not recognize perl's qq() style strings.
    +    * It has no special handling for double delimiter escapes as in basic, or
    +    * the tripled delimiters used in python, but should work on those regardless
    +    * although in those cases a single string literal may be broken up into
    +    * multiple adjacent string literals.
    +    *
    +    * It recognizes C, C++, and shell style comments.
    +    *
    +    * @param {Object} options a set of optional parameters.
    +    * @return {function (Object)} a function that examines the source code
    +    *     in the input job and builds the decoration list.
    +    */
    +  function sourceDecorator(options) {
    +    var shortcutStylePatterns = [], fallthroughStylePatterns = [];
    +    if (options['tripleQuotedStrings']) {
    +      // '''multi-line-string''', 'single-line-string', and double-quoted
    +      shortcutStylePatterns.push(
    +          [PR_STRING,  /^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/,
    +           null, '\'"']);
    +    } else if (options['multiLineStrings']) {
    +      // 'multi-line-string', "multi-line-string"
    +      shortcutStylePatterns.push(
    +          [PR_STRING,  /^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/,
    +           null, '\'"`']);
    +    } else {
    +      // 'single-line-string', "single-line-string"
    +      shortcutStylePatterns.push(
    +          [PR_STRING,
    +           /^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/,
    +           null, '"\'']);
    +    }
    +    if (options['verbatimStrings']) {
    +      // verbatim-string-literal production from the C# grammar.  See issue 93.
    +      fallthroughStylePatterns.push(
    +          [PR_STRING, /^@\"(?:[^\"]|\"\")*(?:\"|$)/, null]);
    +    }
    +    var hc = options['hashComments'];
    +    if (hc) {
    +      if (options['cStyleComments']) {
    +        if (hc > 1) {  // multiline hash comments
    +          shortcutStylePatterns.push(
    +              [PR_COMMENT, /^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/, null, '#']);
    +        } else {
    +          // Stop C preprocessor declarations at an unclosed open comment
    +          shortcutStylePatterns.push(
    +              [PR_COMMENT, /^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\r\n]*)/,
    +               null, '#']);
    +        }
    +        fallthroughStylePatterns.push(
    +            [PR_STRING,
    +             /^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,
    +             null]);
    +      } else {
    +        shortcutStylePatterns.push([PR_COMMENT, /^#[^\r\n]*/, null, '#']);
    +      }
    +    }
    +    if (options['cStyleComments']) {
    +      fallthroughStylePatterns.push([PR_COMMENT, /^\/\/[^\r\n]*/, null]);
    +      fallthroughStylePatterns.push(
    +          [PR_COMMENT, /^\/\*[\s\S]*?(?:\*\/|$)/, null]);
    +    }
    +    if (options['regexLiterals']) {
    +      /**
    +       * @const
    +       */
    +      var REGEX_LITERAL = (
    +          // A regular expression literal starts with a slash that is
    +          // not followed by * or / so that it is not confused with
    +          // comments.
    +          '/(?=[^/*])'
    +          // and then contains any number of raw characters,
    +          + '(?:[^/\\x5B\\x5C]'
    +          // escape sequences (\x5C),
    +          +    '|\\x5C[\\s\\S]'
    +          // or non-nesting character sets (\x5B\x5D);
    +          +    '|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+'
    +          // finally closed by a /.
    +          + '/');
    +      fallthroughStylePatterns.push(
    +          ['lang-regex',
    +           new RegExp('^' + REGEXP_PRECEDER_PATTERN + '(' + REGEX_LITERAL + ')')
    +           ]);
    +    }
    +
    +    var types = options['types'];
    +    if (types) {
    +      fallthroughStylePatterns.push([PR_TYPE, types]);
    +    }
    +
    +    var keywords = ("" + options['keywords']).replace(/^ | $/g, '');
    +    if (keywords.length) {
    +      fallthroughStylePatterns.push(
    +          [PR_KEYWORD,
    +           new RegExp('^(?:' + keywords.replace(/[\s,]+/g, '|') + ')\\b'),
    +           null]);
    +    }
    +
    +    shortcutStylePatterns.push([PR_PLAIN,       /^\s+/, null, ' \r\n\t\xA0']);
    +    fallthroughStylePatterns.push(
    +        // TODO(mikesamuel): recognize non-latin letters and numerals in idents
    +        [PR_LITERAL,     /^@[a-z_$][a-z_$@0-9]*/i, null],
    +        [PR_TYPE,        /^(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/, null],
    +        [PR_PLAIN,       /^[a-z_$][a-z_$@0-9]*/i, null],
    +        [PR_LITERAL,
    +         new RegExp(
    +             '^(?:'
    +             // A hex number
    +             + '0x[a-f0-9]+'
    +             // or an octal or decimal number,
    +             + '|(?:\\d(?:_\\d+)*\\d*(?:\\.\\d*)?|\\.\\d\\+)'
    +             // possibly in scientific notation
    +             + '(?:e[+\\-]?\\d+)?'
    +             + ')'
    +             // with an optional modifier like UL for unsigned long
    +             + '[a-z]*', 'i'),
    +         null, '0123456789'],
    +        // Don't treat escaped quotes in bash as starting strings.  See issue 144.
    +        [PR_PLAIN,       /^\\[\s\S]?/, null],
    +        [PR_PUNCTUATION, /^.[^\s\w\.$@\'\"\`\/\#\\]*/, null]);
    +
    +    return createSimpleLexer(shortcutStylePatterns, fallthroughStylePatterns);
    +  }
    +
    +  var decorateSource = sourceDecorator({
    +        'keywords': ALL_KEYWORDS,
    +        'hashComments': true,
    +        'cStyleComments': true,
    +        'multiLineStrings': true,
    +        'regexLiterals': true
    +      });
    +
    +  /**
    +   * Given a DOM subtree, wraps it in a list, and puts each line into its own
    +   * list item.
    +   *
    +   * @param {Node} node modified in place.  Its content is pulled into an
    +   *     HTMLOListElement, and each line is moved into a separate list item.
    +   *     This requires cloning elements, so the input might not have unique
    +   *     IDs after numbering.
    +   */
    +  function numberLines(node, opt_startLineNum) {
    +    var nocode = /(?:^|\s)nocode(?:\s|$)/;
    +    var lineBreak = /\r\n?|\n/;
    +  
    +    var document = node.ownerDocument;
    +  
    +    var whitespace;
    +    if (node.currentStyle) {
    +      whitespace = node.currentStyle.whiteSpace;
    +    } else if (window.getComputedStyle) {
    +      whitespace = document.defaultView.getComputedStyle(node, null)
    +          .getPropertyValue('white-space');
    +    }
    +    // If it's preformatted, then we need to split lines on line breaks
    +    // in addition to <BR>s.
    +    var isPreformatted = whitespace && 'pre' === whitespace.substring(0, 3);
    +  
    +    var li = document.createElement('LI');
    +    while (node.firstChild) {
    +      li.appendChild(node.firstChild);
    +    }
    +    // An array of lines.  We split below, so this is initialized to one
    +    // un-split line.
    +    var listItems = [li];
    +  
    +    function walk(node) {
    +      switch (node.nodeType) {
    +        case 1:  // Element
    +          if (nocode.test(node.className)) { break; }
    +          if ('BR' === node.nodeName) {
    +            breakAfter(node);
    +            // Discard the <BR> since it is now flush against a </LI>.
    +            if (node.parentNode) {
    +              node.parentNode.removeChild(node);
    +            }
    +          } else {
    +            for (var child = node.firstChild; child; child = child.nextSibling) {
    +              walk(child);
    +            }
    +          }
    +          break;
    +        case 3: case 4:  // Text
    +          if (isPreformatted) {
    +            var text = node.nodeValue;
    +            var match = text.match(lineBreak);
    +            if (match) {
    +              var firstLine = text.substring(0, match.index);
    +              node.nodeValue = firstLine;
    +              var tail = text.substring(match.index + match[0].length);
    +              if (tail) {
    +                var parent = node.parentNode;
    +                parent.insertBefore(
    +                    document.createTextNode(tail), node.nextSibling);
    +              }
    +              breakAfter(node);
    +              if (!firstLine) {
    +                // Don't leave blank text nodes in the DOM.
    +                node.parentNode.removeChild(node);
    +              }
    +            }
    +          }
    +          break;
    +      }
    +    }
    +  
    +    // Split a line after the given node.
    +    function breakAfter(lineEndNode) {
    +      // If there's nothing to the right, then we can skip ending the line
    +      // here, and move root-wards since splitting just before an end-tag
    +      // would require us to create a bunch of empty copies.
    +      while (!lineEndNode.nextSibling) {
    +        lineEndNode = lineEndNode.parentNode;
    +        if (!lineEndNode) { return; }
    +      }
    +  
    +      function breakLeftOf(limit, copy) {
    +        // Clone shallowly if this node needs to be on both sides of the break.
    +        var rightSide = copy ? limit.cloneNode(false) : limit;
    +        var parent = limit.parentNode;
    +        if (parent) {
    +          // We clone the parent chain.
    +          // This helps us resurrect important styling elements that cross lines.
    +          // E.g. in <i>Foo<br>Bar</i>
    +          // should be rewritten to <li><i>Foo</i></li><li><i>Bar</i></li>.
    +          var parentClone = breakLeftOf(parent, 1);
    +          // Move the clone and everything to the right of the original
    +          // onto the cloned parent.
    +          var next = limit.nextSibling;
    +          parentClone.appendChild(rightSide);
    +          for (var sibling = next; sibling; sibling = next) {
    +            next = sibling.nextSibling;
    +            parentClone.appendChild(sibling);
    +          }
    +        }
    +        return rightSide;
    +      }
    +  
    +      var copiedListItem = breakLeftOf(lineEndNode.nextSibling, 0);
    +  
    +      // Walk the parent chain until we reach an unattached LI.
    +      for (var parent;
    +           // Check nodeType since IE invents document fragments.
    +           (parent = copiedListItem.parentNode) && parent.nodeType === 1;) {
    +        copiedListItem = parent;
    +      }
    +      // Put it on the list of lines for later processing.
    +      listItems.push(copiedListItem);
    +    }
    +  
    +    // Split lines while there are lines left to split.
    +    for (var i = 0;  // Number of lines that have been split so far.
    +         i < listItems.length;  // length updated by breakAfter calls.
    +         ++i) {
    +      walk(listItems[i]);
    +    }
    +  
    +    // Make sure numeric indices show correctly.
    +    if (opt_startLineNum === (opt_startLineNum|0)) {
    +      listItems[0].setAttribute('value', opt_startLineNum);
    +    }
    +  
    +    var ol = document.createElement('OL');
    +    ol.className = 'linenums';
    +    var offset = Math.max(0, ((opt_startLineNum - 1 /* zero index */)) | 0) || 0;
    +    for (var i = 0, n = listItems.length; i < n; ++i) {
    +      li = listItems[i];
    +      // Stick a class on the LIs so that stylesheets can
    +      // color odd/even rows, or any other row pattern that
    +      // is co-prime with 10.
    +      li.className = 'L' + ((i + offset) % 10);
    +      if (!li.firstChild) {
    +        li.appendChild(document.createTextNode('\xA0'));
    +      }
    +      ol.appendChild(li);
    +    }
    +    
    +    node.appendChild(ol);
    +  }
    +
    +  /**
    +   * Breaks {@code job.sourceCode} around style boundaries in
    +   * {@code job.decorations} and modifies {@code job.sourceNode} in place.
    +   * @param {Object} job like <pre>{
    +   *    sourceCode: {string} source as plain text,
    +   *    spans: {Array.<number|Node>} alternating span start indices into source
    +   *       and the text node or element (e.g. {@code <BR>}) corresponding to that
    +   *       span.
    +   *    decorations: {Array.<number|string} an array of style classes preceded
    +   *       by the position at which they start in job.sourceCode in order
    +   * }</pre>
    +   * @private
    +   */
    +  function recombineTagsAndDecorations(job) {
    +    var isIE = /\bMSIE\b/.test(navigator.userAgent);
    +    var newlineRe = /\n/g;
    +  
    +    var source = job.sourceCode;
    +    var sourceLength = source.length;
    +    // Index into source after the last code-unit recombined.
    +    var sourceIndex = 0;
    +  
    +    var spans = job.spans;
    +    var nSpans = spans.length;
    +    // Index into spans after the last span which ends at or before sourceIndex.
    +    var spanIndex = 0;
    +  
    +    var decorations = job.decorations;
    +    var nDecorations = decorations.length;
    +    // Index into decorations after the last decoration which ends at or before
    +    // sourceIndex.
    +    var decorationIndex = 0;
    +  
    +    // Remove all zero-length decorations.
    +    decorations[nDecorations] = sourceLength;
    +    var decPos, i;
    +    for (i = decPos = 0; i < nDecorations;) {
    +      if (decorations[i] !== decorations[i + 2]) {
    +        decorations[decPos++] = decorations[i++];
    +        decorations[decPos++] = decorations[i++];
    +      } else {
    +        i += 2;
    +      }
    +    }
    +    nDecorations = decPos;
    +  
    +    // Simplify decorations.
    +    for (i = decPos = 0; i < nDecorations;) {
    +      var startPos = decorations[i];
    +      // Conflate all adjacent decorations that use the same style.
    +      var startDec = decorations[i + 1];
    +      var end = i + 2;
    +      while (end + 2 <= nDecorations && decorations[end + 1] === startDec) {
    +        end += 2;
    +      }
    +      decorations[decPos++] = startPos;
    +      decorations[decPos++] = startDec;
    +      i = end;
    +    }
    +  
    +    nDecorations = decorations.length = decPos;
    +  
    +    var decoration = null;
    +    while (spanIndex < nSpans) {
    +      var spanStart = spans[spanIndex];
    +      var spanEnd = spans[spanIndex + 2] || sourceLength;
    +  
    +      var decStart = decorations[decorationIndex];
    +      var decEnd = decorations[decorationIndex + 2] || sourceLength;
    +  
    +      var end = Math.min(spanEnd, decEnd);
    +  
    +      var textNode = spans[spanIndex + 1];
    +      var styledText;
    +      if (textNode.nodeType !== 1  // Don't muck with <BR>s or <LI>s
    +          // Don't introduce spans around empty text nodes.
    +          && (styledText = source.substring(sourceIndex, end))) {
    +        // This may seem bizarre, and it is.  Emitting LF on IE causes the
    +        // code to display with spaces instead of line breaks.
    +        // Emitting Windows standard issue linebreaks (CRLF) causes a blank
    +        // space to appear at the beginning of every line but the first.
    +        // Emitting an old Mac OS 9 line separator makes everything spiffy.
    +        if (isIE) { styledText = styledText.replace(newlineRe, '\r'); }
    +        textNode.nodeValue = styledText;
    +        var document = textNode.ownerDocument;
    +        var span = document.createElement('SPAN');
    +        span.className = decorations[decorationIndex + 1];
    +        var parentNode = textNode.parentNode;
    +        parentNode.replaceChild(span, textNode);
    +        span.appendChild(textNode);
    +        if (sourceIndex < spanEnd) {  // Split off a text node.
    +          spans[spanIndex + 1] = textNode
    +              // TODO: Possibly optimize by using '' if there's no flicker.
    +              = document.createTextNode(source.substring(end, spanEnd));
    +          parentNode.insertBefore(textNode, span.nextSibling);
    +        }
    +      }
    +  
    +      sourceIndex = end;
    +  
    +      if (sourceIndex >= spanEnd) {
    +        spanIndex += 2;
    +      }
    +      if (sourceIndex >= decEnd) {
    +        decorationIndex += 2;
    +      }
    +    }
    +  }
    +
    +
    +  /** Maps language-specific file extensions to handlers. */
    +  var langHandlerRegistry = {};
    +  /** Register a language handler for the given file extensions.
    +    * @param {function (Object)} handler a function from source code to a list
    +    *      of decorations.  Takes a single argument job which describes the
    +    *      state of the computation.   The single parameter has the form
    +    *      {@code {
    +    *        sourceCode: {string} as plain text.
    +    *        decorations: {Array.<number|string>} an array of style classes
    +    *                     preceded by the position at which they start in
    +    *                     job.sourceCode in order.
    +    *                     The language handler should assigned this field.
    +    *        basePos: {int} the position of source in the larger source chunk.
    +    *                 All positions in the output decorations array are relative
    +    *                 to the larger source chunk.
    +    *      } }
    +    * @param {Array.<string>} fileExtensions
    +    */
    +  function registerLangHandler(handler, fileExtensions) {
    +    for (var i = fileExtensions.length; --i >= 0;) {
    +      var ext = fileExtensions[i];
    +      if (!langHandlerRegistry.hasOwnProperty(ext)) {
    +        langHandlerRegistry[ext] = handler;
    +      } else if (window['console']) {
    +        console['warn']('cannot override language handler %s', ext);
    +      }
    +    }
    +  }
    +  function langHandlerForExtension(extension, source) {
    +    if (!(extension && langHandlerRegistry.hasOwnProperty(extension))) {
    +      // Treat it as markup if the first non whitespace character is a < and
    +      // the last non-whitespace character is a >.
    +      extension = /^\s*</.test(source)
    +          ? 'default-markup'
    +          : 'default-code';
    +    }
    +    return langHandlerRegistry[extension];
    +  }
    +  registerLangHandler(decorateSource, ['default-code']);
    +  registerLangHandler(
    +      createSimpleLexer(
    +          [],
    +          [
    +           [PR_PLAIN,       /^[^<?]+/],
    +           [PR_DECLARATION, /^<!\w[^>]*(?:>|$)/],
    +           [PR_COMMENT,     /^<\!--[\s\S]*?(?:-\->|$)/],
    +           // Unescaped content in an unknown language
    +           ['lang-',        /^<\?([\s\S]+?)(?:\?>|$)/],
    +           ['lang-',        /^<%([\s\S]+?)(?:%>|$)/],
    +           [PR_PUNCTUATION, /^(?:<[%?]|[%?]>)/],
    +           ['lang-',        /^<xmp\b[^>]*>([\s\S]+?)<\/xmp\b[^>]*>/i],
    +           // Unescaped content in javascript.  (Or possibly vbscript).
    +           ['lang-js',      /^<script\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i],
    +           // Contains unescaped stylesheet content
    +           ['lang-css',     /^<style\b[^>]*>([\s\S]*?)(<\/style\b[^>]*>)/i],
    +           ['lang-in.tag',  /^(<\/?[a-z][^<>]*>)/i]
    +          ]),
    +      ['default-markup', 'htm', 'html', 'mxml', 'xhtml', 'xml', 'xsl']);
    +  registerLangHandler(
    +      createSimpleLexer(
    +          [
    +           [PR_PLAIN,        /^[\s]+/, null, ' \t\r\n'],
    +           [PR_ATTRIB_VALUE, /^(?:\"[^\"]*\"?|\'[^\']*\'?)/, null, '\"\'']
    +           ],
    +          [
    +           [PR_TAG,          /^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],
    +           [PR_ATTRIB_NAME,  /^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],
    +           ['lang-uq.val',   /^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],
    +           [PR_PUNCTUATION,  /^[=<>\/]+/],
    +           ['lang-js',       /^on\w+\s*=\s*\"([^\"]+)\"/i],
    +           ['lang-js',       /^on\w+\s*=\s*\'([^\']+)\'/i],
    +           ['lang-js',       /^on\w+\s*=\s*([^\"\'>\s]+)/i],
    +           ['lang-css',      /^style\s*=\s*\"([^\"]+)\"/i],
    +           ['lang-css',      /^style\s*=\s*\'([^\']+)\'/i],
    +           ['lang-css',      /^style\s*=\s*([^\"\'>\s]+)/i]
    +           ]),
    +      ['in.tag']);
    +  registerLangHandler(
    +      createSimpleLexer([], [[PR_ATTRIB_VALUE, /^[\s\S]+/]]), ['uq.val']);
    +  registerLangHandler(sourceDecorator({
    +          'keywords': CPP_KEYWORDS,
    +          'hashComments': true,
    +          'cStyleComments': true,
    +          'types': C_TYPES
    +        }), ['c', 'cc', 'cpp', 'cxx', 'cyc', 'm']);
    +  registerLangHandler(sourceDecorator({
    +          'keywords': 'null,true,false'
    +        }), ['json']);
    +  registerLangHandler(sourceDecorator({
    +          'keywords': CSHARP_KEYWORDS,
    +          'hashComments': true,
    +          'cStyleComments': true,
    +          'verbatimStrings': true,
    +          'types': C_TYPES
    +        }), ['cs']);
    +  registerLangHandler(sourceDecorator({
    +          'keywords': JAVA_KEYWORDS,
    +          'cStyleComments': true
    +        }), ['java']);
    +  registerLangHandler(sourceDecorator({
    +          'keywords': SH_KEYWORDS,
    +          'hashComments': true,
    +          'multiLineStrings': true
    +        }), ['bsh', 'csh', 'sh']);
    +  registerLangHandler(sourceDecorator({
    +          'keywords': PYTHON_KEYWORDS,
    +          'hashComments': true,
    +          'multiLineStrings': true,
    +          'tripleQuotedStrings': true
    +        }), ['cv', 'py']);
    +  registerLangHandler(sourceDecorator({
    +          'keywords': PERL_KEYWORDS,
    +          'hashComments': true,
    +          'multiLineStrings': true,
    +          'regexLiterals': true
    +        }), ['perl', 'pl', 'pm']);
    +  registerLangHandler(sourceDecorator({
    +          'keywords': RUBY_KEYWORDS,
    +          'hashComments': true,
    +          'multiLineStrings': true,
    +          'regexLiterals': true
    +        }), ['rb']);
    +  registerLangHandler(sourceDecorator({
    +          'keywords': JSCRIPT_KEYWORDS,
    +          'cStyleComments': true,
    +          'regexLiterals': true
    +        }), ['js']);
    +  registerLangHandler(sourceDecorator({
    +          'keywords': COFFEE_KEYWORDS,
    +          'hashComments': 3,  // ### style block comments
    +          'cStyleComments': true,
    +          'multilineStrings': true,
    +          'tripleQuotedStrings': true,
    +          'regexLiterals': true
    +        }), ['coffee']);
    +  registerLangHandler(createSimpleLexer([], [[PR_STRING, /^[\s\S]+/]]), ['regex']);
    +
    +  function applyDecorator(job) {
    +    var opt_langExtension = job.langExtension;
    +
    +    try {
    +      // Extract tags, and convert the source code to plain text.
    +      var sourceAndSpans = extractSourceSpans(job.sourceNode);
    +      /** Plain text. @type {string} */
    +      var source = sourceAndSpans.sourceCode;
    +      job.sourceCode = source;
    +      job.spans = sourceAndSpans.spans;
    +      job.basePos = 0;
    +
    +      // Apply the appropriate language handler
    +      langHandlerForExtension(opt_langExtension, source)(job);
    +
    +      // Integrate the decorations and tags back into the source code,
    +      // modifying the sourceNode in place.
    +      recombineTagsAndDecorations(job);
    +    } catch (e) {
    +      if ('console' in window) {
    +        console['log'](e && e['stack'] ? e['stack'] : e);
    +      }
    +    }
    +  }
    +
    +  /**
    +   * @param sourceCodeHtml {string} The HTML to pretty print.
    +   * @param opt_langExtension {string} The language name to use.
    +   *     Typically, a filename extension like 'cpp' or 'java'.
    +   * @param opt_numberLines {number|boolean} True to number lines,
    +   *     or the 1-indexed number of the first line in sourceCodeHtml.
    +   */
    +  function prettyPrintOne(sourceCodeHtml, opt_langExtension, opt_numberLines) {
    +    var container = document.createElement('PRE');
    +    // This could cause images to load and onload listeners to fire.
    +    // E.g. <img onerror="alert(1337)" src="nosuchimage.png">.
    +    // We assume that the inner HTML is from a trusted source.
    +    container.innerHTML = sourceCodeHtml;
    +    if (opt_numberLines) {
    +      numberLines(container, opt_numberLines);
    +    }
    +
    +    var job = {
    +      langExtension: opt_langExtension,
    +      numberLines: opt_numberLines,
    +      sourceNode: container
    +    };
    +    applyDecorator(job);
    +    return container.innerHTML;
    +  }
    +
    +  function prettyPrint(opt_whenDone) {
    +    function byTagName(tn) { return document.getElementsByTagName(tn); }
    +    // fetch a list of nodes to rewrite
    +    var codeSegments = [byTagName('pre'), byTagName('code'), byTagName('xmp')];
    +    var elements = [];
    +    for (var i = 0; i < codeSegments.length; ++i) {
    +      for (var j = 0, n = codeSegments[i].length; j < n; ++j) {
    +        elements.push(codeSegments[i][j]);
    +      }
    +    }
    +    codeSegments = null;
    +
    +    var clock = Date;
    +    if (!clock['now']) {
    +      clock = { 'now': function () { return +(new Date); } };
    +    }
    +
    +    // The loop is broken into a series of continuations to make sure that we
    +    // don't make the browser unresponsive when rewriting a large page.
    +    var k = 0;
    +    var prettyPrintingJob;
    +
    +    var langExtensionRe = /\blang(?:uage)?-([\w.]+)(?!\S)/;
    +    var prettyPrintRe = /\bprettyprint\b/;
    +
    +    function doWork() {
    +      var endTime = (window['PR_SHOULD_USE_CONTINUATION'] ?
    +                     clock['now']() + 250 /* ms */ :
    +                     Infinity);
    +      for (; k < elements.length && clock['now']() < endTime; k++) {
    +        var cs = elements[k];
    +        var className = cs.className;
    +        if (className.indexOf('prettyprint') >= 0) {
    +          // If the classes includes a language extensions, use it.
    +          // Language extensions can be specified like
    +          //     <pre class="prettyprint lang-cpp">
    +          // the language extension "cpp" is used to find a language handler as
    +          // passed to PR.registerLangHandler.
    +          // HTML5 recommends that a language be specified using "language-"
    +          // as the prefix instead.  Google Code Prettify supports both.
    +          // http://dev.w3.org/html5/spec-author-view/the-code-element.html
    +          var langExtension = className.match(langExtensionRe);
    +          // Support <pre class="prettyprint"><code class="language-c">
    +          var wrapper;
    +          if (!langExtension && (wrapper = childContentWrapper(cs))
    +              && "CODE" === wrapper.tagName) {
    +            langExtension = wrapper.className.match(langExtensionRe);
    +          }
    +
    +          if (langExtension) {
    +            langExtension = langExtension[1];
    +          }
    +
    +          // make sure this is not nested in an already prettified element
    +          var nested = false;
    +          for (var p = cs.parentNode; p; p = p.parentNode) {
    +            if ((p.tagName === 'pre' || p.tagName === 'code' ||
    +                 p.tagName === 'xmp') &&
    +                p.className && p.className.indexOf('prettyprint') >= 0) {
    +              nested = true;
    +              break;
    +            }
    +          }
    +          if (!nested) {
    +            // Look for a class like linenums or linenums:<n> where <n> is the
    +            // 1-indexed number of the first line.
    +            var lineNums = cs.className.match(/\blinenums\b(?::(\d+))?/);
    +            lineNums = lineNums
    +                  ? lineNums[1] && lineNums[1].length ? +lineNums[1] : true
    +                  : false;
    +            if (lineNums) { numberLines(cs, lineNums); }
    +
    +            // do the pretty printing
    +            prettyPrintingJob = {
    +              langExtension: langExtension,
    +              sourceNode: cs,
    +              numberLines: lineNums
    +            };
    +            applyDecorator(prettyPrintingJob);
    +          }
    +        }
    +      }
    +      if (k < elements.length) {
    +        // finish up in a continuation
    +        setTimeout(doWork, 250);
    +      } else if (opt_whenDone) {
    +        opt_whenDone();
    +      }
    +    }
    +
    +    doWork();
    +  }
    +
    +   /**
    +    * Find all the {@code <pre>} and {@code <code>} tags in the DOM with
    +    * {@code class=prettyprint} and prettify them.
    +    *
    +    * @param {Function?} opt_whenDone if specified, called when the last entry
    +    *     has been finished.
    +    */
    +  window['prettyPrintOne'] = prettyPrintOne;
    +   /**
    +    * Pretty print a chunk of code.
    +    *
    +    * @param {string} sourceCodeHtml code as html
    +    * @return {string} code as html, but prettier
    +    */
    +  window['prettyPrint'] = prettyPrint;
    +   /**
    +    * Contains functions for creating and registering new language handlers.
    +    * @type {Object}
    +    */
    +  window['PR'] = {
    +        'createSimpleLexer': createSimpleLexer,
    +        'registerLangHandler': registerLangHandler,
    +        'sourceDecorator': sourceDecorator,
    +        'PR_ATTRIB_NAME': PR_ATTRIB_NAME,
    +        'PR_ATTRIB_VALUE': PR_ATTRIB_VALUE,
    +        'PR_COMMENT': PR_COMMENT,
    +        'PR_DECLARATION': PR_DECLARATION,
    +        'PR_KEYWORD': PR_KEYWORD,
    +        'PR_LITERAL': PR_LITERAL,
    +        'PR_NOCODE': PR_NOCODE,
    +        'PR_PLAIN': PR_PLAIN,
    +        'PR_PUNCTUATION': PR_PUNCTUATION,
    +        'PR_SOURCE': PR_SOURCE,
    +        'PR_STRING': PR_STRING,
    +        'PR_TAG': PR_TAG,
    +        'PR_TYPE': PR_TYPE
    +      };
    +})();
    diff --git a/js/sjcl.js b/js/sjcl.js
    index 3f440702..601ebba9 100644
    --- a/js/sjcl.js
    +++ b/js/sjcl.js
    @@ -1,41 +1,42 @@
     "use strict";var sjcl={cipher:{},hash:{},keyexchange:{},mode:{},misc:{},codec:{},exception:{corrupt:function(a){this.toString=function(){return"CORRUPT: "+this.message};this.message=a},invalid:function(a){this.toString=function(){return"INVALID: "+this.message};this.message=a},bug:function(a){this.toString=function(){return"BUG: "+this.message};this.message=a},notReady:function(a){this.toString=function(){return"NOT READY: "+this.message};this.message=a}}};
     if(typeof module!="undefined"&&module.exports)module.exports=sjcl;
    -sjcl.cipher.aes=function(a){this.h[0][0][0]||this.w();var b,c,d,e,f=this.h[0][4],g=this.h[1];b=a.length;var h=1;if(b!==4&&b!==6&&b!==8)throw new sjcl.exception.invalid("invalid aes key size");this.a=[d=a.slice(0),e=[]];for(a=b;a<4*b+28;a++){c=d[a-1];if(a%b===0||b===8&&a%b===4){c=f[c>>>24]<<24^f[c>>16&255]<<16^f[c>>8&255]<<8^f[c&255];if(a%b===0){c=c<<8^c>>>24^h<<24;h=h<<1^(h>>7)*283}}d[a]=d[a-b]^c}for(b=0;a;b++,a--){c=d[b&3?a:a-4];e[b]=a<=4||b<4?c:g[0][f[c>>>24]]^g[1][f[c>>16&255]]^g[2][f[c>>8&255]]^
    +sjcl.cipher.aes=function(a){this.h[0][0][0]||this.z();var b,c,d,e,f=this.h[0][4],g=this.h[1];b=a.length;var h=1;if(b!==4&&b!==6&&b!==8)throw new sjcl.exception.invalid("invalid aes key size");this.a=[d=a.slice(0),e=[]];for(a=b;a<4*b+28;a++){c=d[a-1];if(a%b===0||b===8&&a%b===4){c=f[c>>>24]<<24^f[c>>16&255]<<16^f[c>>8&255]<<8^f[c&255];if(a%b===0){c=c<<8^c>>>24^h<<24;h=h<<1^(h>>7)*283}}d[a]=d[a-b]^c}for(b=0;a;b++,a--){c=d[b&3?a:a-4];e[b]=a<=4||b<4?c:g[0][f[c>>>24]]^g[1][f[c>>16&255]]^g[2][f[c>>8&255]]^
     g[3][f[c&255]]}};
    -sjcl.cipher.aes.prototype={encrypt:function(a){return this.H(a,0)},decrypt:function(a){return this.H(a,1)},h:[[[],[],[],[],[]],[[],[],[],[],[]]],w:function(){var a=this.h[0],b=this.h[1],c=a[4],d=b[4],e,f,g,h=[],i=[],k,j,l,m;for(e=0;e<0x100;e++)i[(h[e]=e<<1^(e>>7)*283)^e]=e;for(f=g=0;!c[f];f^=k||1,g=i[g]||1){l=g^g<<1^g<<2^g<<3^g<<4;l=l>>8^l&255^99;c[f]=l;d[l]=f;j=h[e=h[k=h[f]]];m=j*0x1010101^e*0x10001^k*0x101^f*0x1010100;j=h[l]*0x101^l*0x1010100;for(e=0;e<4;e++){a[e][f]=j=j<<24^j>>>8;b[e][l]=m=m<<24^m>>>8}}for(e=
    -0;e<5;e++){a[e]=a[e].slice(0);b[e]=b[e].slice(0)}},H:function(a,b){if(a.length!==4)throw new sjcl.exception.invalid("invalid aes block size");var c=this.a[b],d=a[0]^c[0],e=a[b?3:1]^c[1],f=a[2]^c[2];a=a[b?1:3]^c[3];var g,h,i,k=c.length/4-2,j,l=4,m=[0,0,0,0];g=this.h[b];var n=g[0],o=g[1],p=g[2],q=g[3],r=g[4];for(j=0;j<k;j++){g=n[d>>>24]^o[e>>16&255]^p[f>>8&255]^q[a&255]^c[l];h=n[e>>>24]^o[f>>16&255]^p[a>>8&255]^q[d&255]^c[l+1];i=n[f>>>24]^o[a>>16&255]^p[d>>8&255]^q[e&255]^c[l+2];a=n[a>>>24]^o[d>>16&
    +sjcl.cipher.aes.prototype={encrypt:function(a){return this.I(a,0)},decrypt:function(a){return this.I(a,1)},h:[[[],[],[],[],[]],[[],[],[],[],[]]],z:function(){var a=this.h[0],b=this.h[1],c=a[4],d=b[4],e,f,g,h=[],i=[],k,j,l,m;for(e=0;e<0x100;e++)i[(h[e]=e<<1^(e>>7)*283)^e]=e;for(f=g=0;!c[f];f^=k||1,g=i[g]||1){l=g^g<<1^g<<2^g<<3^g<<4;l=l>>8^l&255^99;c[f]=l;d[l]=f;j=h[e=h[k=h[f]]];m=j*0x1010101^e*0x10001^k*0x101^f*0x1010100;j=h[l]*0x101^l*0x1010100;for(e=0;e<4;e++){a[e][f]=j=j<<24^j>>>8;b[e][l]=m=m<<24^m>>>8}}for(e=
    +0;e<5;e++){a[e]=a[e].slice(0);b[e]=b[e].slice(0)}},I:function(a,b){if(a.length!==4)throw new sjcl.exception.invalid("invalid aes block size");var c=this.a[b],d=a[0]^c[0],e=a[b?3:1]^c[1],f=a[2]^c[2];a=a[b?1:3]^c[3];var g,h,i,k=c.length/4-2,j,l=4,m=[0,0,0,0];g=this.h[b];var n=g[0],o=g[1],p=g[2],q=g[3],r=g[4];for(j=0;j<k;j++){g=n[d>>>24]^o[e>>16&255]^p[f>>8&255]^q[a&255]^c[l];h=n[e>>>24]^o[f>>16&255]^p[a>>8&255]^q[d&255]^c[l+1];i=n[f>>>24]^o[a>>16&255]^p[d>>8&255]^q[e&255]^c[l+2];a=n[a>>>24]^o[d>>16&
     255]^p[e>>8&255]^q[f&255]^c[l+3];l+=4;d=g;e=h;f=i}for(j=0;j<4;j++){m[b?3&-j:j]=r[d>>>24]<<24^r[e>>16&255]<<16^r[f>>8&255]<<8^r[a&255]^c[l++];g=d;d=e;e=f;f=a;a=g}return m}};
     sjcl.bitArray={bitSlice:function(a,b,c){a=sjcl.bitArray.P(a.slice(b/32),32-(b&31)).slice(1);return c===undefined?a:sjcl.bitArray.clamp(a,c-b)},extract:function(a,b,c){var d=Math.floor(-b-c&31);return((b+c-1^b)&-32?a[b/32|0]<<32-d^a[b/32+1|0]>>>d:a[b/32|0]>>>d)&(1<<c)-1},concat:function(a,b){if(a.length===0||b.length===0)return a.concat(b);var c=a[a.length-1],d=sjcl.bitArray.getPartial(c);return d===32?a.concat(b):sjcl.bitArray.P(b,d,c|0,a.slice(0,a.length-1))},bitLength:function(a){var b=a.length;
     if(b===0)return 0;return(b-1)*32+sjcl.bitArray.getPartial(a[b-1])},clamp:function(a,b){if(a.length*32<b)return a;a=a.slice(0,Math.ceil(b/32));var c=a.length;b&=31;if(c>0&&b)a[c-1]=sjcl.bitArray.partial(b,a[c-1]&2147483648>>b-1,1);return a},partial:function(a,b,c){if(a===32)return b;return(c?b|0:b<<32-a)+a*0x10000000000},getPartial:function(a){return Math.round(a/0x10000000000)||32},equal:function(a,b){if(sjcl.bitArray.bitLength(a)!==sjcl.bitArray.bitLength(b))return false;var c=0,d;for(d=0;d<a.length;d++)c|=
     a[d]^b[d];return c===0},P:function(a,b,c,d){var e;e=0;if(d===undefined)d=[];for(;b>=32;b-=32){d.push(c);c=0}if(b===0)return d.concat(a);for(e=0;e<a.length;e++){d.push(c|a[e]>>>b);c=a[e]<<32-b}e=a.length?a[a.length-1]:0;a=sjcl.bitArray.getPartial(e);d.push(sjcl.bitArray.partial(b+a&31,b+a>32?c:d.pop(),1));return d},k:function(a,b){return[a[0]^b[0],a[1]^b[1],a[2]^b[2],a[3]^b[3]]}};
     sjcl.codec.utf8String={fromBits:function(a){var b="",c=sjcl.bitArray.bitLength(a),d,e;for(d=0;d<c/8;d++){if((d&3)===0)e=a[d/4];b+=String.fromCharCode(e>>>24);e<<=8}return decodeURIComponent(escape(b))},toBits:function(a){a=unescape(encodeURIComponent(a));var b=[],c,d=0;for(c=0;c<a.length;c++){d=d<<8|a.charCodeAt(c);if((c&3)===3){b.push(d);d=0}}c&3&&b.push(sjcl.bitArray.partial(8*(c&3),d));return b}};
     sjcl.codec.hex={fromBits:function(a){var b="",c;for(c=0;c<a.length;c++)b+=((a[c]|0)+0xf00000000000).toString(16).substr(4);return b.substr(0,sjcl.bitArray.bitLength(a)/4)},toBits:function(a){var b,c=[],d;a=a.replace(/\s|0x/g,"");d=a.length;a+="00000000";for(b=0;b<a.length;b+=8)c.push(parseInt(a.substr(b,8),16)^0);return sjcl.bitArray.clamp(c,d*4)}};
    -sjcl.codec.base64={D:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",fromBits:function(a,b,c){var d="",e=0,f=sjcl.codec.base64.D,g=0,h=sjcl.bitArray.bitLength(a);if(c)f=f.substr(0,62)+"-_";for(c=0;d.length*6<h;){d+=f.charAt((g^a[c]>>>e)>>>26);if(e<6){g=a[c]<<6-e;e+=26;c++}else{g<<=6;e-=6}}for(;d.length&3&&!b;)d+="=";return d},toBits:function(a,b){a=a.replace(/\s|=/g,"");var c=[],d=0,e=sjcl.codec.base64.D,f=0,g;if(b)e=e.substr(0,62)+"-_";for(b=0;b<a.length;b++){g=e.indexOf(a.charAt(b));
    -if(g<0)throw new sjcl.exception.invalid("this isn't base64!");if(d>26){d-=26;c.push(f^g>>>d);f=g<<32-d}else{d+=6;f^=g<<32-d}}d&56&&c.push(sjcl.bitArray.partial(d&56,f,1));return c}};sjcl.codec.base64url={fromBits:function(a){return sjcl.codec.base64.fromBits(a,1,1)},toBits:function(a){return sjcl.codec.base64.toBits(a,1)}};sjcl.hash.sha256=function(a){this.a[0]||this.w();if(a){this.n=a.n.slice(0);this.i=a.i.slice(0);this.e=a.e}else this.reset()};sjcl.hash.sha256.hash=function(a){return(new sjcl.hash.sha256).update(a).finalize()};
    -sjcl.hash.sha256.prototype={blockSize:512,reset:function(){this.n=this.N.slice(0);this.i=[];this.e=0;return this},update:function(a){if(typeof a==="string")a=sjcl.codec.utf8String.toBits(a);var b,c=this.i=sjcl.bitArray.concat(this.i,a);b=this.e;a=this.e=b+sjcl.bitArray.bitLength(a);for(b=512+b&-512;b<=a;b+=512)this.C(c.splice(0,16));return this},finalize:function(){var a,b=this.i,c=this.n;b=sjcl.bitArray.concat(b,[sjcl.bitArray.partial(1,1)]);for(a=b.length+2;a&15;a++)b.push(0);b.push(Math.floor(this.e/
    -4294967296));for(b.push(this.e|0);b.length;)this.C(b.splice(0,16));this.reset();return c},N:[],a:[],w:function(){function a(e){return(e-Math.floor(e))*0x100000000|0}var b=0,c=2,d;a:for(;b<64;c++){for(d=2;d*d<=c;d++)if(c%d===0)continue a;if(b<8)this.N[b]=a(Math.pow(c,0.5));this.a[b]=a(Math.pow(c,1/3));b++}},C:function(a){var b,c,d=a.slice(0),e=this.n,f=this.a,g=e[0],h=e[1],i=e[2],k=e[3],j=e[4],l=e[5],m=e[6],n=e[7];for(a=0;a<64;a++){if(a<16)b=d[a];else{b=d[a+1&15];c=d[a+14&15];b=d[a&15]=(b>>>7^b>>>18^
    +sjcl.codec.base64={F:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",fromBits:function(a,b,c){var d="",e=0,f=sjcl.codec.base64.F,g=0,h=sjcl.bitArray.bitLength(a);if(c)f=f.substr(0,62)+"-_";for(c=0;d.length*6<h;){d+=f.charAt((g^a[c]>>>e)>>>26);if(e<6){g=a[c]<<6-e;e+=26;c++}else{g<<=6;e-=6}}for(;d.length&3&&!b;)d+="=";return d},toBits:function(a,b){a=a.replace(/\s|=/g,"");var c=[],d=0,e=sjcl.codec.base64.F,f=0,g;if(b)e=e.substr(0,62)+"-_";for(b=0;b<a.length;b++){g=e.indexOf(a.charAt(b));
    +if(g<0)throw new sjcl.exception.invalid("this isn't base64!");if(d>26){d-=26;c.push(f^g>>>d);f=g<<32-d}else{d+=6;f^=g<<32-d}}d&56&&c.push(sjcl.bitArray.partial(d&56,f,1));return c}};sjcl.codec.base64url={fromBits:function(a){return sjcl.codec.base64.fromBits(a,1,1)},toBits:function(a){return sjcl.codec.base64.toBits(a,1)}};sjcl.hash.sha256=function(a){this.a[0]||this.z();if(a){this.n=a.n.slice(0);this.i=a.i.slice(0);this.e=a.e}else this.reset()};sjcl.hash.sha256.hash=function(a){return(new sjcl.hash.sha256).update(a).finalize()};
    +sjcl.hash.sha256.prototype={blockSize:512,reset:function(){this.n=this.N.slice(0);this.i=[];this.e=0;return this},update:function(a){if(typeof a==="string")a=sjcl.codec.utf8String.toBits(a);var b,c=this.i=sjcl.bitArray.concat(this.i,a);b=this.e;a=this.e=b+sjcl.bitArray.bitLength(a);for(b=512+b&-512;b<=a;b+=512)this.D(c.splice(0,16));return this},finalize:function(){var a,b=this.i,c=this.n;b=sjcl.bitArray.concat(b,[sjcl.bitArray.partial(1,1)]);for(a=b.length+2;a&15;a++)b.push(0);b.push(Math.floor(this.e/
    +4294967296));for(b.push(this.e|0);b.length;)this.D(b.splice(0,16));this.reset();return c},N:[],a:[],z:function(){function a(e){return(e-Math.floor(e))*0x100000000|0}var b=0,c=2,d;a:for(;b<64;c++){for(d=2;d*d<=c;d++)if(c%d===0)continue a;if(b<8)this.N[b]=a(Math.pow(c,0.5));this.a[b]=a(Math.pow(c,1/3));b++}},D:function(a){var b,c,d=a.slice(0),e=this.n,f=this.a,g=e[0],h=e[1],i=e[2],k=e[3],j=e[4],l=e[5],m=e[6],n=e[7];for(a=0;a<64;a++){if(a<16)b=d[a];else{b=d[a+1&15];c=d[a+14&15];b=d[a&15]=(b>>>7^b>>>18^
     b>>>3^b<<25^b<<14)+(c>>>17^c>>>19^c>>>10^c<<15^c<<13)+d[a&15]+d[a+9&15]|0}b=b+n+(j>>>6^j>>>11^j>>>25^j<<26^j<<21^j<<7)+(m^j&(l^m))+f[a];n=m;m=l;l=j;j=k+b|0;k=i;i=h;h=g;g=b+(h&i^k&(h^i))+(h>>>2^h>>>13^h>>>22^h<<30^h<<19^h<<10)|0}e[0]=e[0]+g|0;e[1]=e[1]+h|0;e[2]=e[2]+i|0;e[3]=e[3]+k|0;e[4]=e[4]+j|0;e[5]=e[5]+l|0;e[6]=e[6]+m|0;e[7]=e[7]+n|0}};
    -sjcl.mode.ccm={name:"ccm",encrypt:function(a,b,c,d,e){var f,g=b.slice(0),h=sjcl.bitArray,i=h.bitLength(c)/8,k=h.bitLength(g)/8;e=e||64;d=d||[];if(i<7)throw new sjcl.exception.invalid("ccm: iv must be at least 7 bytes");for(f=2;f<4&&k>>>8*f;f++);if(f<15-i)f=15-i;c=h.clamp(c,8*(15-f));b=sjcl.mode.ccm.G(a,b,c,d,e,f);g=sjcl.mode.ccm.I(a,g,c,b,e,f);return h.concat(g.data,g.tag)},decrypt:function(a,b,c,d,e){e=e||64;d=d||[];var f=sjcl.bitArray,g=f.bitLength(c)/8,h=f.bitLength(b),i=f.clamp(b,h-e),k=f.bitSlice(b,
    -h-e);h=(h-e)/8;if(g<7)throw new sjcl.exception.invalid("ccm: iv must be at least 7 bytes");for(b=2;b<4&&h>>>8*b;b++);if(b<15-g)b=15-g;c=f.clamp(c,8*(15-b));i=sjcl.mode.ccm.I(a,i,c,k,e,b);a=sjcl.mode.ccm.G(a,i.data,c,d,e,b);if(!f.equal(i.tag,a))throw new sjcl.exception.corrupt("ccm: tag doesn't match");return i.data},G:function(a,b,c,d,e,f){var g=[],h=sjcl.bitArray,i=h.k;e/=8;if(e%2||e<4||e>16)throw new sjcl.exception.invalid("ccm: invalid tag length");if(d.length>0xffffffff||b.length>0xffffffff)throw new sjcl.exception.bug("ccm: can't deal with 4GiB or more data");
    -f=[h.partial(8,(d.length?64:0)|e-2<<2|f-1)];f=h.concat(f,c);f[3]|=h.bitLength(b)/8;f=a.encrypt(f);if(d.length){c=h.bitLength(d)/8;if(c<=65279)g=[h.partial(16,c)];else if(c<=0xffffffff)g=h.concat([h.partial(16,65534)],[c]);g=h.concat(g,d);for(d=0;d<g.length;d+=4)f=a.encrypt(i(f,g.slice(d,d+4).concat([0,0,0])))}for(d=0;d<b.length;d+=4)f=a.encrypt(i(f,b.slice(d,d+4).concat([0,0,0])));return h.clamp(f,e*8)},I:function(a,b,c,d,e,f){var g,h=sjcl.bitArray;g=h.k;var i=b.length,k=h.bitLength(b);c=h.concat([h.partial(8,
    +sjcl.mode.ccm={name:"ccm",encrypt:function(a,b,c,d,e){var f,g=b.slice(0),h=sjcl.bitArray,i=h.bitLength(c)/8,k=h.bitLength(g)/8;e=e||64;d=d||[];if(i<7)throw new sjcl.exception.invalid("ccm: iv must be at least 7 bytes");for(f=2;f<4&&k>>>8*f;f++);if(f<15-i)f=15-i;c=h.clamp(c,8*(15-f));b=sjcl.mode.ccm.H(a,b,c,d,e,f);g=sjcl.mode.ccm.J(a,g,c,b,e,f);return h.concat(g.data,g.tag)},decrypt:function(a,b,c,d,e){e=e||64;d=d||[];var f=sjcl.bitArray,g=f.bitLength(c)/8,h=f.bitLength(b),i=f.clamp(b,h-e),k=f.bitSlice(b,
    +h-e);h=(h-e)/8;if(g<7)throw new sjcl.exception.invalid("ccm: iv must be at least 7 bytes");for(b=2;b<4&&h>>>8*b;b++);if(b<15-g)b=15-g;c=f.clamp(c,8*(15-b));i=sjcl.mode.ccm.J(a,i,c,k,e,b);a=sjcl.mode.ccm.H(a,i.data,c,d,e,b);if(!f.equal(i.tag,a))throw new sjcl.exception.corrupt("ccm: tag doesn't match");return i.data},H:function(a,b,c,d,e,f){var g=[],h=sjcl.bitArray,i=h.k;e/=8;if(e%2||e<4||e>16)throw new sjcl.exception.invalid("ccm: invalid tag length");if(d.length>0xffffffff||b.length>0xffffffff)throw new sjcl.exception.bug("ccm: can't deal with 4GiB or more data");
    +f=[h.partial(8,(d.length?64:0)|e-2<<2|f-1)];f=h.concat(f,c);f[3]|=h.bitLength(b)/8;f=a.encrypt(f);if(d.length){c=h.bitLength(d)/8;if(c<=65279)g=[h.partial(16,c)];else if(c<=0xffffffff)g=h.concat([h.partial(16,65534)],[c]);g=h.concat(g,d);for(d=0;d<g.length;d+=4)f=a.encrypt(i(f,g.slice(d,d+4).concat([0,0,0])))}for(d=0;d<b.length;d+=4)f=a.encrypt(i(f,b.slice(d,d+4).concat([0,0,0])));return h.clamp(f,e*8)},J:function(a,b,c,d,e,f){var g,h=sjcl.bitArray;g=h.k;var i=b.length,k=h.bitLength(b);c=h.concat([h.partial(8,
     f-1)],c).concat([0,0,0]).slice(0,4);d=h.bitSlice(g(d,a.encrypt(c)),0,e);if(!i)return{tag:d,data:[]};for(g=0;g<i;g+=4){c[3]++;e=a.encrypt(c);b[g]^=e[0];b[g+1]^=e[1];b[g+2]^=e[2];b[g+3]^=e[3]}return{tag:d,data:h.clamp(b,k)}}};
    -sjcl.mode.ocb2={name:"ocb2",encrypt:function(a,b,c,d,e,f){if(sjcl.bitArray.bitLength(c)!==128)throw new sjcl.exception.invalid("ocb iv must be 128 bits");var g,h=sjcl.mode.ocb2.A,i=sjcl.bitArray,k=i.k,j=[0,0,0,0];c=h(a.encrypt(c));var l,m=[];d=d||[];e=e||64;for(g=0;g+4<b.length;g+=4){l=b.slice(g,g+4);j=k(j,l);m=m.concat(k(c,a.encrypt(k(c,l))));c=h(c)}l=b.slice(g);b=i.bitLength(l);g=a.encrypt(k(c,[0,0,0,b]));l=i.clamp(k(l.concat([0,0,0]),g),b);j=k(j,k(l.concat([0,0,0]),g));j=a.encrypt(k(j,k(c,h(c))));
    -if(d.length)j=k(j,f?d:sjcl.mode.ocb2.pmac(a,d));return m.concat(i.concat(l,i.clamp(j,e)))},decrypt:function(a,b,c,d,e,f){if(sjcl.bitArray.bitLength(c)!==128)throw new sjcl.exception.invalid("ocb iv must be 128 bits");e=e||64;var g=sjcl.mode.ocb2.A,h=sjcl.bitArray,i=h.k,k=[0,0,0,0],j=g(a.encrypt(c)),l,m,n=sjcl.bitArray.bitLength(b)-e,o=[];d=d||[];for(c=0;c+4<n/32;c+=4){l=i(j,a.decrypt(i(j,b.slice(c,c+4))));k=i(k,l);o=o.concat(l);j=g(j)}m=n-c*32;l=a.encrypt(i(j,[0,0,0,m]));l=i(l,h.clamp(b.slice(c),
    -m).concat([0,0,0]));k=i(k,l);k=a.encrypt(i(k,i(j,g(j))));if(d.length)k=i(k,f?d:sjcl.mode.ocb2.pmac(a,d));if(!h.equal(h.clamp(k,e),h.bitSlice(b,n)))throw new sjcl.exception.corrupt("ocb: tag doesn't match");return o.concat(h.clamp(l,m))},pmac:function(a,b){var c,d=sjcl.mode.ocb2.A,e=sjcl.bitArray,f=e.k,g=[0,0,0,0],h=a.encrypt([0,0,0,0]);h=f(h,d(d(h)));for(c=0;c+4<b.length;c+=4){h=d(h);g=f(g,a.encrypt(f(h,b.slice(c,c+4))))}b=b.slice(c);if(e.bitLength(b)<128){h=f(h,d(h));b=e.concat(b,[2147483648|0,0,
    -0,0])}g=f(g,b);return a.encrypt(f(d(f(h,d(h))),g))},A:function(a){return[a[0]<<1^a[1]>>>31,a[1]<<1^a[2]>>>31,a[2]<<1^a[3]>>>31,a[3]<<1^(a[0]>>>31)*135]}};sjcl.misc.hmac=function(a,b){this.M=b=b||sjcl.hash.sha256;var c=[[],[]],d=b.prototype.blockSize/32;this.l=[new b,new b];if(a.length>d)a=b.hash(a);for(b=0;b<d;b++){c[0][b]=a[b]^909522486;c[1][b]=a[b]^1549556828}this.l[0].update(c[0]);this.l[1].update(c[1])};
    +sjcl.mode.ocb2={name:"ocb2",encrypt:function(a,b,c,d,e,f){if(sjcl.bitArray.bitLength(c)!==128)throw new sjcl.exception.invalid("ocb iv must be 128 bits");var g,h=sjcl.mode.ocb2.B,i=sjcl.bitArray,k=i.k,j=[0,0,0,0];c=h(a.encrypt(c));var l,m=[];d=d||[];e=e||64;for(g=0;g+4<b.length;g+=4){l=b.slice(g,g+4);j=k(j,l);m=m.concat(k(c,a.encrypt(k(c,l))));c=h(c)}l=b.slice(g);b=i.bitLength(l);g=a.encrypt(k(c,[0,0,0,b]));l=i.clamp(k(l.concat([0,0,0]),g),b);j=k(j,k(l.concat([0,0,0]),g));j=a.encrypt(k(j,k(c,h(c))));
    +if(d.length)j=k(j,f?d:sjcl.mode.ocb2.pmac(a,d));return m.concat(i.concat(l,i.clamp(j,e)))},decrypt:function(a,b,c,d,e,f){if(sjcl.bitArray.bitLength(c)!==128)throw new sjcl.exception.invalid("ocb iv must be 128 bits");e=e||64;var g=sjcl.mode.ocb2.B,h=sjcl.bitArray,i=h.k,k=[0,0,0,0],j=g(a.encrypt(c)),l,m,n=sjcl.bitArray.bitLength(b)-e,o=[];d=d||[];for(c=0;c+4<n/32;c+=4){l=i(j,a.decrypt(i(j,b.slice(c,c+4))));k=i(k,l);o=o.concat(l);j=g(j)}m=n-c*32;l=a.encrypt(i(j,[0,0,0,m]));l=i(l,h.clamp(b.slice(c),
    +m).concat([0,0,0]));k=i(k,l);k=a.encrypt(i(k,i(j,g(j))));if(d.length)k=i(k,f?d:sjcl.mode.ocb2.pmac(a,d));if(!h.equal(h.clamp(k,e),h.bitSlice(b,n)))throw new sjcl.exception.corrupt("ocb: tag doesn't match");return o.concat(h.clamp(l,m))},pmac:function(a,b){var c,d=sjcl.mode.ocb2.B,e=sjcl.bitArray,f=e.k,g=[0,0,0,0],h=a.encrypt([0,0,0,0]);h=f(h,d(d(h)));for(c=0;c+4<b.length;c+=4){h=d(h);g=f(g,a.encrypt(f(h,b.slice(c,c+4))))}b=b.slice(c);if(e.bitLength(b)<128){h=f(h,d(h));b=e.concat(b,[2147483648|0,0,
    +0,0])}g=f(g,b);return a.encrypt(f(d(f(h,d(h))),g))},B:function(a){return[a[0]<<1^a[1]>>>31,a[1]<<1^a[2]>>>31,a[2]<<1^a[3]>>>31,a[3]<<1^(a[0]>>>31)*135]}};sjcl.misc.hmac=function(a,b){this.M=b=b||sjcl.hash.sha256;var c=[[],[]],d=b.prototype.blockSize/32;this.l=[new b,new b];if(a.length>d)a=b.hash(a);for(b=0;b<d;b++){c[0][b]=a[b]^909522486;c[1][b]=a[b]^1549556828}this.l[0].update(c[0]);this.l[1].update(c[1])};
     sjcl.misc.hmac.prototype.encrypt=sjcl.misc.hmac.prototype.mac=function(a,b){a=(new this.M(this.l[0])).update(a,b).finalize();return(new this.M(this.l[1])).update(a).finalize()};
     sjcl.misc.pbkdf2=function(a,b,c,d,e){c=c||1E3;if(d<0||c<0)throw sjcl.exception.invalid("invalid params to pbkdf2");if(typeof a==="string")a=sjcl.codec.utf8String.toBits(a);e=e||sjcl.misc.hmac;a=new e(a);var f,g,h,i,k=[],j=sjcl.bitArray;for(i=1;32*k.length<(d||1);i++){e=f=a.encrypt(j.concat(b,[i]));for(g=1;g<c;g++){f=a.encrypt(f);for(h=0;h<f.length;h++)e[h]^=f[h]}k=k.concat(e)}if(d)k=j.clamp(k,d);return k};
    -sjcl.random={randomWords:function(a,b){var c=[];b=this.isReady(b);var d;if(b===0)throw new sjcl.exception.notReady("generator isn't seeded");else b&2&&this.U(!(b&1));for(b=0;b<a;b+=4){(b+1)%0x10000===0&&this.L();d=this.u();c.push(d[0],d[1],d[2],d[3])}this.L();return c.slice(0,a)},setDefaultParanoia:function(a){this.t=a},addEntropy:function(a,b,c){c=c||"user";var d,e,f=(new Date).valueOf(),g=this.q[c],h=this.isReady();d=this.F[c];if(d===undefined)d=this.F[c]=this.R++;if(g===undefined)g=this.q[c]=0;this.q[c]=
    -(this.q[c]+1)%this.b.length;switch(typeof a){case "number":break;case "object":if(b===undefined)for(c=b=0;c<a.length;c++)for(e=a[c];e>0;){b++;e>>>=1}this.b[g].update([d,this.J++,2,b,f,a.length].concat(a));break;case "string":if(b===undefined)b=a.length;this.b[g].update([d,this.J++,3,b,f,a.length]);this.b[g].update(a);break;default:throw new sjcl.exception.bug("random: addEntropy only supports number, array or string");}this.j[g]+=b;this.f+=b;if(h===0){this.isReady()!==0&&this.K("seeded",Math.max(this.g,
    -this.f));this.K("progress",this.getProgress())}},isReady:function(a){a=this.B[a!==undefined?a:this.t];return this.g&&this.g>=a?this.j[0]>80&&(new Date).valueOf()>this.O?3:1:this.f>=a?2:0},getProgress:function(a){a=this.B[a?a:this.t];return this.g>=a?1["0"]:this.f>a?1["0"]:this.f/a},startCollectors:function(){if(!this.m){if(window.addEventListener){window.addEventListener("load",this.o,false);window.addEventListener("mousemove",this.p,false)}else if(document.attachEvent){document.attachEvent("onload",
    -this.o);document.attachEvent("onmousemove",this.p)}else throw new sjcl.exception.bug("can't attach event");this.m=true}},stopCollectors:function(){if(this.m){if(window.removeEventListener){window.removeEventListener("load",this.o,false);window.removeEventListener("mousemove",this.p,false)}else if(window.detachEvent){window.detachEvent("onload",this.o);window.detachEvent("onmousemove",this.p)}this.m=false}},addEventListener:function(a,b){this.r[a][this.Q++]=b},removeEventListener:function(a,b){var c;
    -a=this.r[a];var d=[];for(c in a)a.hasOwnProperty(c)&&a[c]===b&&d.push(c);for(b=0;b<d.length;b++){c=d[b];delete a[c]}},b:[new sjcl.hash.sha256],j:[0],z:0,q:{},J:0,F:{},R:0,g:0,f:0,O:0,a:[0,0,0,0,0,0,0,0],d:[0,0,0,0],s:undefined,t:6,m:false,r:{progress:{},seeded:{}},Q:0,B:[0,48,64,96,128,192,0x100,384,512,768,1024],u:function(){for(var a=0;a<4;a++){this.d[a]=this.d[a]+1|0;if(this.d[a])break}return this.s.encrypt(this.d)},L:function(){this.a=this.u().concat(this.u());this.s=new sjcl.cipher.aes(this.a)},
    -T:function(a){this.a=sjcl.hash.sha256.hash(this.a.concat(a));this.s=new sjcl.cipher.aes(this.a);for(a=0;a<4;a++){this.d[a]=this.d[a]+1|0;if(this.d[a])break}},U:function(a){var b=[],c=0,d;this.O=b[0]=(new Date).valueOf()+3E4;for(d=0;d<16;d++)b.push(Math.random()*0x100000000|0);for(d=0;d<this.b.length;d++){b=b.concat(this.b[d].finalize());c+=this.j[d];this.j[d]=0;if(!a&&this.z&1<<d)break}if(this.z>=1<<this.b.length){this.b.push(new sjcl.hash.sha256);this.j.push(0)}this.f-=c;if(c>this.g)this.g=c;this.z++;
    -this.T(b)},p:function(a){sjcl.random.addEntropy([a.x||a.clientX||a.offsetX,a.y||a.clientY||a.offsetY],2,"mouse")},o:function(){sjcl.random.addEntropy(new Date,2,"loadtime")},K:function(a,b){var c;a=sjcl.random.r[a];var d=[];for(c in a)a.hasOwnProperty(c)&&d.push(a[c]);for(c=0;c<d.length;c++)d[c](b)}};try{var s=new Uint32Array(32);crypto.getRandomValues(s);sjcl.random.addEntropy(s,1024,"crypto['getRandomValues']")}catch(t){}
    +sjcl.random={randomWords:function(a,b){var c=[];b=this.isReady(b);var d;if(b===0)throw new sjcl.exception.notReady("generator isn't seeded");else b&2&&this.U(!(b&1));for(b=0;b<a;b+=4){(b+1)%0x10000===0&&this.L();d=this.w();c.push(d[0],d[1],d[2],d[3])}this.L();return c.slice(0,a)},setDefaultParanoia:function(a){this.t=a},addEntropy:function(a,b,c){c=c||"user";var d,e,f=(new Date).valueOf(),g=this.q[c],h=this.isReady(),i=0;d=this.G[c];if(d===undefined)d=this.G[c]=this.R++;if(g===undefined)g=this.q[c]=
    +0;this.q[c]=(this.q[c]+1)%this.b.length;switch(typeof a){case "number":if(b===undefined)b=1;this.b[g].update([d,this.u++,1,b,f,1,a|0]);break;case "object":c=Object.prototype.toString.call(a);if(c==="[object Uint32Array]"){e=[];for(c=0;c<a.length;c++)e.push(a[c]);a=e}else{if(c!=="[object Array]")i=1;for(c=0;c<a.length&&!i;c++)if(typeof a[c]!="number")i=1}if(!i){if(b===undefined)for(c=b=0;c<a.length;c++)for(e=a[c];e>0;){b++;e>>>=1}this.b[g].update([d,this.u++,2,b,f,a.length].concat(a))}break;case "string":if(b===
    +undefined)b=a.length;this.b[g].update([d,this.u++,3,b,f,a.length]);this.b[g].update(a);break;default:i=1}if(i)throw new sjcl.exception.bug("random: addEntropy only supports number, array of numbers or string");this.j[g]+=b;this.f+=b;if(h===0){this.isReady()!==0&&this.K("seeded",Math.max(this.g,this.f));this.K("progress",this.getProgress())}},isReady:function(a){a=this.C[a!==undefined?a:this.t];return this.g&&this.g>=a?this.j[0]>80&&(new Date).valueOf()>this.O?3:1:this.f>=a?2:0},getProgress:function(a){a=
    +this.C[a?a:this.t];return this.g>=a?1:this.f>a?1:this.f/a},startCollectors:function(){if(!this.m){if(window.addEventListener){window.addEventListener("load",this.o,false);window.addEventListener("mousemove",this.p,false)}else if(document.attachEvent){document.attachEvent("onload",this.o);document.attachEvent("onmousemove",this.p)}else throw new sjcl.exception.bug("can't attach event");this.m=true}},stopCollectors:function(){if(this.m){if(window.removeEventListener){window.removeEventListener("load",
    +this.o,false);window.removeEventListener("mousemove",this.p,false)}else if(window.detachEvent){window.detachEvent("onload",this.o);window.detachEvent("onmousemove",this.p)}this.m=false}},addEventListener:function(a,b){this.r[a][this.Q++]=b},removeEventListener:function(a,b){var c;a=this.r[a];var d=[];for(c in a)a.hasOwnProperty(c)&&a[c]===b&&d.push(c);for(b=0;b<d.length;b++){c=d[b];delete a[c]}},b:[new sjcl.hash.sha256],j:[0],A:0,q:{},u:0,G:{},R:0,g:0,f:0,O:0,a:[0,0,0,0,0,0,0,0],d:[0,0,0,0],s:undefined,
    +t:6,m:false,r:{progress:{},seeded:{}},Q:0,C:[0,48,64,96,128,192,0x100,384,512,768,1024],w:function(){for(var a=0;a<4;a++){this.d[a]=this.d[a]+1|0;if(this.d[a])break}return this.s.encrypt(this.d)},L:function(){this.a=this.w().concat(this.w());this.s=new sjcl.cipher.aes(this.a)},T:function(a){this.a=sjcl.hash.sha256.hash(this.a.concat(a));this.s=new sjcl.cipher.aes(this.a);for(a=0;a<4;a++){this.d[a]=this.d[a]+1|0;if(this.d[a])break}},U:function(a){var b=[],c=0,d;this.O=b[0]=(new Date).valueOf()+3E4;for(d=
    +0;d<16;d++)b.push(Math.random()*0x100000000|0);for(d=0;d<this.b.length;d++){b=b.concat(this.b[d].finalize());c+=this.j[d];this.j[d]=0;if(!a&&this.A&1<<d)break}if(this.A>=1<<this.b.length){this.b.push(new sjcl.hash.sha256);this.j.push(0)}this.f-=c;if(c>this.g)this.g=c;this.A++;this.T(b)},p:function(a){sjcl.random.addEntropy([a.x||a.clientX||a.offsetX||0,a.y||a.clientY||a.offsetY||0],2,"mouse")},o:function(){sjcl.random.addEntropy((new Date).valueOf(),2,"loadtime")},K:function(a,b){var c;a=sjcl.random.r[a];
    +var d=[];for(c in a)a.hasOwnProperty(c)&&d.push(a[c]);for(c=0;c<d.length;c++)d[c](b)}};try{var s=new Uint32Array(32);crypto.getRandomValues(s);sjcl.random.addEntropy(s,1024,"crypto['getRandomValues']")}catch(t){}
     sjcl.json={defaults:{v:1,iter:1E3,ks:128,ts:64,mode:"ccm",adata:"",cipher:"aes"},encrypt:function(a,b,c,d){c=c||{};d=d||{};var e=sjcl.json,f=e.c({iv:sjcl.random.randomWords(4,0)},e.defaults),g;e.c(f,c);c=f.adata;if(typeof f.salt==="string")f.salt=sjcl.codec.base64.toBits(f.salt);if(typeof f.iv==="string")f.iv=sjcl.codec.base64.toBits(f.iv);if(!sjcl.mode[f.mode]||!sjcl.cipher[f.cipher]||typeof a==="string"&&f.iter<=100||f.ts!==64&&f.ts!==96&&f.ts!==128||f.ks!==128&&f.ks!==192&&f.ks!==0x100||f.iv.length<
    -2||f.iv.length>4)throw new sjcl.exception.invalid("json encrypt: invalid parameters");if(typeof a==="string"){g=sjcl.misc.cachedPbkdf2(a,f);a=g.key.slice(0,f.ks/32);f.salt=g.salt}if(typeof b==="string")b=sjcl.codec.utf8String.toBits(b);if(typeof c==="string")c=sjcl.codec.utf8String.toBits(c);g=new sjcl.cipher[f.cipher](a);e.c(d,f);d.key=a;f.ct=sjcl.mode[f.mode].encrypt(g,b,f.iv,c,f.ts);return e.encode(e.V(f,e.defaults))},decrypt:function(a,b,c,d){c=c||{};d=d||{};var e=sjcl.json;b=e.c(e.c(e.c({},e.defaults),
    -e.decode(b)),c,true);var f;c=b.adata;if(typeof b.salt==="string")b.salt=sjcl.codec.base64.toBits(b.salt);if(typeof b.iv==="string")b.iv=sjcl.codec.base64.toBits(b.iv);if(!sjcl.mode[b.mode]||!sjcl.cipher[b.cipher]||typeof a==="string"&&b.iter<=100||b.ts!==64&&b.ts!==96&&b.ts!==128||b.ks!==128&&b.ks!==192&&b.ks!==0x100||!b.iv||b.iv.length<2||b.iv.length>4)throw new sjcl.exception.invalid("json decrypt: invalid parameters");if(typeof a==="string"){f=sjcl.misc.cachedPbkdf2(a,b);a=f.key.slice(0,b.ks/32);
    -b.salt=f.salt}if(typeof c==="string")c=sjcl.codec.utf8String.toBits(c);f=new sjcl.cipher[b.cipher](a);c=sjcl.mode[b.mode].decrypt(f,b.ct,b.iv,c,b.ts);e.c(d,b);d.key=a;return sjcl.codec.utf8String.fromBits(c)},encode:function(a){var b,c="{",d="";for(b in a)if(a.hasOwnProperty(b)){if(!b.match(/^[a-z0-9]+$/i))throw new sjcl.exception.invalid("json encode: invalid property name");c+=d+'"'+b+'":';d=",";switch(typeof a[b]){case "number":case "boolean":c+=a[b];break;case "string":c+='"'+escape(a[b])+'"';
    -break;case "object":c+='"'+sjcl.codec.base64.fromBits(a[b],1)+'"';break;default:throw new sjcl.exception.bug("json encode: unsupported type");}}return c+"}"},decode:function(a){a=a.replace(/\s/g,"");if(!a.match(/^\{.*\}$/))throw new sjcl.exception.invalid("json decode: this isn't json!");a=a.replace(/^\{|\}$/g,"").split(/,/);var b={},c,d;for(c=0;c<a.length;c++){if(!(d=a[c].match(/^(?:(["']?)([a-z][a-z0-9]*)\1):(?:(\d+)|"([a-z0-9+\/%*_.@=\-]*)")$/i)))throw new sjcl.exception.invalid("json decode: this isn't json!");
    -b[d[2]]=d[3]?parseInt(d[3],10):d[2].match(/^(ct|salt|iv)$/)?sjcl.codec.base64.toBits(d[4]):unescape(d[4])}return b},c:function(a,b,c){if(a===undefined)a={};if(b===undefined)return a;var d;for(d in b)if(b.hasOwnProperty(d)){if(c&&a[d]!==undefined&&a[d]!==b[d])throw new sjcl.exception.invalid("required parameter overridden");a[d]=b[d]}return a},V:function(a,b){var c={},d;for(d in a)if(a.hasOwnProperty(d)&&a[d]!==b[d])c[d]=a[d];return c},W:function(a,b){var c={},d;for(d=0;d<b.length;d++)if(a[b[d]]!==
    -undefined)c[b[d]]=a[b[d]];return c}};sjcl.encrypt=sjcl.json.encrypt;sjcl.decrypt=sjcl.json.decrypt;sjcl.misc.S={};sjcl.misc.cachedPbkdf2=function(a,b){var c=sjcl.misc.S,d;b=b||{};d=b.iter||1E3;c=c[a]=c[a]||{};d=c[d]=c[d]||{firstSalt:b.salt&&b.salt.length?b.salt.slice(0):sjcl.random.randomWords(2,0)};c=b.salt===undefined?d.firstSalt:b.salt;d[c]=d[c]||sjcl.misc.pbkdf2(a,c,b.iter);return{key:d[c].slice(0),salt:c.slice(0)}};
    +2||f.iv.length>4)throw new sjcl.exception.invalid("json encrypt: invalid parameters");if(typeof a==="string"){g=sjcl.misc.cachedPbkdf2(a,f);a=g.key.slice(0,f.ks/32);f.salt=g.salt}if(typeof b==="string")b=sjcl.codec.utf8String.toBits(b);if(typeof c==="string")c=sjcl.codec.utf8String.toBits(c);g=new sjcl.cipher[f.cipher](a);e.c(d,f);d.key=a;f.ct=sjcl.mode[f.mode].encrypt(g,b,f.iv,c,f.ts);return e.encode(f)},decrypt:function(a,b,c,d){c=c||{};d=d||{};var e=sjcl.json;b=e.c(e.c(e.c({},e.defaults),e.decode(b)),
    +c,true);var f;c=b.adata;if(typeof b.salt==="string")b.salt=sjcl.codec.base64.toBits(b.salt);if(typeof b.iv==="string")b.iv=sjcl.codec.base64.toBits(b.iv);if(!sjcl.mode[b.mode]||!sjcl.cipher[b.cipher]||typeof a==="string"&&b.iter<=100||b.ts!==64&&b.ts!==96&&b.ts!==128||b.ks!==128&&b.ks!==192&&b.ks!==0x100||!b.iv||b.iv.length<2||b.iv.length>4)throw new sjcl.exception.invalid("json decrypt: invalid parameters");if(typeof a==="string"){f=sjcl.misc.cachedPbkdf2(a,b);a=f.key.slice(0,b.ks/32);b.salt=f.salt}if(typeof c===
    +"string")c=sjcl.codec.utf8String.toBits(c);f=new sjcl.cipher[b.cipher](a);c=sjcl.mode[b.mode].decrypt(f,b.ct,b.iv,c,b.ts);e.c(d,b);d.key=a;return sjcl.codec.utf8String.fromBits(c)},encode:function(a){var b,c="{",d="";for(b in a)if(a.hasOwnProperty(b)){if(!b.match(/^[a-z0-9]+$/i))throw new sjcl.exception.invalid("json encode: invalid property name");c+=d+'"'+b+'":';d=",";switch(typeof a[b]){case "number":case "boolean":c+=a[b];break;case "string":c+='"'+escape(a[b])+'"';break;case "object":c+='"'+
    +sjcl.codec.base64.fromBits(a[b],1)+'"';break;default:throw new sjcl.exception.bug("json encode: unsupported type");}}return c+"}"},decode:function(a){a=a.replace(/\s/g,"");if(!a.match(/^\{.*\}$/))throw new sjcl.exception.invalid("json decode: this isn't json!");a=a.replace(/^\{|\}$/g,"").split(/,/);var b={},c,d;for(c=0;c<a.length;c++){if(!(d=a[c].match(/^(?:(["']?)([a-z][a-z0-9]*)\1):(?:(\d+)|"([a-z0-9+\/%*_.@=\-]*)")$/i)))throw new sjcl.exception.invalid("json decode: this isn't json!");b[d[2]]=
    +d[3]?parseInt(d[3],10):d[2].match(/^(ct|salt|iv)$/)?sjcl.codec.base64.toBits(d[4]):unescape(d[4])}return b},c:function(a,b,c){if(a===undefined)a={};if(b===undefined)return a;var d;for(d in b)if(b.hasOwnProperty(d)){if(c&&a[d]!==undefined&&a[d]!==b[d])throw new sjcl.exception.invalid("required parameter overridden");a[d]=b[d]}return a},W:function(a,b){var c={},d;for(d in a)if(a.hasOwnProperty(d)&&a[d]!==b[d])c[d]=a[d];return c},V:function(a,b){var c={},d;for(d=0;d<b.length;d++)if(a[b[d]]!==undefined)c[b[d]]=
    +a[b[d]];return c}};sjcl.encrypt=sjcl.json.encrypt;sjcl.decrypt=sjcl.json.decrypt;sjcl.misc.S={};sjcl.misc.cachedPbkdf2=function(a,b){var c=sjcl.misc.S,d;b=b||{};d=b.iter||1E3;c=c[a]=c[a]||{};d=c[d]=c[d]||{firstSalt:b.salt&&b.salt.length?b.salt.slice(0):sjcl.random.randomWords(2,0)};c=b.salt===undefined?d.firstSalt:b.salt;d[c]=d[c]||sjcl.misc.pbkdf2(a,c,b.iter);return{key:d[c].slice(0),salt:c.slice(0)}};
    diff --git a/js/zerobin.js b/js/zerobin.js
    index 23329a28..2392598b 100644
    --- a/js/zerobin.js
    +++ b/js/zerobin.js
    @@ -1,8 +1,12 @@
     /**
    - * ZeroBin 0.15
    + * ZeroBin
      *
    - * @link http://sebsauvage.net/wiki/doku.php?id=php:zerobin
    - * @author sebsauvage
    + * a zero-knowledge paste bin
    + *
    + * @link      http://sebsauvage.net/wiki/doku.php?id=php:zerobin
    + * @copyright 2012 Sébastien SAUVAGE (sebsauvage.net)
    + * @license   http://www.opensource.org/licenses/zlib-license.php The zlib/libpng License
    + * @version   0.15
      */
     
     // Immediately start random number generator collector.
    @@ -90,7 +94,7 @@ function setElementText(element, text) {
         if ($('div#oldienotice').is(":visible")) {
             // IE<10 do not support white-space:pre-wrap; so we have to do this BIG UGLY STINKING THING.
             element.text(text.replace(/\n/ig,'{BIG_UGLY_STINKING_THING__OH_GOD_I_HATE_IE}'));
    -        element.html(element.text().replace(/{BIG_UGLY_STINKING_THING__OH_GOD_I_HATE_IE}/ig,"\r\n<br>"));
    +        element.html(element.text().replace(/{BIG_UGLY_STINKING_THING__OH_GOD_I_HATE_IE}/ig,"\n<br />"));
         }
         // for other (sane) browsers:
         else {
    @@ -108,19 +112,22 @@ function displayMessages(key, comments) {
         try { // Try to decrypt the paste.
             var cleartext = zeroDecipher(key, comments[0].data);
         } catch(err) {
    -        $('div#cleartext').hide();
    -        $('button#clonebutton').hide();
    +        $('div#cleartext').addClass('hidden');
    +        $('div#prettymessage').addClass('hidden');
    +        $('button#clonebutton').addClass('hidden');
             showError('Could not decrypt data (Wrong key ?)');
             return;
         }
         setElementText($('div#cleartext'), cleartext);
    +    setElementText($('pre#prettyprint'), cleartext);
         urls2links($('div#cleartext')); // Convert URLs to clickable links.
    +    prettyPrint();
     
         // Display paste expiration.
    -    if (comments[0].meta.expire_date) $('div#remainingtime').removeClass('foryoureyesonly').text('This document will expire in '+secondsToHuman(comments[0].meta.remaining_time)+'.').show();
    +    if (comments[0].meta.expire_date) $('div#remainingtime').removeClass('foryoureyesonly').text('This document will expire in '+secondsToHuman(comments[0].meta.remaining_time)+'.').removeClass('hidden');
         if (comments[0].meta.burnafterreading) {
    -        $('div#remainingtime').addClass('foryoureyesonly').text('FOR YOUR EYES ONLY.  Don\'t close this window, this message can\'t be displayed again.').show();
    -        $('button#clonebutton').hide(); // Discourage cloning (as it can't really be prevented).
    +        $('div#remainingtime').addClass('foryoureyesonly').text('FOR YOUR EYES ONLY.  Don\'t close this window, this message can\'t be displayed again.').removeClass('hidden');
    +        $('button#clonebutton').addClass('hidden'); // Discourage cloning (as it can't really be prevented).
         }
     
         // If the discussion is opened on this paste, display it.
    @@ -141,10 +148,10 @@ function displayMessages(key, comments) {
                 if ($(cname).length) {
                     place = $(cname);
                 }
    -            var divComment = $('<div class="comment" id="comment_' + comment.meta.commentid+'">'
    +            var divComment = $('<article><div class="comment" id="comment_' + comment.meta.commentid+'">'
                                    + '<div class="commentmeta"><span class="nickname"></span><span class="commentdate"></span></div><div class="commentdata"></div>'
                                    + '<button onclick="open_reply($(this),\'' + comment.meta.commentid + '\');return false;">Reply</button>'
    -                               + '</div>');
    +                               + '</div></article>');
                 setElementText(divComment.find('div.commentdata'), cleartext);
                 // Convert URLs to clickable links in comment.
                 urls2links(divComment.find('div.commentdata'));
    @@ -154,7 +161,7 @@ function displayMessages(key, comments) {
                 try {
                     divComment.find('span.nickname').text(zeroDecipher(key, comment.meta.nickname));
                 } catch(err) { }
    -            divComment.find('span.commentdate').text('  ('+(new Date(comment.meta.postdate*1000).toUTCString())+')').attr('title','CommentID: ' + comment.meta.commentid);
    +            divComment.find('span.commentdate').text('  ('+(new Date(comment.meta.postdate*1000).toString())+')').attr('title','CommentID: ' + comment.meta.commentid);
     
                 // If an avatar is available, display it.
                 if (comment.meta.vizhash) {
    @@ -164,7 +171,7 @@ function displayMessages(key, comments) {
                 place.append(divComment);
             }
             $('div#comments').append('<div class="comment"><button onclick="open_reply($(this),\'' + pasteID() + '\');return false;">Add comment</button></div>');
    -        $('div#discussion').show();
    +        $('div#discussion').removeClass('hidden');
         }
     }
     
    @@ -178,11 +185,10 @@ function open_reply(source, commentid) {
         source.after('<div class="reply">'
                     + '<input type="text" id="nickname" title="Optional nickname..." value="Optional nickname..." />'
                     + '<textarea id="replymessage" class="replymessage" cols="80" rows="7"></textarea>'
    -                + '<br><button id="replybutton" onclick="send_comment(\'' + commentid + '\');return false;">Post comment</button>'
    -                + '<div id="replystatus">&nbsp;</div>'
    +                + '<br /><button id="replybutton" onclick="send_comment(\'' + commentid + '\');return false;">Post comment</button>'
    +                + '<div id="replystatus"> </div>'
                     + '</div>');
         $('input#nickname').focus(function() {
    -        $(this).css('color', '#000');
             if ($(this).val() == $(this).attr('title')) {
                 $(this).val('');
             }
    @@ -255,10 +261,12 @@ function send_data() {
                     stateExistingPaste();
                     var url = scriptLocation() + "?" + data.id + '#' + randomkey;
                     showStatus('');
    -                $('div#pastelink').html('Your paste is <a href="' + url + '">' + url + '</a>').show();
    +                $('div#pastelink').html('Your paste is <a href="' + url + '">' + url + '</a>').removeClass('hidden');
                     setElementText($('div#cleartext'), $('textarea#message').val());
    +                setElementText($('pre#prettyprint'), $('textarea#message').val());
                     urls2links($('div#cleartext'));
                     showStatus('');
    +                prettyPrint();
                 }
                 else if (data.status==1) {
                     showError('Could not create paste: '+data.message);
    @@ -273,44 +281,46 @@ function send_data() {
      * Put the screen in "New paste" mode.
      */
     function stateNewPaste() {
    -    $('button#sendbutton').show();
    -    $('button#clonebutton').hide();
    -    $('div#expiration').show();
    -    $('div#remainingtime').hide();
    -    $('div#language').hide(); // $('#language').show();
    -    $('input#password').hide(); //$('#password').show();
    -    $('div#opendisc').show();
    -    $('button#newbutton').show();
    -    $('div#pastelink').hide();
    +    $('button#sendbutton').removeClass('hidden');
    +    $('button#clonebutton').addClass('hidden');
    +    $('div#expiration').removeClass('hidden');
    +    $('div#remainingtime').addClass('hidden');
    +    $('div#language').addClass('hidden'); // $('#language').removeClass('hidden');
    +    $('input#password').addClass('hidden'); //$('#password').removeClass('hidden');
    +    $('div#opendisc').removeClass('hidden');
    +    $('button#newbutton').removeClass('hidden');
    +    $('div#pastelink').addClass('hidden');
         $('textarea#message').text('');
    -    $('textarea#message').show();
    -    $('div#cleartext').hide();
    -    $('div#message').focus();
    -    $('div#discussion').hide();
    +    $('textarea#message').removeClass('hidden');
    +    $('div#cleartext').addClass('hidden');
    +    $('textarea#message').focus();
    +    $('div#discussion').addClass('hidden');
    +    $('div#prettymessage').addClass('hidden');
     }
     
     /**
      * Put the screen in "Existing paste" mode.
      */
     function stateExistingPaste() {
    -    $('button#sendbutton').hide();
    +    $('button#sendbutton').addClass('hidden');
     
         // No "clone" for IE<10.
         if ($('div#oldienotice').is(":visible")) {
    -        $('button#clonebutton').hide();
    +        $('button#clonebutton').addClass('hidden');
         }
         else {
    -        $('button#clonebutton').show();
    +        $('button#clonebutton').removeClass('hidden');
         }
     
    -    $('div#expiration').hide();
    -    $('div#language').hide();
    -    $('input#password').hide();
    -    $('div#opendisc').hide();
    -    $('button#newbutton').show();
    -    $('div#pastelink').hide();
    -    $('textarea#message').hide();
    -    $('div#cleartext').show();
    +    $('div#expiration').addClass('hidden');
    +    $('div#language').addClass('hidden');
    +    $('input#password').addClass('hidden');
    +    $('div#opendisc').addClass('hidden');
    +    $('button#newbutton').removeClass('hidden');
    +    $('div#pastelink').addClass('hidden');
    +    $('textarea#message').addClass('hidden');
    +    $('div#cleartext').addClass('hidden');
    +    $('div#prettymessage').removeClass('hidden');
     }
     
     /**
    @@ -351,11 +361,11 @@ function showStatus(message, spin) {
         $('div#replystatus').removeClass('errorMessage');
         $('div#replystatus').text(message);
         if (!message) {
    -        $('div#status').html('&nbsp');
    +        $('div#status').html(' ');
             return;
         }
         if (message == '') {
    -        $('div#status').html('&nbsp');
    +        $('div#status').html(' ');
             return;
         }
         $('div#status').removeClass('errorMessage');
    @@ -409,6 +419,9 @@ function pageKey() {
     }
     
     $(function() {
    +    // hide "no javascript" message
    +    $('#noscript').hide();
    +
         $('select#pasteExpiration').change(function() {
             if ($(this).val() == 'burn') {
                 $('div#opendisc').addClass('buttondisabled');
    @@ -420,7 +433,6 @@ $(function() {
             }
         });
     
    -
         // Display an existing paste
         if ($('div#cipherdata').text().length > 1) {
             // Missing decryption key in URL ?
    diff --git a/lib/.htaccess b/lib/.htaccess
    new file mode 100644
    index 00000000..b584d98c
    --- /dev/null
    +++ b/lib/.htaccess
    @@ -0,0 +1,2 @@
    +Allow from none
    +Deny from all
    diff --git a/lib/rain.tpl.class.php b/lib/RainTPL.php
    similarity index 97%
    rename from lib/rain.tpl.class.php
    rename to lib/RainTPL.php
    index ea83b2c1..817e6450 100644
    --- a/lib/rain.tpl.class.php
    +++ b/lib/RainTPL.php
    @@ -21,7 +21,7 @@ class RainTPL{
     		 *
     		 * @var string
     		 */
    -		static $tpl_dir = "tpl/";
    +		static $tpl_dir = 'tpl/';
     
     
     		/**
    @@ -29,7 +29,7 @@ class RainTPL{
     		 *
     		 * @var string
     		 */
    -		static $cache_dir = "tmp/";
    +		static $cache_dir = 'tmp/';
     
     
     		/**
    @@ -81,10 +81,10 @@ class RainTPL{
     		 *
     		 */
     		static $check_template_update = true;
    -                
    +
     
     		/**
    -		 * PHP tags <? ?> 
    +		 * PHP tags <? ?>
     		 * True: php tags are enabled into the template
     		 * False: php tags are disabled into the template and rendered as html
     		 *
    @@ -92,7 +92,7 @@ class RainTPL{
     		 */
     		static $php_enabled = false;
     
    -		
    +
     		/**
     		 * Debug mode flag.
     		 * True: debug mode is used, syntax errors are displayed directly in template. Execution of script is not terminated.
    @@ -257,9 +257,9 @@ protected function check_template( $tpl_name ){
     
     			$tpl_basename                       = basename( $tpl_name );														// template basename
     			$tpl_basedir                        = strpos($tpl_name,"/") ? dirname($tpl_name) . '/' : null;						// template basedirectory
    -			$tpl_dir                            = self::$tpl_dir . $tpl_basedir;								// template directory
    +			$tpl_dir                            = PATH . self::$tpl_dir . $tpl_basedir;								// template directory
     			$this->tpl['tpl_filename']          = $tpl_dir . $tpl_basename . '.' . self::$tpl_ext;	// template filename
    -			$temp_compiled_filename             = self::$cache_dir . $tpl_basename . "." . md5( $tpl_dir . serialize(self::$config_name_sum));
    +			$temp_compiled_filename             = PATH . self::$cache_dir . $tpl_basename . "." . md5( $tpl_dir . serialize(self::$config_name_sum));
     			$this->tpl['compiled_filename']     = $temp_compiled_filename . '.rtpl.php';	// cache filename
     			$this->tpl['cache_filename']        = $temp_compiled_filename . '.s_' . $this->cache_id . '.rtpl.php';	// static cache filename
     
    @@ -271,7 +271,7 @@ protected function check_template( $tpl_name ){
     
     			// file doesn't exsist, or the template was updated, Rain will compile the template
     			if( !file_exists( $this->tpl['compiled_filename'] ) || ( self::$check_template_update && filemtime($this->tpl['compiled_filename']) < filemtime( $this->tpl['tpl_filename'] ) ) ){
    -				$this->compileFile( $tpl_basename, $tpl_basedir, $this->tpl['tpl_filename'], self::$cache_dir, $this->tpl['compiled_filename'] );
    +				$this->compileFile( $tpl_basename, $tpl_basedir, $this->tpl['tpl_filename'], PATH . self::$cache_dir, $this->tpl['compiled_filename'] );
     				return true;
     			}
     			$this->tpl['checked'] = true;
    @@ -285,7 +285,7 @@ protected function check_template( $tpl_name ){
     	*/
     	protected function xml_reSubstitution($capture) {
         		return "<?php echo '<?xml ".stripslashes($capture[1])." ?>'; ?>";
    -	} 
    +	}
     
     	/**
     	 * Compile and write the compiled template file
    @@ -304,11 +304,11 @@ protected function compileFile( $tpl_basename, $tpl_basedir, $tpl_filename, $cac
     			$template_code = str_replace( array("<?","?>"), array("&lt;?","?&gt;"), $template_code );
     
     		//xml re-substitution
    -		$template_code = preg_replace_callback ( "/##XML(.*?)XML##/s", array($this, 'xml_reSubstitution'), $template_code ); 
    +		$template_code = preg_replace_callback ( "/##XML(.*?)XML##/s", array($this, 'xml_reSubstitution'), $template_code );
     
     		//compile template
     		$template_compiled = "<?php if(!class_exists('raintpl')){exit;}?>" . $this->compileTemplate( $template_code, $tpl_basedir );
    -		
    +
     
     		// fix the php-eating-newline-after-closing-tag-problem
     		$template_compiled = str_replace( "?>\n", "?>\n\n", $template_compiled );
    @@ -413,7 +413,7 @@ protected function compileCode( $parsed_code ){
     
     				// if the cache is active
     				if( isset($code[ 2 ]) ){
    -					
    +
     					//dynamic include
     					$compiled_code .= '<?php $tpl = new '.get_class($this).';' .
     								 'if( $cache = $tpl->cache( $template = basename("'.$include_var.'") ) )' .
    @@ -426,7 +426,7 @@ protected function compileCode( $parsed_code ){
     								 '} ?>';
     				}
     				else{
    -	
    +
     					//dynamic include
     					$compiled_code .= '<?php $tpl = new '.get_class($this).';' .
     									  '$tpl_dir_temp = self::$tpl_dir;' .
    @@ -434,8 +434,8 @@ protected function compileCode( $parsed_code ){
     									  ( !$loop_level ? null : '$tpl->assign( "key", $key'.$loop_level.' ); $tpl->assign( "value", $value'.$loop_level.' );' ).
     									  '$tpl->draw( dirname("'.$include_var.'") . ( substr("'.$include_var.'",-1,1) != "/" ? "/" : "" ) . basename("'.$include_var.'") );'.
     									  '?>';
    -					
    -					
    +
    +
     				}
     
     			}
    @@ -548,7 +548,7 @@ protected function compileCode( $parsed_code ){
     				else
     					// parse the function
     					$parsed_function = $function . $this->var_replace( $code[ 2 ], $tag_left_delimiter = null, $tag_right_delimiter = null, $php_left_delimiter = null, $php_right_delimiter = null, $loop_level );
    -				
    +
     				//if code
     				$compiled_code .=   "<?php echo $parsed_function; ?>";
     			}
    @@ -582,8 +582,8 @@ protected function compileCode( $parsed_code ){
     		}
     		return $compiled_code;
     	}
    -	
    -	
    +
    +
     	/**
     	 * Reduce a path, eg. www/library/../filepath//file => www/filepath/file
     	 * @param type $path
    @@ -611,8 +611,8 @@ protected function path_replace( $html, $tpl_basedir ){
     
     		if( self::$path_replace ){
     
    -			$tpl_dir = self::$base_url . self::$tpl_dir . $tpl_basedir;
    -			
    +			$tpl_dir = self::$base_url . PATH . self::$tpl_dir . $tpl_basedir;
    +
     			// reduce the path
     			$path = $this->reduce_path($tpl_dir);
     
    @@ -683,7 +683,7 @@ function func_replace( $html, $tag_left_delimiter, $tag_right_delimiter, $php_le
     			$this->function_check( $tag );
     
     			$extra_var = $this->var_replace( $extra_var, null, null, null, null, $loop_level );
    -            
    +
     
     			// check if there's an operator = in the variable tags, if there's this is an initialization so it will not output any value
     			$is_init_variable = preg_match( "/^(\s*?)\=[^=](.*?)$/", $extra_var );
    @@ -712,7 +712,7 @@ function func_replace( $html, $tag_left_delimiter, $tag_right_delimiter, $php_le
     
     			//if there's a function
     			if( $function_var ){
    -                
    +
                     // check if there's a function or a static method and separate, function by parameters
     				$function_var = str_replace("::", "@double_dot@", $function_var );
     
    @@ -786,7 +786,7 @@ function var_replace( $html, $tag_left_delimiter, $tag_right_delimiter, $php_lef
     
                                 // check if there's an operator = in the variable tags, if there's this is an initialization so it will not output any value
                                 $is_init_variable = preg_match( "/^[a-z_A-Z\.\[\](\-\>)]*=[^=]*$/", $extra_var );
    -                            
    +
                                 //function associate to variable
                                 $function_var = ( $extra_var and $extra_var[0] == '|') ? substr( $extra_var, 1 ) : null;
     
    @@ -805,16 +805,16 @@ function var_replace( $html, $tag_left_delimiter, $tag_right_delimiter, $php_lef
     
                                 //transform .$variable in ["$variable"] and .variable in ["variable"]
                                 $variable_path = preg_replace('/\.(\${0,1}\w+)/', '["\\1"]', $variable_path );
    -                            
    +
                                 // if is an assignment also assign the variable to $this->var['value']
                                 if( $is_init_variable )
                                     $extra_var = "=\$this->var['{$var_name}']{$variable_path}" . $extra_var;
     
    -                                
    +
     
                                 //if there's a function
                                 if( $function_var ){
    -                                
    +
                                         // check if there's a function or a static method and separate, function by parameters
                                         $function_var = str_replace("::", "@double_dot@", $function_var );
     
    @@ -855,13 +855,13 @@ function var_replace( $html, $tag_left_delimiter, $tag_right_delimiter, $php_lef
                                                 $php_var = '$' . $var_name . $variable_path;
                                 }else
                                         $php_var = '$' . $var_name . $variable_path;
    -                            
    +
                                 // compile the variable for php
                                 if( isset( $function ) )
                                         $php_var = $php_left_delimiter . ( !$is_init_variable && $echo ? 'echo ' : null ) . ( $params ? "( $function( $php_var, $params ) )" : "$function( $php_var )" ) . $php_right_delimiter;
                                 else
                                         $php_var = $php_left_delimiter . ( !$is_init_variable && $echo ? 'echo ' : null ) . $php_var . $extra_var . $php_right_delimiter;
    -                            
    +
                                 $html = str_replace( $tag, $php_var, $html );
     
     
    diff --git a/lib/auto.php b/lib/auto.php
    new file mode 100644
    index 00000000..ac3f169a
    --- /dev/null
    +++ b/lib/auto.php
    @@ -0,0 +1,38 @@
    +<?php
    +/**
    + * ZeroBin
    + *
    + * a zero-knowledge paste bin
    + *
    + * @link      http://sebsauvage.net/wiki/doku.php?id=php:zerobin
    + * @copyright 2012 Sébastien SAUVAGE (sebsauvage.net)
    + * @license   http://www.opensource.org/licenses/zlib-license.php The zlib/libpng License
    + * @version   0.15
    + */
    +
    +spl_autoload_register('auto::loader');
    +
    +/**
    + * auto
    + *
    + * provides autoloading functionality
    + */
    +class auto
    +{
    +    /**
    +     * strips slashes deeply
    +     *
    +     * @access public
    +     * @static
    +     * @param  mixed $value
    +     * @return mixed
    +     */
    +    public static function loader($class_name)
    +    {
    +        $filename = PATH . 'lib/' . str_replace('_', '/', $class_name) . '.php';
    +        if(is_readable($filename)) {
    +            return include $filename;
    +        }
    +        return false;
    +    }
    +}
    diff --git a/lib/filter.php b/lib/filter.php
    new file mode 100644
    index 00000000..68c4f590
    --- /dev/null
    +++ b/lib/filter.php
    @@ -0,0 +1,53 @@
    +<?php
    +/**
    + * ZeroBin
    + *
    + * a zero-knowledge paste bin
    + *
    + * @link      http://sebsauvage.net/wiki/doku.php?id=php:zerobin
    + * @copyright 2012 Sébastien SAUVAGE (sebsauvage.net)
    + * @license   http://www.opensource.org/licenses/zlib-license.php The zlib/libpng License
    + * @version   0.15
    + */
    +
    +/**
    + * filter
    + *
    + * Provides data filtering functions.
    + */
    +class filter
    +{
    +    /**
    +     * strips slashes deeply
    +     *
    +     * @access public
    +     * @static
    +     * @param  mixed $value
    +     * @return mixed
    +     */
    +    public static function stripslashes_deep($value)
    +    {
    +        return is_array($value) ?
    +            array_map('filter::stripslashes_deep', $value) :
    +            stripslashes($value);
    +    }
    +
    +    /**
    +     * format a given number of bytes
    +     *
    +     * @access public
    +     * @static
    +     * @param  int $size
    +     * @return string
    +     */
    +    public static function size_humanreadable($size)
    +    {
    +        $iec = array('B', 'kiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB');
    +        $i = 0;
    +        while ( ( $size / 1024 ) >= 1 ) {
    +                $size = $size / 1024;
    +                $i++;
    +        }
    +        return number_format($size, ($i ? 2 : 0), '.', ' ') . ' ' . $iec[$i];
    +    }
    +}
    diff --git a/lib/sjcl.php b/lib/sjcl.php
    new file mode 100644
    index 00000000..7c4ef8c6
    --- /dev/null
    +++ b/lib/sjcl.php
    @@ -0,0 +1,62 @@
    +<?php
    +/**
    + * ZeroBin
    + *
    + * a zero-knowledge paste bin
    + *
    + * @link      http://sebsauvage.net/wiki/doku.php?id=php:zerobin
    + * @copyright 2012 Sébastien SAUVAGE (sebsauvage.net)
    + * @license   http://www.opensource.org/licenses/zlib-license.php The zlib/libpng License
    + * @version   0.15
    + */
    +
    +/**
    + * sjcl
    + *
    + * Provides SJCL validation function.
    + */
    +class sjcl
    +{
    +    /**
    +     * SJCL validator
    +     *
    +     * Checks if a json string is a proper SJCL encrypted message.
    +     *
    +     * @access public
    +     * @static
    +     * @param  string $encoded JSON
    +     * @return bool
    +     */
    +    public static function isValid($encoded)
    +    {
    +        $accepted_keys = array('iv','salt','ct');
    +
    +        // Make sure content is valid json
    +        $decoded = json_decode($encoded);
    +        if (is_null($decoded)) return false;
    +        $decoded = (array) $decoded;
    +
    +        // Make sure no additionnal keys were added.
    +        if (
    +            count(array_keys($decoded)) != count($accepted_keys)
    +        ) return false;
    +
    +        // Make sure required fields are present and contain base64 data.
    +        foreach($accepted_keys as $k)
    +        {
    +            if (!(
    +                array_key_exists($k, $decoded) &&
    +                $ct = base64_decode($decoded[$k], $strict=true)
    +            )) return false;
    +        }
    +
    +        // Make sure some fields have a reasonable size.
    +        if (strlen($decoded['iv']) > 24) return false;
    +        if (strlen($decoded['salt']) > 14) return false;
    +
    +        // Reject data if entropy is too low
    +        if (strlen($ct) > strlen(gzdeflate($ct))) return false;
    +
    +        return true;
    +    }
    +}
    diff --git a/lib/trafficlimiter.php b/lib/trafficlimiter.php
    new file mode 100644
    index 00000000..3a2e8cd7
    --- /dev/null
    +++ b/lib/trafficlimiter.php
    @@ -0,0 +1,122 @@
    +<?php
    +/**
    + * ZeroBin
    + *
    + * a zero-knowledge paste bin
    + *
    + * @link      http://sebsauvage.net/wiki/doku.php?id=php:zerobin
    + * @copyright 2012 Sébastien SAUVAGE (sebsauvage.net)
    + * @license   http://www.opensource.org/licenses/zlib-license.php The zlib/libpng License
    + * @version   0.15
    + */
    +
    +/**
    + * traffic_limiter
    + *
    + * Handles traffic limiting, so no user does more than one call per 10 seconds.
    + */
    +class trafficlimiter
    +{
    +    /**
    +     * @access private
    +     * @static
    +     * @var    int
    +     */
    +    private static $_limit = 10;
    +
    +    /**
    +     * @access private
    +     * @static
    +     * @var    string
    +     */
    +    private static $_path = 'data';
    +
    +    /**
    +     * set the time limit in seconds
    +     *
    +     * @access public
    +     * @static
    +     * @param  int $limit
    +     * @return void
    +     */
    +    public static function setLimit($limit)
    +    {
    +        self::$_limit = $limit;
    +    }
    +
    +    /**
    +     * set the path
    +     *
    +     * @access public
    +     * @static
    +     * @param  string $path
    +     * @return void
    +     */
    +    public static function setPath($path)
    +    {
    +        self::$_path = $path;
    +    }
    +
    +    /**
    +     * traffic limiter
    +     *
    +     * Make sure the IP address makes at most 1 request every 10 seconds.
    +     *
    +     * @access public
    +     * @static
    +     * @param  string $ip
    +     * @return bool
    +     */
    +    public static function canPass($ip)
    +    {
    +        // Create storage directory if it does not exist.
    +        if (!is_dir(self::$_path)) mkdir(self::$_path, 0705);
    +        // Create .htaccess file if it does not exist.
    +        if (!is_file(self::$_path . '/.htaccess'))
    +        {
    +            file_put_contents(
    +                self::$_path . '/.htaccess',
    +                'Allow from none' . PHP_EOL .
    +                'Deny from all'. PHP_EOL
    +            );
    +        }
    +        $file = self::$_path . '/traffic_limiter.php';
    +        if (!is_file($file))
    +        {
    +            file_put_contents(
    +                $file,
    +                '<?php' . PHP_EOL .
    +                '$GLOBALS[\'traffic_limiter\'] = array();' . PHP_EOL
    +            );
    +            chmod($file, 0705);
    +        }
    +
    +        require $file;
    +        $now = time();
    +        $tl = $GLOBALS['traffic_limiter'];
    +
    +        // purge file of expired IPs to keep it small
    +        foreach($tl as $key => $time)
    +        {
    +            if ($time + self::$_limit < $now)
    +            {
    +                unset($tl[$key]);
    +            }
    +        }
    +
    +        if (array_key_exists($ip, $tl) && ($tl[$ip] + self::$_limit >= $now))
    +        {
    +            $result = false;
    +        } else {
    +            $tl[$ip] = time();
    +            $result = true;
    +        }
    +        file_put_contents(
    +            $file,
    +            '<?php' . PHP_EOL .
    +            '$GLOBALS[\'traffic_limiter\'] = ' .
    +            var_export($tl, true) . ';' . PHP_EOL
    +        );
    +        return $result;
    +    }
    +}
    diff --git a/lib/vizhash_gd_zero.php b/lib/vizhash16x16.php
    similarity index 92%
    rename from lib/vizhash_gd_zero.php
    rename to lib/vizhash16x16.php
    index c9e081f2..110f4088 100644
    --- a/lib/vizhash_gd_zero.php
    +++ b/lib/vizhash16x16.php
    @@ -22,17 +22,17 @@ function __construct()
         {
             $this->width=16;
             $this->height=16;
    -        
    +
             // Read salt from file (and create it if does not exist).
             // The salt will make vizhash avatar unique on each ZeroBin installation
             // to prevent IP checking.
    -        $saltfile = 'data/salt.php';
    +        $saltfile = PATH . 'data/salt.php';
             if (!is_file($saltfile))
                 file_put_contents($saltfile,'<?php /* |'.$this->randomSalt().'| */ ?>');
             $items=explode('|',file_get_contents($saltfile));
             $this->salt = $items[1];
    -    }  
    -    
    +    }
    +
         // Generate a 16x16 png corresponding to $text.
         // Input: $text (string)
         // Output: PNG data. Or empty string if GD is not available.
    @@ -61,14 +61,14 @@ function generate($text)
             $image = $this->degrade($image,$op,array($r0,$g0,$b0),array(0,0,0));
     
             for($i=0; $i<7; $i=$i+1)
    -        {     
    +        {
                 $action=$this->getInt();
                 $color = imagecolorallocate($image, $r,$g,$b);
                 $r = ($r0 + $this->getInt()/25)%256;
                 $g = ($g0 + $this->getInt()/25)%256;
                 $b = ($b0 + $this->getInt()/25)%256;
                 $r0=$r; $g0=$g; $b0=$b;
    -            $this->drawshape($image,$action,$color);   
    +            $this->drawshape($image,$action,$color);
             }
     
             $color = imagecolorallocate($image,$this->getInt(),$this->getInt(),$this->getInt());
    @@ -78,10 +78,10 @@ function generate($text)
             $imagedata = ob_get_contents();
             ob_end_clean();
             imagedestroy($image);
    -        
    +
             return $imagedata;
    -    } 
    -    
    +    }
    +
         // Generate a large random hexadecimal salt.
         private function randomSalt()
         {
    @@ -89,25 +89,25 @@ private function randomSalt()
             for($i=0;$i<6;$i++) { $randomSalt.=base_convert(mt_rand(),10,16); }
             return $randomSalt;
         }
    -   
    -    
    +
    +
         private function getInt() // Returns a single integer from the $VALUES array (0...255)
         {
    -        $v= $this->VALUES[$this->VALUES_INDEX]; 
    +        $v= $this->VALUES[$this->VALUES_INDEX];
             $this->VALUES_INDEX++;
             $this->VALUES_INDEX %= count($this->VALUES); // Warp around the array
             return $v;
         }
    -    private function getX() // Returns a single integer from the array (roughly mapped to image width) 
    +    private function getX() // Returns a single integer from the array (roughly mapped to image width)
         {
             return $this->width*$this->getInt()/256;
         }
     
    -    private function getY() // Returns a single integer from the array (roughly mapped to image height) 
    -    { 
    +    private function getY() // Returns a single integer from the array (roughly mapped to image height)
    +    {
             return $this->height*$this->getInt()/256;
    -    }  
    -    
    +    }
    +
         # Gradient function taken from:
         # http://www.supportduweb.com/scripts_tutoriaux-code-source-41-gd-faire-un-degrade-en-php-gd-fonction-degrade-imagerie.html
         private function degrade($img,$direction,$color1,$color2)
    @@ -129,17 +129,17 @@ private function degrade($img,$direction,$color1,$color2)
                 }
                 return $img;
         }
    -    
    +
         private function drawshape($image,$action,$color)
         {
             switch($action%7)
             {
                 case 0:
    -                ImageFilledRectangle ($image,$this->getX(),$this->getY(),$this->getX(),$this->getY(),$color);  
    +                ImageFilledRectangle ($image,$this->getX(),$this->getY(),$this->getX(),$this->getY(),$color);
                     break;
                 case 1:
                 case 2:
    -                ImageFilledEllipse ($image, $this->getX(), $this->getY(), $this->getX(), $this->getY(), $color);  
    +                ImageFilledEllipse ($image, $this->getX(), $this->getY(), $this->getX(), $this->getY(), $color);
                     break;
                 case 3:
                     $points = array($this->getX(), $this->getY(), $this->getX(), $this->getY(), $this->getX(), $this->getY(),$this->getX(), $this->getY());
    @@ -150,9 +150,9 @@ private function drawshape($image,$action,$color)
                 case 6:
                     $start=$this->getInt()*360/256; $end=$start+$this->getInt()*180/256;
                     ImageFilledArc ($image, $this->getX(), $this->getY(), $this->getX(), $this->getY(),$start,$end,$color,IMG_ARC_PIE);
    -                break;     
    +                break;
             }
    -    }    
    -}    
    +    }
    +}
     
     ?>
    \ No newline at end of file
    diff --git a/lib/zerobin.php b/lib/zerobin.php
    new file mode 100644
    index 00000000..754a7d14
    --- /dev/null
    +++ b/lib/zerobin.php
    @@ -0,0 +1,438 @@
    +<?php
    +/**
    + * ZeroBin
    + *
    + * a zero-knowledge paste bin
    + *
    + * @link      http://sebsauvage.net/wiki/doku.php?id=php:zerobin
    + * @copyright 2012 Sébastien SAUVAGE (sebsauvage.net)
    + * @license   http://www.opensource.org/licenses/zlib-license.php The zlib/libpng License
    + * @version   0.15
    + */
    +
    +/**
    + * zerobin
    + *
    + * Controller, puts it all together.
    + */
    +class zerobin
    +{
    +    /*
    +     * @const string version
    +     */
    +    const VERSION = 'Alpha 0.15';
    +
    +    /**
    +     * @access private
    +     * @var    array
    +     */
    +    private $_conf = array(
    +        'model' => 'zerobin_data',
    +    );
    +
    +    /**
    +     * @access private
    +     * @var    string
    +     */
    +    private $_data = '';
    +
    +    /**
    +     * @access private
    +     * @var    string
    +     */
    +    private $_error = '';
    +
    +    /**
    +     * @access private
    +     * @var    zerobin_data
    +     */
    +    private $_model;
    +
    +    /**
    +     * constructor
    +     *
    +     * initializes and runs ZeroBin
    +     *
    +     * @access public
    +     */
    +    public function __construct()
    +    {
    +        if (version_compare(PHP_VERSION, '5.2.6') < 0)
    +            die('ZeroBin requires php 5.2.6 or above to work. Sorry.');
    +
    +        // In case stupid admin has left magic_quotes enabled in php.ini.
    +        if (get_magic_quotes_gpc())
    +        {
    +            $_POST   = array_map('filter::stripslashes_deep', $_POST);
    +            $_GET    = array_map('filter::stripslashes_deep', $_GET);
    +            $_COOKIE = array_map('filter::stripslashes_deep', $_COOKIE);
    +        }
    +
    +        // Load config from ini file.
    +        $this->_init();
    +
    +        // Create new paste or comment.
    +        if (!empty($_POST['data']))
    +        {
    +            $this->_create();
    +        }
    +        // Display an existing paste.
    +        elseif (!empty($_SERVER['QUERY_STRING']))
    +        {
    +            $this->_read();
    +        }
    +
    +        // Display ZeroBin frontend
    +        $this->_view();
    +    }
    +
    +    /**
    +     * initialize zerobin
    +     *
    +     * @access private
    +     * @return void
    +     */
    +    private function _init()
    +    {
    +        foreach (array('cfg', 'lib') as $dir)
    +        {
    +            if (!is_file(PATH . $dir . '/.htaccess')) file_put_contents(
    +                PATH . $dir . '/.htaccess',
    +                'Allow from none' . PHP_EOL .
    +                'Deny from all'. PHP_EOL
    +            );
    +        }
    +
    +        $this->_conf = parse_ini_file(PATH . 'cfg/conf.ini', true);
    +        $this->_model = $this->_conf['model']['class'];
    +    }
    +
    +    /**
    +     * get the model, create one if needed
    +     *
    +     * @access private
    +     * @return zerobin_data
    +     */
    +    private function _model()
    +    {
    +        // if needed, initialize the model
    +        if(is_string($this->_model)) {
    +            $this->_model = forward_static_call(
    +                array($this->_model, 'getInstance'),
    +                $this->_conf['model_options']
    +            );
    +        }
    +        return $this->_model;
    +    }
    +
    +    /**
    +     * Store new paste or comment.
    +     *
    +     * POST contains:
    +     * data (mandatory) = json encoded SJCL encrypted text (containing keys: iv,salt,ct)
    +     *
    +     * All optional data will go to meta information:
    +     * expire (optional) = expiration delay (never,5min,10min,1hour,1day,1week,1month,1year,burn) (default:never)
    +     * opendiscusssion (optional) = is the discussion allowed on this paste ? (0/1) (default:0)
    +     * nickname (optional) = in discussion, encoded SJCL encrypted text nickname of author of comment (containing keys: iv,salt,ct)
    +     * parentid (optional) = in discussion, which comment this comment replies to.
    +     * pasteid (optional) = in discussion, which paste this comment belongs to.
    +     *
    +     * @access private
    +     * @return void
    +     */
    +    private function _create()
    +    {
    +        header('Content-type: application/json');
    +        $error = false;
    +
    +        // Make sure last paste from the IP address was more than X seconds ago.
    +        trafficlimiter::setLimit($this->_conf['traffic']['limit']);
    +        trafficlimiter::setPath($this->_conf['traffic']['dir']);
    +        if (
    +            !trafficlimiter::canPass($_SERVER['REMOTE_ADDR'])
    +        ) $this->_return_message(
    +            1,
    +            'Please wait ' .
    +            $this->_conf['traffic']['limit'] .
    +            ' seconds between each post.'
    +        );
    +
    +        // Make sure content is not too big.
    +        $data = $_POST['data'];
    +        if (
    +            strlen($data) > $this->_conf['main']['sizelimit']
    +        ) $this->_return_message(
    +            1,
    +            'Paste is limited to ' .
    +            $this->_conf['main']['sizelimit'] .
    +            ' ' .
    +            filter::size_humanreadable($this->_conf['main']['sizelimit']) .
    +            ' of encrypted data.'
    +        );
    +
    +        // Make sure format is correct.
    +        if (!sjcl::isValid($data)) $this->_return_message(1, 'Invalid data.');
    +
    +        // Read additional meta-information.
    +        $meta=array();
    +
    +        // Read expiration date
    +        if (!empty($_POST['expire']))
    +        {
    +            switch ($_POST['expire'])
    +            {
    +                case 'burn':
    +                    $meta['burnafterreading'] = true;
    +                    break;
    +                case '5min':
    +                    $meta['expire_date'] = time()+5*60;
    +                    break;
    +                case '10min':
    +                    $meta['expire_date'] = time()+10*60;
    +                    break;
    +                case '1hour':
    +                    $meta['expire_date'] = time()+60*60;
    +                    break;
    +                case '1day':
    +                    $meta['expire_date'] = time()+24*60*60;
    +                    break;
    +                case '1week':
    +                    $meta['expire_date'] = time()+7*24*60*60;
    +                    break;
    +                case '1month':
    +                    $meta['expire_date'] = strtotime('+1 month');
    +                    break;
    +                case '1year':
    +                    $meta['expire_date'] = strtotime('+1 year');
    +            }
    +        }
    +
    +        // Read open discussion flag.
    +        if ($this->_conf['main']['opendiscussion'] && !empty($_POST['opendiscussion']))
    +        {
    +            $opendiscussion = $_POST['opendiscussion'];
    +            if ($opendiscussion != 0)
    +            {
    +                if ($opendiscussion != 1) $error = true;
    +                $meta['opendiscussion'] = true;
    +            }
    +        }
    +
    +        // You can't have an open discussion on a "Burn after reading" paste:
    +        if (isset($meta['burnafterreading'])) unset($meta['opendiscussion']);
    +
    +        // Optional nickname for comments
    +        if (!empty($_POST['nickname']))
    +        {
    +            // Generation of the anonymous avatar (Vizhash):
    +            // If a nickname is provided, we generate a Vizhash.
    +            // (We assume that if the user did not enter a nickname, he/she wants
    +            // to be anonymous and we will not generate the vizhash.)
    +            $nick = $_POST['nickname'];
    +            if (!sjcl::isValid($nick))
    +            {
    +                $error = true;
    +            }
    +            else
    +            {
    +                $meta['nickname'] = $nick;
    +                $vz = new vizhash16x16();
    +                $pngdata = $vz->generate($_SERVER['REMOTE_ADDR']);
    +                if ($pngdata != '')
    +                {
    +                    $meta['vizhash'] = 'data:image/png;base64,' . base64_encode($pngdata);
    +                }
    +                // Once the avatar is generated, we do not keep the IP address, nor its hash.
    +            }
    +        }
    +
    +        if ($error) $this->_return_message(1, 'Invalid data.');
    +
    +        // Add post date to meta.
    +        $meta['postdate'] = time();
    +
    +        // We just want a small hash to avoid collisions:
    +        // Half-MD5 (64 bits) will do the trick
    +        $dataid = substr(hash('md5', $data), 0, 16);
    +
    +        $storage = array('data' => $data);
    +
    +        // Add meta-information only if necessary.
    +        if (count($meta)) $storage['meta'] = $meta;
    +
    +        // The user posts a comment.
    +        if (
    +            !empty($_POST['parentid']) &&
    +            !empty($_POST['pasteid'])
    +        )
    +        {
    +            $pasteid  = $_POST['pasteid'];
    +            $parentid = $_POST['parentid'];
    +            if (
    +                !preg_match('/[a-f\d]{16}/', $pasteid) ||
    +                !preg_match('/[a-f\d]{16}/', $parentid)
    +            ) $this->_return_message(1, 'Invalid data.');
    +
    +            // Comments do not expire (it's the paste that expires)
    +            unset($storage['expire_date']);
    +            unset($storage['opendiscussion']);
    +
    +            // Make sure paste exists.
    +            if (
    +                !$this->_model()->exists($pasteid)
    +            ) $this->_return_message(1, 'Invalid data.');
    +
    +            // Make sure the discussion is opened in this paste.
    +            $paste = $this->_model()->read($pasteid);
    +            if (
    +                !$paste->meta->opendiscussion
    +            ) $this->_return_message(1, 'Invalid data.');
    +
    +            // Check for improbable collision.
    +            if (
    +                $this->_model()->existsComment($pasteid, $parentid, $dataid)
    +            ) $this->_return_message(1, 'You are unlucky. Try again.');
    +
    +            // New comment
    +            if (
    +                $this->_model()->createComment($pasteid, $parentid, $dataid, $storage) === false
    +            ) $this->_return_message(1, 'Error saving comment. Sorry.');
    +
    +            // 0 = no error
    +            $this->_return_message(0, $dataid);
    +        }
    +        // The user posts a standard paste.
    +        else
    +        {
    +            // Check for improbable collision.
    +            if (
    +                $this->_model()->exists($dataid)
    +            ) $this->_return_message(1, 'You are unlucky. Try again.');
    +
    +            // New paste
    +            if (
    +                $this->_model()->create($dataid, $storage) === false
    +            ) $this->_return_message(1, 'Error saving paste. Sorry.');
    +
    +            // 0 = no error
    +            $this->_return_message(0, $dataid);
    +        }
    +
    +        $this->_return_message(1, 'Server error.');
    +    }
    +
    +    /**
    +     * Read an existing paste or comment.
    +     *
    +     * @access private
    +     * @return void
    +     */
    +    private function _read()
    +    {
    +        $dataid = $_SERVER['QUERY_STRING'];
    +
    +        // Is this a valid paste identifier?
    +        if (preg_match('/[a-f\d]{16}/', $dataid))
    +        {
    +            // Check that paste exists.
    +            if ($this->_model()->exists($dataid))
    +            {
    +                // Get the paste itself.
    +                $paste = $this->_model()->read($dataid);
    +
    +                // See if paste has expired.
    +                if (
    +                    isset($paste->meta->expire_date) &&
    +                    $paste->meta->expire_date < time()
    +                )
    +                {
    +                    // Delete the paste
    +                    $this->_model()->delete($dataid);
    +                    $this->_error = 'Paste does not exist or has expired.';
    +                }
    +                // If no error, return the paste.
    +                else
    +                {
    +                    // We kindly provide the remaining time before expiration (in seconds)
    +                    if (
    +                        property_exists($paste->meta, 'expire_date')
    +                    ) $paste->meta->remaining_time = $paste->meta->expire_date - time();
    +
    +                    // The paste itself is the first in the list of encrypted messages.
    +                    $messages = array($paste);
    +
    +                    // If it's a discussion, get all comments.
    +                    if (
    +                        property_exists($paste->meta, 'opendiscussion') &&
    +                        $paste->meta->opendiscussion
    +                    )
    +                    {
    +                        $messages = array_merge(
    +                            $messages,
    +                            $this->_model()->readComments($dataid)
    +                        );
    +                    }
    +                    $this->_data = json_encode($messages);
    +
    +                    // If the paste was meant to be read only once, delete it.
    +                    if (
    +                        property_exists($paste->meta, 'burnafterreading') &&
    +                        $paste->meta->burnafterreading
    +                    ) $this->_model()->delete($dataid);
    +                }
    +            }
    +            else
    +            {
    +                $this->_error = 'Paste does not exist or has expired.';
    +            }
    +        }
    +    }
    +
    +    /**
    +     * Display ZeroBin frontend.
    +     *
    +     * @access private
    +     * @return void
    +     */
    +    private function _view()
    +    {
    +        // set headers to disable caching
    +        $time = gmdate('D, d M Y H:i:s \G\M\T');
    +        header('Cache-Control: no-store, no-cache, must-revalidate');
    +        header('Pragma: no-cache');
    +        header('Expires: ' . $time);
    +        header('Last-Modified: ' . $time);
    +        header('Vary: Accept');
    +
    +        $page = new RainTPL;
    +        // We escape it here because ENT_NOQUOTES can't be used in RainTPL templates.
    +        $page->assign('CIPHERDATA', htmlspecialchars($this->_data, ENT_NOQUOTES));
    +        $page->assign('ERRORMESSAGE', $this->_error);
    +        $page->assign('OPENDISCUSSION', $this->_conf['main']['opendiscussion']);
    +        $page->assign('VERSION', self::VERSION);
    +        $page->draw('page');
    +    }
    +
    +    /**
    +     * return JSON encoded message and exit
    +     *
    +     * @access private
    +     * @param  bool $status
    +     * @param  string $message
    +     * @return void
    +     */
    +    private function _return_message($status, $message)
    +    {
    +        $result = array('status' => $status);
    +        if ($status)
    +        {
    +            $result['message'] = $message;
    +        }
    +        else
    +        {
    +            $result['id'] = $message;
    +        }
    +        exit(json_encode($result));
    +    }
    +}
    diff --git a/lib/zerobin/abstract.php b/lib/zerobin/abstract.php
    new file mode 100644
    index 00000000..234fb7f5
    --- /dev/null
    +++ b/lib/zerobin/abstract.php
    @@ -0,0 +1,124 @@
    +<?php
    +/**
    + * ZeroBin
    + *
    + * a zero-knowledge paste bin
    + *
    + * @link      http://sebsauvage.net/wiki/doku.php?id=php:zerobin
    + * @copyright 2012 Sébastien SAUVAGE (sebsauvage.net)
    + * @license   http://www.opensource.org/licenses/zlib-license.php The zlib/libpng License
    + * @version   0.15
    + */
    +
    +/**
    + * zerobin_abstract
    + *
    + * Abstract model for ZeroBin data access, implemented as a singleton.
    + */
    +abstract class zerobin_abstract
    +{
    +	/**
    +     * singleton instance
    +     *
    +     * @access private
    +     * @static
    +     * @var zerobin
    +     */
    +    protected static $_instance = null;
    +
    +    /**
    +     * enforce singleton, disable constructor
    +     *
    +     * Instantiate using {@link getInstance()}, zerobin is a singleton object.
    +     *
    +     * @access protected
    +     */
    +    protected function __construct() {}
    +
    +    /**
    +     * enforce singleton, disable cloning
    +     *
    +     * Instantiate using {@link getInstance()}, zerobin is a singleton object.
    +     *
    +     * @access private
    +     */
    +    private function __clone() {}
    +
    +    /**
    +     * get instance of singleton
    +     *
    +     * @access public
    +     * @static
    +     * @return zerobin_abstract
    +     */
    +    public static function getInstance($options) {}
    +
    +    /**
    +     * Create a paste.
    +     *
    +     * @access public
    +     * @param  string $pasteid
    +     * @param  array  $paste
    +     * @return int|false
    +     */
    +    abstract public function create($pasteid, $paste);
    +
    +    /**
    +     * Read a paste.
    +     *
    +     * @access public
    +     * @param  string $pasteid
    +     * @return string
    +     */
    +    abstract public function read($pasteid);
    +
    +    /**
    +     * Delete a paste and its discussion.
    +     *
    +     * @access public
    +     * @param  string $pasteid
    +     * @return void
    +     */
    +    abstract public function delete($pasteid);
    +
    +    /**
    +     * Test if a paste exists.
    +     *
    +     * @access public
    +     * @param  string $dataid
    +     * @return void
    +     */
    +    abstract public function exists($pasteid);
    +
    +    /**
    +     * Create a comment in a paste.
    +     *
    +     * @access public
    +     * @param  string $pasteid
    +     * @param  string $parentid
    +     * @param  string $commentid
    +     * @param  array  $comment
    +     * @return int|false
    +     */
    +    abstract public function createComment($pasteid, $parentid, $commentid, $comment);
    +
    +    /**
    +     * Read all comments of paste.
    +     *
    +     * @access public
    +     * @param  string $pasteid
    +     * @return array
    +     */
    +    abstract public function readComments($pasteid);
    +
    +    /**
    +     * Test if a comment exists.
    +     *
    +     * @access public
    +     * @param  string $dataid
    +     * @param  string $parentid
    +     * @param  string $commentid
    +     * @return void
    +     */
    +    abstract public function existsComment($pasteid, $parentid, $commentid);
    +}
    diff --git a/lib/zerobin/data.php b/lib/zerobin/data.php
    new file mode 100644
    index 00000000..a9e42dbc
    --- /dev/null
    +++ b/lib/zerobin/data.php
    @@ -0,0 +1,254 @@
    +<?php
    +/**
    + * ZeroBin
    + *
    + * a zero-knowledge paste bin
    + *
    + * @link      http://sebsauvage.net/wiki/doku.php?id=php:zerobin
    + * @copyright 2012 Sébastien SAUVAGE (sebsauvage.net)
    + * @license   http://www.opensource.org/licenses/zlib-license.php The zlib/libpng License
    + * @version   0.15
    + */
    +
    +/**
    + * zerobin_data
    + *
    + * Model for data access, implemented as a singleton.
    + */
    +class zerobin_data extends zerobin_abstract
    +{
    +    /*
    +     * @access private
    +     * @static
    +     * @var string directory where data is stored
    +     */
    +    private static $_dir = 'data/';
    +
    +    /**
    +     * get instance of singleton
    +     *
    +     * @access public
    +     * @static
    +     * @return zerobin_data
    +     */
    +    public static function getInstance($options = null)
    +    {
    +        // if given update the data directory
    +        if (
    +        	is_array($options) &&
    +        	array_key_exists('dir', $options)
    +        ) self::$_dir = $options['dir'] . DIRECTORY_SEPARATOR;
    +        // if needed initialize the singleton
    +        if(!(self::$_instance instanceof zerobin_data)) {
    +            self::$_instance = new self;
    +            self::_init();
    +        }
    +        return self::$_instance;
    +    }
    +
    +    /**
    +     * Create a paste.
    +     *
    +     * @access public
    +     * @param  string $pasteid
    +     * @param  array  $paste
    +     * @return int|false
    +     */
    +    public function create($pasteid, $paste)
    +    {
    +        $storagedir = self::_dataid2path($pasteid);
    +        if (is_file($storagedir . $pasteid)) return false;
    +        if (!is_dir($storagedir)) mkdir($storagedir, 0705, true);
    +        return (bool) file_put_contents($storagedir . $pasteid, json_encode($paste));
    +    }
    +
    +    /**
    +     * Read a paste.
    +     *
    +     * @access public
    +     * @param  string $pasteid
    +     * @return stdClass|false
    +     */
    +    public function read($pasteid)
    +    {
    +        if(!$this->exists($pasteid)) return false;
    +        return json_decode(
    +            file_get_contents(self::_dataid2path($pasteid) . $pasteid)
    +        );
    +    }
    +
    +    /**
    +     * Delete a paste and its discussion.
    +     *
    +     * @access public
    +     * @param  string $pasteid
    +     * @return void
    +     */
    +    public function delete($pasteid)
    +    {
    +        // Delete the paste itself.
    +        unlink(self::_dataid2path($pasteid) . $pasteid);
    +
    +        // Delete discussion if it exists.
    +        $discdir = self::_dataid2discussionpath($pasteid);
    +        if (is_dir($discdir))
    +        {
    +            // Delete all files in discussion directory
    +            $dir = dir($discdir);
    +            while (false !== ($filename = $dir->read()))
    +            {
    +                if (is_file($discdir.$filename)) unlink($discdir.$filename);
    +            }
    +            $dir->close();
    +
    +            // Delete the discussion directory.
    +            rmdir($discdir);
    +        }
    +    }
    +
    +    /**
    +     * Test if a paste exists.
    +     *
    +     * @access public
    +     * @param  string $dataid
    +     * @return void
    +     */
    +    public function exists($pasteid)
    +    {
    +        return is_file(self::_dataid2path($pasteid) . $pasteid);
    +    }
    +
    +    /**
    +     * Create a comment in a paste.
    +     *
    +     * @access public
    +     * @param  string $pasteid
    +     * @param  string $parentid
    +     * @param  string $commentid
    +     * @param  array  $comment
    +     * @return int|false
    +     */
    +    public function createComment($pasteid, $parentid, $commentid, $comment)
    +    {
    +        $storagedir = self::_dataid2discussionpath($pasteid);
    +        $filename = $pasteid . '.' . $commentid . '.' . $parentid;
    +        if (is_file($storagedir . $filename)) return false;
    +        if (!is_dir($storagedir)) mkdir($storagedir, 0705, true);
    +        return file_put_contents($storagedir . $filename, json_encode($comment));
    +    }
    +
    +    /**
    +     * Read all comments of paste.
    +     *
    +     * @access public
    +     * @param  string $pasteid
    +     * @return array
    +     */
    +    public function readComments($pasteid)
    +    {
    +        $comments = array();
    +        $discdir = self::_dataid2discussionpath($pasteid);
    +        if (is_dir($discdir))
    +        {
    +            // Delete all files in discussion directory
    +            $dir = dir($discdir);
    +            while (false !== ($filename = $dir->read()))
    +            {
    +                // Filename is in the form pasteid.commentid.parentid:
    +                // - pasteid is the paste this reply belongs to.
    +                // - commentid is the comment identifier itself.
    +                // - parentid is the comment this comment replies to (It can be pasteid)
    +                if (is_file($discdir.$filename))
    +                {
    +                    $comment = json_decode(file_get_contents($discdir.$filename));
    +                    $items = explode('.', $filename);
    +                    // Add some meta information not contained in file.
    +                    $comment->meta->commentid=$items[1];
    +                    $comment->meta->parentid=$items[2];
    +
    +                    // Store in array
    +                    $comments[$comment->meta->postdate]=$comment;
    +                }
    +            }
    +            $dir->close();
    +
    +            // Sort comments by date, oldest first.
    +            ksort($comments);
    +        }
    +        return $comments;
    +    }
    +
    +    /**
    +     * Test if a comment exists.
    +     *
    +     * @access public
    +     * @param  string $dataid
    +     * @param  string $parentid
    +     * @param  string $commentid
    +     * @return void
    +     */
    +    public function existsComment($pasteid, $parentid, $commentid)
    +    {
    +        return is_file(
    +            self::_dataid2discussionpath($pasteid) .
    +            $pasteid . '.' . $commentid . '.' . $parentid
    +        );
    +    }
    +
    +    /**
    +     * initialize zerobin
    +     *
    +     * @access private
    +     * @static
    +     * @return void
    +     */
    +    private static function _init()
    +    {
    +        // Create storage directory if it does not exist.
    +        if (!is_dir(self::$_dir)) mkdir(self::$_dir, 0705);
    +        // Create .htaccess file if it does not exist.
    +        if (!is_file(self::$_dir . '.htaccess'))
    +        {
    +            file_put_contents(
    +                self::$_dir . '.htaccess',
    +                'Allow from none' . PHP_EOL .
    +                'Deny from all'. PHP_EOL
    +            );
    +        }
    +    }
    +
    +    /**
    +     * Convert paste id to storage path.
    +     *
    +     * The idea is to creates subdirectories in order to limit the number of files per directory.
    +     * (A high number of files in a single directory can slow things down.)
    +     * eg. "f468483c313401e8" will be stored in "data/f4/68/f468483c313401e8"
    +     * High-trafic websites may want to deepen the directory structure (like Squid does).
    +     *
    +     * eg. input 'e3570978f9e4aa90' --> output 'data/e3/57/'
    +     *
    +     * @access private
    +     * @static
    +     * @param  string $dataid
    +     * @return void
    +     */
    +    private static function _dataid2path($dataid)
    +    {
    +        return self::$_dir . substr($dataid,0,2) . '/' . substr($dataid,2,2) . '/';
    +    }
    +
    +    /**
    +     * Convert paste id to discussion storage path.
    +     *
    +     * eg. input 'e3570978f9e4aa90' --> output 'data/e3/57/e3570978f9e4aa90.discussion/'
    +     *
    +     * @access private
    +     * @static
    +     * @param  string $dataid
    +     * @return void
    +     */
    +    private static function _dataid2discussionpath($dataid)
    +    {
    +        return self::_dataid2path($dataid) . $dataid . '.discussion/';
    +    }
    +}
    diff --git a/lib/zerobin/db.php b/lib/zerobin/db.php
    new file mode 100644
    index 00000000..714dbd87
    --- /dev/null
    +++ b/lib/zerobin/db.php
    @@ -0,0 +1,402 @@
    +<?php
    +/**
    + * ZeroBin
    + *
    + * a zero-knowledge paste bin
    + *
    + * @link      http://sebsauvage.net/wiki/doku.php?id=php:zerobin
    + * @copyright 2012 Sébastien SAUVAGE (sebsauvage.net)
    + * @license   http://www.opensource.org/licenses/zlib-license.php The zlib/libpng License
    + * @version   0.15
    + */
    +
    +/**
    + * zerobin_db
    + *
    + * Model for DB access, implemented as a singleton.
    + */
    +class zerobin_db extends zerobin_abstract
    +{
    +    /*
    +     * @access private
    +     * @static
    +     * @var array to cache select queries
    +     */
    +    private static $_cache = array();
    +
    +    /*
    +     * @access private
    +     * @static
    +     * @var PDO instance of database connection
    +     */
    +    private static $_db;
    +
    +    /*
    +     * @access private
    +     * @static
    +     * @var string table prefix
    +     */
    +    private static $_prefix = '';
    +
    +    /*
    +     * @access private
    +     * @static
    +     * @var string database type
    +     */
    +    private static $_type = '';
    +
    +    /**
    +     * get instance of singleton
    +     *
    +     * @access public
    +     * @static
    +     * @throws Exception
    +     * @return zerobin_db
    +     */
    +    public static function getInstance($options = null)
    +    {
    +        // if needed initialize the singleton
    +        if(!(self::$_instance instanceof zerobin_db)) {
    +            self::$_instance = new self;
    +        }
    +
    +        if (is_array($options))
    +        {
    +            // set table prefix if given
    +            if (array_key_exists('tbl', $options)) self::$_prefix = $options['tbl'];
    +
    +            // initialize the db connection with new options
    +            if (
    +                array_key_exists('dsn', $options) &&
    +                array_key_exists('usr', $options) &&
    +                array_key_exists('pwd', $options) &&
    +                array_key_exists('opt', $options)
    +            )
    +            {
    +                self::$_db = new PDO(
    +                    $options['dsn'],
    +                    $options['usr'],
    +                    $options['pwd'],
    +                    $options['opt']
    +                );
    +
    +                // check if the database contains the required tables
    +                self::$_type = strtolower(
    +                    substr($options['dsn'], 0, strpos($options['dsn'], ':'))
    +                );
    +                switch(self::$_type)
    +                {
    +                    case 'ibm':
    +                        $sql = 'SELECT tabname FROM SYSCAT.TABLES ';
    +                        break;
    +                    case 'informix':
    +                        $sql = 'SELECT tabname FROM systables ';
    +                        break;
    +                    case 'mssql':
    +                        $sql = "SELECT name FROM sysobjects "
    +                             . "WHERE type = 'U' ORDER BY name";
    +                        break;
    +                    case 'mysql':
    +                        $sql = 'SHOW TABLES';
    +                        break;
    +                    case 'oci':
    +                        $sql = 'SELECT table_name FROM all_tables';
    +                        break;
    +                    case 'pgsql':
    +                        $sql = "SELECT c.relname AS table_name "
    +                             . "FROM pg_class c, pg_user u "
    +                             . "WHERE c.relowner = u.usesysid AND c.relkind = 'r' "
    +                             . "AND NOT EXISTS (SELECT 1 FROM pg_views WHERE viewname = c.relname) "
    +                             . "AND c.relname !~ '^(pg_|sql_)' "
    +                             . "UNION "
    +                             . "SELECT c.relname AS table_name "
    +                             . "FROM pg_class c "
    +                             . "WHERE c.relkind = 'r' "
    +                             . "AND NOT EXISTS (SELECT 1 FROM pg_views WHERE viewname = c.relname) "
    +                             . "AND NOT EXISTS (SELECT 1 FROM pg_user WHERE usesysid = c.relowner) "
    +                             . "AND c.relname !~ '^pg_'";
    +                        break;
    +                    case 'sqlite':
    +                        $sql = "SELECT name FROM sqlite_master WHERE type='table' "
    +                             . "UNION ALL SELECT name FROM sqlite_temp_master "
    +                             . "WHERE type='table' ORDER BY name";
    +                        break;
    +                    default:
    +                        throw new Exception(
    +                            'PDO type ' .
    +                            self::$_type .
    +                            ' is currently not supported.'
    +                        );
    +                }
    +                $statement = self::$_db->query($sql);
    +                $tables = $statement->fetchAll(PDO::FETCH_COLUMN, 0);
    +
    +                // create paste table if needed
    +                if (!array_key_exists(self::$_prefix . 'paste', $tables))
    +                {
    +                    self::$_db->exec(
    +                        'CREATE TABLE ' . self::$_prefix . 'paste ( ' .
    +                        'dataid CHAR(16), ' .
    +                        'data TEXT, ' .
    +                        'postdate INT, ' .
    +                        'expiredate INT, ' .
    +                        'opendiscussion INT, ' .
    +                        'burnafterreading INT );'
    +                    );
    +                }
    +
    +                // create comment table if needed
    +                if (!array_key_exists(self::$_prefix . 'comment', $tables))
    +                {
    +                    self::$_db->exec(
    +                        'CREATE TABLE ' . self::$_prefix . 'comment ( ' .
    +                        'dataid CHAR(16), ' .
    +                        'pasteid CHAR(16), ' .
    +                        'parentid CHAR(16), ' .
    +                        'data TEXT, ' .
    +                        'nickname VARCHAR(255), ' .
    +                        'vizhash TEXT, ' .
    +                        'postdate INT );'
    +                    );
    +                }
    +            }
    +        }
    +
    +        return parent::$_instance;
    +    }
    +
    +    /**
    +     * Create a paste.
    +     *
    +     * @access public
    +     * @param  string $pasteid
    +     * @param  array  $paste
    +     * @return bool
    +     */
    +    public function create($pasteid, $paste)
    +    {
    +        if (
    +        		array_key_exists($pasteid, self::$_cache)
    +        ) {
    +            if(false !== self::$_cache[$pasteid]) {
    +                return false;
    +            } else {
    +                unset(self::$_cache[$pasteid]);
    +            }
    +        }
    +
    +        if (
    +            !array_key_exists('opendiscussion', $paste['meta'])
    +        ) $paste['meta']['opendiscussion'] = false;
    +        if (
    +            !array_key_exists('burnafterreading', $paste['meta'])
    +        ) $paste['meta']['burnafterreading'] = false;
    +        return self::_exec(
    +            'INSERT INTO ' . self::$_prefix . 'paste VALUES(?,?,?,?,?,?)',
    +            array(
    +                $pasteid,
    +                $paste['data'],
    +                $paste['meta']['postdate'],
    +                $paste['meta']['expire_date'],
    +                (int) $paste['meta']['opendiscussion'],
    +                (int) $paste['meta']['burnafterreading'],
    +            )
    +        );
    +    }
    +
    +    /**
    +     * Read a paste.
    +     *
    +     * @access public
    +     * @param  string $pasteid
    +     * @return stdClass|false
    +     */
    +    public function read($pasteid)
    +    {
    +        if (
    +            !array_key_exists($pasteid, self::$_cache)
    +        ) {
    +            self::$_cache[$pasteid] = false;
    +            $paste = self::_select(
    +                'SELECT * FROM ' . self::$_prefix . 'paste WHERE dataid = ?',
    +                array($pasteid), true
    +            );
    +
    +            if(false !== $paste) {
    +                // create object
    +                self::$_cache[$pasteid] = new stdClass;
    +                self::$_cache[$pasteid]->data = $paste['data'];
    +                self::$_cache[$pasteid]->meta = new stdClass;
    +                self::$_cache[$pasteid]->meta->postdate = (int) $paste['postdate'];
    +                self::$_cache[$pasteid]->meta->expire_date = (int) $paste['expiredate'];
    +                if (
    +                    $paste['opendiscussion']
    +                ) self::$_cache[$pasteid]->meta->opendiscussion = true;
    +                if (
    +                    $paste['burnafterreading']
    +                ) self::$_cache[$pasteid]->meta->burnafterreading = true;
    +            }
    +        }
    +
    +        return self::$_cache[$pasteid];
    +    }
    +
    +    /**
    +     * Delete a paste and its discussion.
    +     *
    +     * @access public
    +     * @param  string $pasteid
    +     * @return void
    +     */
    +    public function delete($pasteid)
    +    {
    +        self::_exec(
    +            'DELETE FROM ' . self::$_prefix . 'paste WHERE dataid = ?',
    +            array($pasteid)
    +        );
    +        self::_exec(
    +            'DELETE FROM ' . self::$_prefix . 'comment WHERE pasteid = ?',
    +            array($pasteid)
    +        );
    +        if (
    +        		array_key_exists($pasteid, self::$_cache)
    +        ) unset(self::$_cache[$pasteid]);
    +    }
    +
    +    /**
    +     * Test if a paste exists.
    +     *
    +     * @access public
    +     * @param  string $dataid
    +     * @return void
    +     */
    +    public function exists($pasteid)
    +    {
    +        if (
    +            !array_key_exists($pasteid, self::$_cache)
    +        ) self::$_cache[$pasteid] = $this->read($pasteid);
    +        return (bool) self::$_cache[$pasteid];
    +    }
    +
    +    /**
    +     * Create a comment in a paste.
    +     *
    +     * @access public
    +     * @param  string $pasteid
    +     * @param  string $parentid
    +     * @param  string $commentid
    +     * @param  array  $comment
    +     * @return int|false
    +     */
    +    public function createComment($pasteid, $parentid, $commentid, $comment)
    +    {
    +        return self::_exec(
    +            'INSERT INTO ' . self::$_prefix . 'comment VALUES(?,?,?,?,?,?,?)',
    +            array(
    +                $commentid,
    +                $pasteid,
    +                $parentid,
    +                $comment['data'],
    +                $comment['meta']['nickname'],
    +                $comment['meta']['vizhash'],
    +                $comment['meta']['postdate'],
    +            )
    +        );
    +    }
    +
    +    /**
    +     * Read all comments of paste.
    +     *
    +     * @access public
    +     * @param  string $pasteid
    +     * @return array
    +     */
    +    public function readComments($pasteid)
    +    {
    +        $rows = self::_select(
    +            'SELECT * FROM ' . self::$_prefix . 'comment WHERE pasteid = ?',
    +            array($pasteid)
    +        );
    +
    +        // create object
    +        $commentTemplate = new stdClass;
    +        $commentTemplate->meta = new stdClass;
    +
    +        // create comment list
    +        $comments = array();
    +        if (count($rows))
    +        {
    +            foreach ($rows as $row)
    +            {
    +                $i = (int) $row['postdate'];
    +                $comments[$i] = clone $commentTemplate;
    +                $comments[$i]->data = $row['data'];
    +                $comments[$i]->meta->nickname = $row['nickname'];
    +                $comments[$i]->meta->vizhash = $row['vizhash'];
    +                $comments[$i]->meta->postdate = $i;
    +                $comments[$i]->meta->commentid = $row['dataid'];
    +                $comments[$i]->meta->parentid = $row['parentid'];
    +            }
    +            ksort($comments);
    +        }
    +        return $comments;
    +    }
    +
    +    /**
    +     * Test if a comment exists.
    +     *
    +     * @access public
    +     * @param  string $dataid
    +     * @param  string $parentid
    +     * @param  string $commentid
    +     * @return void
    +     */
    +    public function existsComment($pasteid, $parentid, $commentid)
    +    {
    +        return (bool) self::_select(
    +            'SELECT dataid FROM ' . self::$_prefix . 'comment ' .
    +            'WHERE pasteid = ? AND parentid = ? AND dataid = ?',
    +            array($pasteid, $parentid, $commentid), true
    +        );
    +    }
    +
    +    /**
    +     * execute a statement
    +     *
    +     * @access private
    +     * @static
    +     * @param  string $sql
    +     * @param  array $params
    +     * @throws PDOException
    +     * @return array
    +     */
    +    private static function _exec($sql, array $params)
    +    {
    +        $statement = self::$_db->prepare($sql);
    +        $result = $statement->execute($params);
    +        $statement->closeCursor();
    +        return $result;
    +    }
    +
    +    /**
    +     * run a select statement
    +     *
    +     * @access private
    +     * @static
    +     * @param  string $sql
    +     * @param  array $params
    +     * @param  bool $firstOnly if only the first row should be returned
    +     * @throws PDOException
    +     * @return array
    +     */
    +    private static function _select($sql, array $params, $firstOnly = false)
    +    {
    +        $statement = self::$_db->prepare($sql);
    +        $statement->execute($params);
    +        $result = $firstOnly ?
    +            $statement->fetch(PDO::FETCH_ASSOC) :
    +            $statement->fetchAll(PDO::FETCH_ASSOC);
    +        $statement->closeCursor();
    +        return $result;
    +    }
    +}
    diff --git a/robots.txt b/robots.txt
    new file mode 100644
    index 00000000..41237327
    --- /dev/null
    +++ b/robots.txt
    @@ -0,0 +1,3 @@
    +# Might as well keep robots away for performance and privacy reasons.
    +User-agent: *
    +Disallow: /
    diff --git a/tpl/page.html b/tpl/page.html
    index 06873b03..a11721ce 100644
    --- a/tpl/page.html
    +++ b/tpl/page.html
    @@ -1,81 +1,96 @@
    -<html>
    -<head>
    -<title>ZeroBin</title>
    -<link type="text/css" rel="stylesheet" href="css/zerobin.css?{$VERSION|rawurlencode}#" /> 
    -<script src="js/jquery.js#"></script>
    -<script src="js/sjcl.js#"></script>
    -<script src="js/base64.js#"></script>
    -<script src="js/rawdeflate.js#"></script>
    -<script src="js/rawinflate.js#"></script>
    -<script src="js/zerobin.js?{$VERSION|rawurlencode}#"></script>
    -
    -<!--[if lt IE 10]>
    -<style> body {padding-left:60px;padding-right:60px;} div#ienotice {display:block;} </style>
    -<![endif]-->
    -
    -<!--[if lt IE 10]>
    -<style> div#ienotice {display:block; }  div#oldienotice {display:block; } </style>
    -<![endif]-->
    -
    -</head>
    -  <body>
    -      <div id="aboutbox">
    -          ZeroBin is a minimalist, opensource online pastebin where the server has zero knowledge of pasted data.
    -          Data is encrypted/decrypted <i>in the browser</i> using 256 bits AES. 
    -          More information on the <a href="http://sebsauvage.net/wiki/doku.php?id=php:zerobin">project page</a>.<br />
    -          <span style="text-decoration:blink;font-size:10pt;color:#a4b3c4;">&#9654;</span> Note: This is a test service: 
    -          Data may be deleted anytime. Kittens will die if you abuse this service.
    -</div>
    -    <h1 title="ZeroBin" onclick="window.location.href=scriptLocation();return false;">ZeroBin</h1><br>
    -    <h2>Because ignorance is bliss</h2><br>
    -    <h3>{$VERSION}</h3>
    -    <noscript><div class="nonworking">Javascript is required for ZeroBin to work.<br>Sorry for the inconvenience.</div></noscript>
    -    <div id="oldienotice" class="nonworking">ZeroBin requires a modern browser to work.</div>
    -    <div id="ienotice">Still using Internet Explorer ? &nbsp;Do yourself a favor, switch to a modern browser: 
    -        <a href="http://www.mozilla.org/firefox/">Firefox</a>, 
    -        <a href="http://www.opera.com/">Opera</a>, 
    -        <a href="http://www.google.com/chrome">Chrome</a>, 
    -        <a href="http://www.apple.com/safari">Safari</a>...
    -    </div>
    -    <div id="status">&nbsp;</div>
    -    <div id="errormessage" style="display:none">{$ERRORMESSAGE|htmlspecialchars}</div>
    -    <div id="toolbar">
    -    <button id="newbutton" onclick="window.location.href=scriptLocation();return false;" style="display:none;"><img src="img/icon_new.png#" width="11" height="15" />New</button>
    -    <button id="sendbutton" onclick="send_data();return false;" style="display:none;"><img src="img/icon_send.png#" width="18" height="15" />Send</button>
    -    <button id="clonebutton" onclick="clonePaste();return false;" style="display:none;"><img src="img/icon_clone.png#" width="15" height="17" />Clone</button>
    -      <div id="expiration" style="display:none;">Expire: 
    -      <select id="pasteExpiration" name="pasteExpiration">
    -        <option value="burn">Burn after reading</option>
    -        <option value="10min">10 minutes</option>
    -        <option value="1hour">1 hour</option>
    -        <option value="1day">1 day</option>
    -        <option value="1month" selected="selected">1 month</option>
    -        <option value="1year">1 year</option>
    -        <option value="never">Never</option>
    -      </select>
    -      </div>
    -      <div id="remainingtime" style="display:none;"></div>
    -      <div id="language" style="display:none;">
    -      <select name="language">
    -        <option value="language" selected="selected">Language</option>
    -        <option value="C/C++">C/C++</option>
    -        <option value="php">php</option>
    -        <option value="python">Python</option>
    -      </select>
    -      </div>
    -        <input id="password" value="Optional password..." style="display:none;" />
    -        <div id="opendisc" class="button" style="display:none;">
    -           <input type="checkbox" id="opendiscussion" name="opendiscussion" />
    -           <label for="opendiscussion">Open discussion</label>
    -        </div>
    -    </div>
    -    <div id="pastelink" style="display:none;"></div>
    -    <div id="cleartext" style="display:none;"></div>
    -    <textarea id="message" name="message" cols="80" rows="25" style="display:none;"></textarea>
    -    <div id="discussion" style="display:none;">
    -        <h4>Discussion</h4>
    -        <div id="comments"></div>
    -    </div>
    -    <div id="cipherdata" style="display:none;">{$CIPHERDATA}</div>
    +<!DOCTYPE html>
    +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
    +	<head>
    +		<meta charset="utf-8" />
    +		<meta name="robots" content="noindex" />
    +		<title>ZeroBin</title>
    +		<link type="text/css" rel="stylesheet" href="css/zerobin.css?{$VERSION|rawurlencode}#" /> 
    +		<link type="text/css" rel="stylesheet" href="css/prettify.css?{$VERSION|rawurlencode}#" />
    +		<script type="text/javascript" src="js/jquery.js#"></script>
    +		<script type="text/javascript" src="js/sjcl.js#"></script>
    +		<script type="text/javascript" src="js/base64.js#"></script>
    +		<script type="text/javascript" src="js/rawdeflate.js#"></script>
    +		<script type="text/javascript" src="js/rawinflate.js#"></script>
    +		<script type="text/javascript" src="js/prettify.js#"></script>
    +		<script type="text/javascript" src="js/zerobin.js?{$VERSION|rawurlencode}#"></script>
    +		<!--[if lt IE 10]>
    +		<style> body {padding-left:60px;padding-right:60px;} div#ienotice {display:block;} </style>
    +		<![endif]-->
    +		<!--[if lt IE 10]>
    +		<style> div#ienotice {display:block; }  div#oldienotice {display:block; } </style>
    +		<![endif]-->
    +	</head>
    +	<body>
    +		<header>
    +			<div id="aboutbox">
    +				ZeroBin is a minimalist, opensource online pastebin where the server has zero knowledge of pasted data.
    +				Data is encrypted/decrypted <i>in the browser</i> using 256 bits AES. 
    +				More information on the <a href="http://sebsauvage.net/wiki/doku.php?id=php:zerobin">project page</a>.<br />
    +				<span class="blink">▶</span> Note: This is a test service: 
    +				Data may be deleted anytime. Kittens will die if you abuse this service.
    +			</div>
    +			<h1 title="ZeroBin" onclick="window.location.href=scriptLocation();return false;">ZeroBin</h1><br />
    +			<h2>Because ignorance is bliss</h2><br />
    +			<h3>{$VERSION}</h3>
    +			<div id="noscript" class="nonworking">Javascript is required for ZeroBin to work.<br />Sorry for the inconvenience.</div>
    +			<div id="oldienotice" class="nonworking">ZeroBin requires a modern browser to work.</div>
    +	    <div id="ienotice">Still using Internet Explorer ?  Do yourself a favor, switch to a modern browser: 
    +				<a href="http://www.mozilla.org/firefox/">Firefox</a>, 
    +				<a href="http://www.opera.com/">Opera</a>, 
    +				<a href="http://www.google.com/chrome">Chrome</a>, 
    +				<a href="http://www.apple.com/safari">Safari</a>...
    +			</div>
    +		</header>
    +		<section>
    +			<article>
    +				<div id="status"> </div>
    +				<div id="errormessage" class="hidden">{$ERRORMESSAGE|htmlspecialchars}</div>
    +				<div id="toolbar">
    +					<button id="newbutton" onclick="window.location.href=scriptLocation();return false;" class="hidden"><img src="img/icon_new.png#" width="11" height="15" alt="" />New</button>
    +					<button id="sendbutton" onclick="send_data();return false;" class="hidden"><img src="img/icon_send.png#" width="18" height="15" alt="" />Send</button>
    +					<button id="clonebutton" onclick="clonePaste();return false;" class="hidden"><img src="img/icon_clone.png#" width="15" height="17" alt="" />Clone</button>
    +		      <div id="expiration" class="hidden">Expire: 
    +			      <select id="pasteExpiration" name="pasteExpiration">
    +							<option value="burn">Burn after reading</option>
    +							<option value="5min">5 minutes</option>
    +							<option value="10min">10 minutes</option>
    +							<option value="1hour">1 hour</option>
    +							<option value="1day">1 day</option>
    +							<option value="1week">1 week</option>
    +							<option value="1month" selected="selected">1 month</option>
    +							<option value="1year">1 year</option>
    +							<option value="never">Never</option>
    +						</select>
    +					</div>
    +					<div id="remainingtime" class="hidden"></div>
    +					<div id="language" class="hidden">
    +						<select name="language">
    +							<option value="language" selected="selected">Language</option>
    +							<option value="C/C++">C/C++</option>
    +							<option value="php">php</option>
    +							<option value="python">Python</option>
    +						</select>
    +					</div>
    +					<input id="password" value="Optional password..." class="hidden" />
    +					<div id="opendisc" class="button hidden">
    +						<input type="checkbox" id="opendiscussion" name="opendiscussion" {if="!$OPENDISCUSSION"} disabled="disabled"{/if} />
    +						<label for="opendiscussion">Open discussion</label>
    +					</div>
    +				</div>
    +				<div id="pastelink" class="hidden"></div>
    +				<div id="prettymessage" class="hidden">
    +					<pre id="prettyprint" class="prettyprint linenums:1"></pre>
    +				</div>
    +				<div id="cleartext" class="hidden"></div>
    +				<textarea id="message" name="message" cols="80" rows="25" class="hidden"></textarea>
    +			</article>
    +		</section>
    +		<section>
    +			<div id="discussion" class="hidden">
    +				<h4>Discussion</h4>
    +				<div id="comments"></div>
    +			</div>
    +		</section>
    +		<div id="cipherdata" class="hidden">{$CIPHERDATA}</div>
       </body>
     </html>
    diff --git a/tst/README.md b/tst/README.md
    new file mode 100644
    index 00000000..ad21697a
    --- /dev/null
    +++ b/tst/README.md
    @@ -0,0 +1,16 @@
    +Running unit tests
    +==================
    +
    +In order to run these tests, you will need to install the following packages
    +and its dependencies:
    +* phpunit
    +* php5-gd
    +* php5-sqlite
    +* php5-xdebug
    +
    +Example for Debian and Ubuntu:
    +    $ sudo aptitude install phpunit php5-mysql php5-xdebug
    +
    +To run the tests, just change into this directory and run phpunit:
    +    $ cd ZeroBin/tst
    +    $ phpunit
    diff --git a/tst/RainTPL.php b/tst/RainTPL.php
    new file mode 100644
    index 00000000..bbea6f61
    --- /dev/null
    +++ b/tst/RainTPL.php
    @@ -0,0 +1,77 @@
    +<?php
    +class RainTPLTest extends PHPUnit_Framework_TestCase
    +{
    +    private static $data = '{"iv":"EN39/wd5Nk8HAiSG2K5AsQ","salt":"QKN1DBXe5PI","ct":"8hA83xDdXjD7K2qfmw5NdA"}';
    +
    +    private static $error = 'foo bar';
    +
    +    private static $version = 'Version 1.2.3';
    +
    +    private $_content;
    +
    +    public function setUp()
    +    {
    +        /* Setup Routine */
    +        $page = new RainTPL;
    +        $page::configure(array('cache_dir' => 'tmp/'));
    +        // We escape it here because ENT_NOQUOTES can't be used in RainTPL templates.
    +        $page->assign('CIPHERDATA', htmlspecialchars(self::$data, ENT_NOQUOTES));
    +        $page->assign('ERRORMESSAGE', self::$error);
    +        $page->assign('OPENDISCUSSION', false);
    +        $page->assign('VERSION', self::$version);
    +        ob_start();
    +        $page->draw('page');
    +        $this->_content = ob_get_contents();
    +        // run a second time from cache
    +        $page->cache('page');
    +        $page->draw('page');
    +        ob_end_clean();
    +    }
    +
    +    public function tearDown()
    +    {
    +        /* Tear Down Routine */
    +        helper::rmdir(PATH . 'tmp');
    +    }
    +
    +    public function testTemplateRendersCorrectly()
    +    {
    +        $this->assertTag(
    +            array(
    +                'id' => 'cipherdata',
    +                'content' => htmlspecialchars(self::$data, ENT_NOQUOTES)
    +            ),
    +            $this->_content,
    +            'outputs data correctly'
    +        );
    +        $this->assertTag(
    +            array(
    +                'id' => 'errormessage',
    +                'content' => self::$error
    +            ),
    +            $this->_content,
    +            'outputs error correctly'
    +        );
    +        $this->assertTag(
    +            array(
    +                'id' => 'opendiscussion',
    +                'attributes' => array(
    +                    'disabled' => 'disabled'
    +                ),
    +            ),
    +            $this->_content,
    +            'disables discussions if configured'
    +        );
    +        // testing version number in JS address, since other instances may not be present in different templates
    +        $this->assertTag(
    +            array(
    +                'tag' => 'script',
    +                'attributes' => array(
    +                    'src' => 'js/zerobin.js?' . rawurlencode(self::$version)
    +                ),
    +            ),
    +            $this->_content,
    +            'outputs version correctly'
    +        );
    +    }
    +}
    diff --git a/tst/auto.php b/tst/auto.php
    new file mode 100644
    index 00000000..f8cb4c09
    --- /dev/null
    +++ b/tst/auto.php
    @@ -0,0 +1,8 @@
    +<?php
    +class autoTest extends PHPUnit_Framework_TestCase
    +{
    +    public function testAutoloaderReturnsFalseWhenCallingNonExistingClass()
    +    {
    +        $this->assertFalse(auto::loader('foo2501bar42'), 'calling non existent class');
    +    }
    +}
    diff --git a/tst/bootstrap.php b/tst/bootstrap.php
    new file mode 100644
    index 00000000..117a0aa8
    --- /dev/null
    +++ b/tst/bootstrap.php
    @@ -0,0 +1,31 @@
    +<?php
    +error_reporting( E_ALL | E_STRICT );
    +
    +// change this, if your php files and data is outside of your webservers document root
    +define('PATH', '..' . DIRECTORY_SEPARATOR);
    +
    +require PATH . 'lib/auto.php';
    +
    +class helper
    +{
    +    public static function rmdir($path)
    +    {
    +    	$path .= DIRECTORY_SEPARATOR;
    +    	$dir = dir($path);
    +    	while(false !== ($file = $dir->read())) {
    +    		if($file != '.' && $file != '..') {
    +    			if(is_dir($path . $file)) {
    +    				self::rmdir($path . $file);
    +    			} elseif(is_file($path . $file)) {
    +    				if(!@unlink($path . $file)) {
    +    					throw new Exception('Error deleting file "' . $path . $file . '".');
    +    				}
    +    			}
    +    		}
    +    	}
    +    	$dir->close();
    +    	if(!@rmdir($path)) {
    +    		throw new Exception('Error deleting directory "' . $path . '".');
    +    	}
    +    }
    +}
    \ No newline at end of file
    diff --git a/tst/filter.php b/tst/filter.php
    new file mode 100644
    index 00000000..198a13f5
    --- /dev/null
    +++ b/tst/filter.php
    @@ -0,0 +1,47 @@
    +<?php
    +class filterTest extends PHPUnit_Framework_TestCase
    +{
    +    public function testFilterStripsSlashesDeeply()
    +    {
    +        $this->assertEquals(
    +            array("f'oo", "b'ar", array("fo'o", "b'ar")),
    +            filter::stripslashes_deep(array("f\\'oo", "b\\'ar", array("fo\\'o", "b\\'ar")))
    +        );
    +    }
    +
    +    public function testFilterMakesSizesHumanlyReadable()
    +    {
    +        $this->assertEquals('1 B', filter::size_humanreadable(1));
    +        $this->assertEquals('1 000 B', filter::size_humanreadable(1000));
    +        $this->assertEquals('1.00 kiB', filter::size_humanreadable(1024));
    +        $this->assertEquals('1.21 kiB', filter::size_humanreadable(1234));
    +        $exponent = 1024;
    +        $this->assertEquals('1 000.00 kiB', filter::size_humanreadable(1000 * $exponent));
    +        $this->assertEquals('1.00 MiB', filter::size_humanreadable(1024 * $exponent));
    +        $this->assertEquals('1.21 MiB', filter::size_humanreadable(1234 * $exponent));
    +        $exponent *= 1024;
    +        $this->assertEquals('1 000.00 MiB', filter::size_humanreadable(1000 * $exponent));
    +        $this->assertEquals('1.00 GiB', filter::size_humanreadable(1024 * $exponent));
    +        $this->assertEquals('1.21 GiB', filter::size_humanreadable(1234 * $exponent));
    +        $exponent *= 1024;
    +        $this->assertEquals('1 000.00 GiB', filter::size_humanreadable(1000 * $exponent));
    +        $this->assertEquals('1.00 TiB', filter::size_humanreadable(1024 * $exponent));
    +        $this->assertEquals('1.21 TiB', filter::size_humanreadable(1234 * $exponent));
    +        $exponent *= 1024;
    +        $this->assertEquals('1 000.00 TiB', filter::size_humanreadable(1000 * $exponent));
    +        $this->assertEquals('1.00 PiB', filter::size_humanreadable(1024 * $exponent));
    +        $this->assertEquals('1.21 PiB', filter::size_humanreadable(1234 * $exponent));
    +        $exponent *= 1024;
    +        $this->assertEquals('1 000.00 PiB', filter::size_humanreadable(1000 * $exponent));
    +        $this->assertEquals('1.00 EiB', filter::size_humanreadable(1024 * $exponent));
    +        $this->assertEquals('1.21 EiB', filter::size_humanreadable(1234 * $exponent));
    +        $exponent *= 1024;
    +        $this->assertEquals('1 000.00 EiB', filter::size_humanreadable(1000 * $exponent));
    +        $this->assertEquals('1.00 ZiB', filter::size_humanreadable(1024 * $exponent));
    +        $this->assertEquals('1.21 ZiB', filter::size_humanreadable(1234 * $exponent));
    +        $exponent *= 1024;
    +        $this->assertEquals('1 000.00 ZiB', filter::size_humanreadable(1000 * $exponent));
    +        $this->assertEquals('1.00 YiB', filter::size_humanreadable(1024 * $exponent));
    +        $this->assertEquals('1.21 YiB', filter::size_humanreadable(1234 * $exponent));
    +    }
    +}
    diff --git a/tst/phpunit.xml b/tst/phpunit.xml
    new file mode 100644
    index 00000000..035b040a
    --- /dev/null
    +++ b/tst/phpunit.xml
    @@ -0,0 +1,17 @@
    +<phpunit bootstrap="bootstrap.php" colors="true">
    +	<testsuite name="ZeroBin Test Suite">
    +		<directory suffix=".php">./</directory>
    +	</testsuite>
    +	<filter>
    +		<whitelist>
    +			<directory suffix=".php">../lib</directory>
    +			<exclude>
    +				<file>../lib/zerobin/abstract.php</file>
    +			</exclude>
    +		</whitelist>
    +	</filter>
    +	<logging>
    +		<log type="coverage-html" target="log/coverage-report" charset="UTF-8" yui="true" highlight="true" lowUpperBound="50" highLowerBound="80" />
    +		<log type="testdox-html" target="log/testdox.html" />
    +	</logging>
    +</phpunit>
    \ No newline at end of file
    diff --git a/tst/sjcl.php b/tst/sjcl.php
    new file mode 100644
    index 00000000..b90f054e
    --- /dev/null
    +++ b/tst/sjcl.php
    @@ -0,0 +1,14 @@
    +<?php
    +class sjclTest extends PHPUnit_Framework_TestCase
    +{
    +    public function testSjclValidatorValidatesCorrectly()
    +    {
    +        $this->assertTrue(sjcl::isValid('{"iv":"83Ax/OdUav3SanDW9dcQPg","salt":"Gx1vA2/gQ3U","ct":"j7ImByuE5xCqD2YXm6aSyA"}'), 'valid sjcl');
    +        $this->assertFalse(sjcl::isValid('{"iv":"$","salt":"Gx1vA2/gQ3U","ct":"j7ImByuE5xCqD2YXm6aSyA"}'), 'invalid base64 encoding of iv');
    +        $this->assertFalse(sjcl::isValid('{"iv":"83Ax/OdUav3SanDW9dcQPg","salt":"$","ct":"j7ImByuE5xCqD2YXm6aSyA"}'), 'invalid base64 encoding of salt');
    +        $this->assertFalse(sjcl::isValid('{"iv":"83Ax/OdUav3SanDW9dcQPg","salt":"Gx1vA2/gQ3U","ct":"$"}'), 'invalid base64 encoding of ct');
    +        $this->assertFalse(sjcl::isValid('{"iv":"MTIzNDU2Nzg5MDEyMzQ1Njc4OTA=","salt":"Gx1vA2/gQ3U","ct":"j7ImByuE5xCqD2YXm6aSyA"}'), 'iv to long');
    +        $this->assertFalse(sjcl::isValid('{"iv":"83Ax/OdUav3SanDW9dcQPg","salt":"MTIzNDU2Nzg5MDEyMzQ1Njc4OTA=","ct":"j7ImByuE5xCqD2YXm6aSyA"}'), 'salt to long');
    +        $this->assertFalse(sjcl::isValid('{"iv":"83Ax/OdUav3SanDW9dcQPg","salt":"Gx1vA2/gQ3U","ct":"j7ImByuE5xCqD2YXm6aSyA","foo":"MTIzNDU2Nzg5MDEyMzQ1Njc4OTA="}'), 'invalid additional key');
    +    }
    +}
    diff --git a/tst/trafficlimiter.php b/tst/trafficlimiter.php
    new file mode 100644
    index 00000000..68947b95
    --- /dev/null
    +++ b/tst/trafficlimiter.php
    @@ -0,0 +1,30 @@
    +<?php
    +class trafficlimiterTest extends PHPUnit_Framework_TestCase
    +{
    +    private $_path;
    +
    +    public function setUp()
    +    {
    +        /* Setup Routine */
    +        $this->_path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'trafficlimit' . DIRECTORY_SEPARATOR;
    +        trafficlimiter::setPath($this->_path);
    +    }
    +
    +    public function tearDown()
    +    {
    +        /* Tear Down Routine */
    +        helper::rmdir($this->_path);
    +    }
    +
    +    public function testTrafficGetsLimited()
    +    {
    +        trafficlimiter::setLimit(4);
    +        $this->assertTrue(trafficlimiter::canPass('127.0.0.1'), 'first request may pass');
    +        sleep(2);
    +        $this->assertFalse(trafficlimiter::canPass('127.0.0.1'), 'second request is to fast, may not pass');
    +        sleep(3);
    +        $this->assertTrue(trafficlimiter::canPass('127.0.0.1'), 'third request waited long enough and may pass');
    +        $this->assertTrue(trafficlimiter::canPass('2001:1620:2057:dead:beef::cafe:babe'), 'fourth request has different ip and may pass');
    +        $this->assertFalse(trafficlimiter::canPass('127.0.0.1'), 'fifth request is to fast, may not pass');
    +    }
    +}
    diff --git a/tst/vizhash16x16.php b/tst/vizhash16x16.php
    new file mode 100644
    index 00000000..cf73011c
    --- /dev/null
    +++ b/tst/vizhash16x16.php
    @@ -0,0 +1,41 @@
    +<?php
    +class vizhash16x16Test extends PHPUnit_Framework_TestCase
    +{
    +    private $_dataDirCreated;
    +
    +    private $_file;
    +
    +    private $_path;
    +
    +    public function setUp()
    +    {
    +        /* Setup Routine */
    +        $this->_path = PATH . 'data' . DIRECTORY_SEPARATOR;
    +        $this->_dataDirCreated = !is_dir($this->_path);
    +        if($this->_dataDirCreated) mkdir($this->_path);
    +        $this->_file = $this->_path . 'vizhash.png';
    +    }
    +
    +    public function tearDown()
    +    {
    +        /* Tear Down Routine */
    +    		if($this->_dataDirCreated) {
    +		        helper::rmdir($this->_path);
    +        } else {
    +            if(!@unlink($this->_file)) {
    +            	throw new Exception('Error deleting file "' . $this->_file . '".');
    +            }
    +        }
    +    }
    +
    +    public function testVizhashGeneratesUniquePngsPerIp()
    +    {
    +        $vz = new vizhash16x16();
    +        $pngdata = $vz->generate('127.0.0.1');
    +        file_put_contents($this->_file, $pngdata);
    +        $finfo = new finfo(FILEINFO_MIME_TYPE);
    +        $this->assertEquals('image/png', $finfo->file($this->_file));
    +        $this->assertNotEquals($pngdata, $vz->generate('2001:1620:2057:dead:beef::cafe:babe'));
    +        $this->assertEquals($pngdata, $vz->generate('127.0.0.1'));
    +    }
    +}
    diff --git a/tst/zerobin/data.php b/tst/zerobin/data.php
    new file mode 100644
    index 00000000..7e85285a
    --- /dev/null
    +++ b/tst/zerobin/data.php
    @@ -0,0 +1,70 @@
    +<?php
    +class zerobin_dataTest extends PHPUnit_Framework_TestCase
    +{
    +    private static $pasteid = '501f02e9eeb8bcec';
    +
    +    private static $paste = array(
    +        'data' => '{"iv":"EN39/wd5Nk8HAiSG2K5AsQ","salt":"QKN1DBXe5PI","ct":"8hA83xDdXjD7K2qfmw5NdA"}',
    +        'meta' => array(
    +            'postdate' => 1344803344,
    +            'expire_date' => 1344803644,
    +            'opendiscussion' => true,
    +        ),
    +    );
    +
    +    private static $commentid = 'c47efb4741195f42';
    +
    +    private static $comment = array(
    +        'data' => '{"iv":"Pd4pOKWkmDTT9uPwVwd5Ag","salt":"ZIUhFTliVz4","ct":"6nOCU3peNDclDDpFtJEBKA"}',
    +        'meta' => array(
    +            'nickname' => '{"iv":"76MkAtOGC4oFogX/aSMxRA","salt":"ZIUhFTliVz4","ct":"b6Ae/U1xJdsX/+lATud4sQ"}',
    +            'vizhash' => 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABGUlEQVQokWOsl5/94983CNKQMjnxaOePf98MeKwPfNjkLZ3AgARab6b9+PeNEVnDj3/ff/z7ZiHnzsDA8Pv7H2TVPJw8EAYLAwb48OaVgIgYKycLsrYv378wMDB8//qdCVMDRA9EKSsnCwRBxNsepaLboMFlyMDAICAi9uHNK24GITQ/MDAwoNhgIGMLtwGrzegaLjw5jMz9+vUdnN17uwDCQDhJgk0O07yvX9+teDX1x79v6DYIsIjgcgMaYGFgYOBg4kJx2JejkAiBxAw+PzAwMNz4dp6wDXDw4MdNNOl0rWYsNkD89OLXI/xmo9sgzatJjAYmBgYGDiauD3/ePP18nVgb4MF89+M5ZX6js293wUMpnr8KTQMAxsCJnJ30apMAAAAASUVORK5CYII=',
    +            'postdate' => 1344803528,
    +        ),
    +    );
    +
    +    private $_model;
    +
    +    private $_path;
    +
    +    public function setUp()
    +    {
    +        /* Setup Routine */
    +        $this->_path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'zerobin_data';
    +        $this->_model = zerobin_data::getInstance(array('dir' => $this->_path));
    +    }
    +
    +    public function tearDown()
    +    {
    +        /* Tear Down Routine */
    +        helper::rmdir($this->_path);
    +    }
    +
    +    public function testFileBasedDataStoreWorks()
    +    {
    +        // storing pastes
    +        $this->assertFalse($this->_model->exists(self::$pasteid), 'paste does not yet exist');
    +        $this->assertTrue($this->_model->create(self::$pasteid, self::$paste), 'store new paste');
    +        $this->assertTrue($this->_model->exists(self::$pasteid), 'paste exists after storing it');
    +        $this->assertFalse($this->_model->create(self::$pasteid, self::$paste), 'unable to store the same paste twice');
    +        $this->assertEquals(json_decode(json_encode(self::$paste)), $this->_model->read(self::$pasteid));
    +
    +        // storing comments
    +        $this->assertFalse($this->_model->existsComment(self::$pasteid, self::$pasteid, self::$commentid), 'comment does not yet exist');
    +        $this->assertTrue($this->_model->createComment(self::$pasteid, self::$pasteid, self::$commentid, self::$comment) !== false, 'store comment');
    +        $this->assertTrue($this->_model->existsComment(self::$pasteid, self::$pasteid, self::$commentid), 'comment exists after storing it');
    +        $comment = json_decode(json_encode(self::$comment));
    +        $comment->meta->commentid = self::$commentid;
    +        $comment->meta->parentid = self::$pasteid;
    +        $this->assertEquals(
    +            array($comment->meta->postdate => $comment),
    +            $this->_model->readComments(self::$pasteid)
    +        );
    +
    +        // deleting pastes
    +        $this->_model->delete(self::$pasteid);
    +        $this->assertFalse($this->_model->exists(self::$pasteid), 'paste successfully deleted');
    +        $this->assertFalse($this->_model->existsComment(self::$pasteid, self::$pasteid, self::$commentid), 'comment was deleted with paste');
    +        $this->assertFalse($this->_model->read(self::$pasteid), 'paste can no longer be found');
    +    }
    +}
    diff --git a/tst/zerobin/db.php b/tst/zerobin/db.php
    new file mode 100644
    index 00000000..132f290a
    --- /dev/null
    +++ b/tst/zerobin/db.php
    @@ -0,0 +1,68 @@
    +<?php
    +class zerobin_dbTest extends PHPUnit_Framework_TestCase
    +{
    +    private static $pasteid = '501f02e9eeb8bcec';
    +
    +    private static $paste = array(
    +        'data' => '{"iv":"EN39/wd5Nk8HAiSG2K5AsQ","salt":"QKN1DBXe5PI","ct":"8hA83xDdXjD7K2qfmw5NdA"}',
    +        'meta' => array(
    +            'postdate' => 1344803344,
    +            'expire_date' => 1344803644,
    +            'opendiscussion' => true,
    +        ),
    +    );
    +
    +    private static $commentid = 'c47efb4741195f42';
    +
    +    private static $comment = array(
    +        'data' => '{"iv":"Pd4pOKWkmDTT9uPwVwd5Ag","salt":"ZIUhFTliVz4","ct":"6nOCU3peNDclDDpFtJEBKA"}',
    +        'meta' => array(
    +            'nickname' => '{"iv":"76MkAtOGC4oFogX/aSMxRA","salt":"ZIUhFTliVz4","ct":"b6Ae/U1xJdsX/+lATud4sQ"}',
    +            'vizhash' => 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABGUlEQVQokWOsl5/94983CNKQMjnxaOePf98MeKwPfNjkLZ3AgARab6b9+PeNEVnDj3/ff/z7ZiHnzsDA8Pv7H2TVPJw8EAYLAwb48OaVgIgYKycLsrYv378wMDB8//qdCVMDRA9EKSsnCwRBxNsepaLboMFlyMDAICAi9uHNK24GITQ/MDAwoNhgIGMLtwGrzegaLjw5jMz9+vUdnN17uwDCQDhJgk0O07yvX9+teDX1x79v6DYIsIjgcgMaYGFgYOBg4kJx2JejkAiBxAw+PzAwMNz4dp6wDXDw4MdNNOl0rWYsNkD89OLXI/xmo9sgzatJjAYmBgYGDiauD3/ePP18nVgb4MF89+M5ZX6js293wUMpnr8KTQMAxsCJnJ30apMAAAAASUVORK5CYII=',
    +            'postdate' => 1344803528,
    +        ),
    +    );
    +
    +    private $_model;
    +
    +    public function setUp()
    +    {
    +        /* Setup Routine */
    +        $this->_model = zerobin_db::getInstance(
    +            array(
    +                'dsn' => 'sqlite::memory:',
    +                'usr' => null,
    +                'pwd' => null,
    +                'opt' => array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION),
    +            )
    +        );
    +    }
    +
    +    public function testDatabaseBasedDataStoreWorks()
    +    {
    +        // storing pastes
    +        $this->assertFalse($this->_model->exists(self::$pasteid), 'paste does not yet exist');
    +        $this->assertTrue($this->_model->create(self::$pasteid, self::$paste), 'store new paste');
    +        $this->assertTrue($this->_model->exists(self::$pasteid), 'paste exists after storing it');
    +        $this->assertFalse($this->_model->create(self::$pasteid, self::$paste), 'unable to store the same paste twice');
    +        $this->assertEquals(json_decode(json_encode(self::$paste)), $this->_model->read(self::$pasteid));
    +
    +        // storing comments
    +        $this->assertFalse($this->_model->existsComment(self::$pasteid, self::$pasteid, self::$commentid), 'comment does not yet exist');
    +        $this->assertTrue($this->_model->createComment(self::$pasteid, self::$pasteid, self::$commentid, self::$comment) !== false, 'store comment');
    +        $this->assertTrue($this->_model->existsComment(self::$pasteid, self::$pasteid, self::$commentid), 'comment exists after storing it');
    +        $comment = json_decode(json_encode(self::$comment));
    +        $comment->meta->commentid = self::$commentid;
    +        $comment->meta->parentid = self::$pasteid;
    +        $this->assertEquals(
    +            array($comment->meta->postdate => $comment),
    +            $this->_model->readComments(self::$pasteid)
    +        );
    +
    +        // deleting pastes
    +        $this->_model->delete(self::$pasteid);
    +        $this->assertFalse($this->_model->exists(self::$pasteid), 'paste successfully deleted');
    +        $this->assertFalse($this->_model->existsComment(self::$pasteid, self::$pasteid, self::$commentid), 'comment was deleted with paste');
    +        $this->assertFalse($this->_model->read(self::$pasteid), 'paste can no longer be found');
    +    }
    +}