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:
} 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.
+ * } or {@code } element to specify the
+ * language, as in {@code }. 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.
+ *
+ * Change log:
+ * cbeust, 2006/08/22
+ *
+ * Java annotations (start with "@") are now captured as literals ("lit")
+ *
+ * @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.
+ *
+ * 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.
+ *
+ *
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.} 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.
+ *
+ *
+ * The HTML DOM structure:
+ *
+ * (Element "p"
+ * (Element "b"
+ * (Text "print ")) ; #1
+ * (Text "'Hello '") ; #2
+ * (Element "br") ; #3
+ * (Text " + 'World';")) ; #4
+ *
+ *
+ * corresponds to the HTML
+ * {@code
print 'Hello '
+ 'World';
}.
+ *
+ *
+ * It will produce the output:
+ *
+ * {
+ * sourceCode: "print 'Hello '\n + 'World';",
+ * // 1 2
+ * // 012345678901234 5678901234567
+ * spans: [0, #1, 6, #2, 14, #3, 15, #4]
+ * }
+ *
+ *
+ * where #1 is a reference to the {@code "print "} text node above, and so
+ * on for the other text nodes.
+ *
+ *
+ *
+ * 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.
+ *
+ *
+ * @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.
+ *
+ * This is meant to return the CODE element in {@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 ......
} 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
+ * '
-
-
-
-
-
-
-
-
-
-
-
-
-
- ZeroBin is a minimalist, opensource online pastebin where the server has zero knowledge of pasted data.
- Data is encrypted/decrypted in the browser using 256 bits AES.
- More information on the project page.
- ▶ Note: This is a test service:
- Data may be deleted anytime. Kittens will die if you abuse this service.
-
- ZeroBin
- Because ignorance is bliss
- {$VERSION}
-
- ZeroBin requires a modern browser to work.
- Still using Internet Explorer ? Do yourself a favor, switch to a modern browser:
- Firefox,
- Opera,
- Chrome,
- Safari...
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+ ZeroBin
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ZeroBin is a minimalist, opensource online pastebin where the server has zero knowledge of pasted data.
+ Data is encrypted/decrypted in the browser using 256 bits AES.
+ More information on the project page.
+ ▶ Note: This is a test service:
+ Data may be deleted anytime. Kittens will die if you abuse this service.
+
+ ZeroBin
+ Because ignorance is bliss
+ {$VERSION}
+ Javascript is required for ZeroBin to work.
Sorry for the inconvenience.
+ ZeroBin requires a modern browser to work.
+
+
+
+
+
+ {$ERRORMESSAGE|htmlspecialchars}
+
+
+
+
+ Expire:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Discussion
+
+
+
+ {$CIPHERDATA}
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 @@
+ '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 @@
+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 @@
+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 @@
+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 @@
+
+
+ ./
+
+
+
+ ../lib
+
+ ../lib/zerobin/abstract.php
+
+
+
+
+
+
+
+
\ 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 @@
+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 @@
+_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 @@
+_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 @@
+ '{"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 @@
+ '{"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');
+ }
+}