41 Commits
0.21 ... 0.22

Author SHA1 Message Date
El RIDO
6b3ad32665 Updated credits and changelog 2015-11-29 18:23:38 +01:00
Adam Fisher
0997520c1d Add favicon
Conflicts:
	tpl/page.html
2015-11-29 17:50:50 +01:00
El RIDO
2dc4422a27 separating the dark bootstrap theme in to two variants regarding the
new/send button placements
2015-11-16 20:06:06 +01:00
El RIDO
3fe7e77390 added changelog entry for #55 fix 2015-11-16 20:05:08 +01:00
El RIDO
ce107c928e supporting disabled password option in the JS part, fixes #55 2015-11-16 19:58:45 +01:00
El RIDO
24a4328c55 incrementing version, updating changelog, added missing phpdoc comments 2015-11-09 21:39:42 +01:00
El RIDO
bba485ef6d adding remarks as discussed in #53 2015-11-09 20:43:24 +01:00
El RIDO
d8ae1be2ff updating database documentation 2015-11-01 17:10:36 +01:00
El RIDO
42a9c92b5e improved database backend support for larger files (100 KiB - 16 MiB),
introduced database versioning to reduce amount of checks done per
request
2015-11-01 17:02:20 +01:00
El RIDO
9d27e7a65d added dark bootstrap theme, resolving #47 2015-10-31 20:56:55 +01:00
El RIDO
d42975580a expire_options and formatter_options should not be filled up with
default values, resolves #52
2015-10-24 08:44:17 +02:00
El RIDO
176dff3b70 renaming config file to make updates easier, resolving #50 2015-10-22 21:13:15 +02:00
El RIDO
5a9879623f added incorrectly ignored unit test file, correcting ignore 2015-10-22 20:51:01 +02:00
El RIDO
740d62005e small CSS improvement, partially resolves #48 2015-10-19 19:39:45 +02:00
El RIDO
40019624fd wrap long lines in plaintext format but force horizontal scroll on
syntaxhighlighting, resolves #45
2015-10-18 22:16:15 +02:00
El RIDO
e3f4aa982c adding configuration option to set a default language and/or force it,
resolves #39
2015-10-18 20:38:07 +02:00
El RIDO
ca07398b66 adding option to hide clone button on expiring pastes, resolves #34 2015-10-18 17:56:45 +02:00
El RIDO
f96b0c0afe adding unit tests for all JSON-LD cases 2015-10-18 14:46:07 +02:00
El RIDO
14d08ec56d working on JSON-LD validity, added CORS headers preparing external API
call support
2015-10-18 14:37:58 +02:00
El RIDO
22d0b1ec22 updating comment format to match defined JSON-LD API context 2015-10-18 11:38:48 +02:00
El RIDO
f21567133c changing paste read output for API refactoring 2015-10-18 11:08:28 +02:00
El RIDO
b92b38cee8 found and resolved issues in database layer, thanks to report in #42 2015-10-16 23:13:36 +02:00
El RIDO
87b41a0c3d implemented tab input support from #40, thank you azlux! 2015-10-15 22:06:01 +02:00
El RIDO
2e3bacb699 fixing deletion issue in request refactoring, starting work on API read
refactoring
2015-10-15 22:04:57 +02:00
El RIDO
5d61b90d6b Changing template as per issue #35 2015-10-14 20:43:51 +02:00
El RIDO
512b3d1172 fixing "missing" comments when they were posted during the same second 2015-10-12 21:07:41 +02:00
El RIDO
1d6cfb7f3b refactoring delete API, added external JSON-LD context 2015-10-11 21:22:00 +02:00
El RIDO
9e6e29bc93 working on API: simplifying PUT request mocking 2015-10-11 18:50:48 +02:00
El RIDO
e5b096ed8c found and fixed a bug when using expiration together with discussion 2015-10-03 17:54:18 +02:00
El RIDO
add980d36f adding UI tests for database configuration, fixed an issue with comment
table creation
2015-10-03 15:52:37 +02:00
El RIDO
7ec94e0db5 implementing request refactoring, beginning JS changes for JSON API, but
discovered that DELETE and PUT are not available on all webservers by
default
2015-09-27 20:34:39 +02:00
El RIDO
6b7dc44039 preparing unit test for request object 2015-09-27 15:37:17 +02:00
El RIDO
ce3f10f143 improving unit tests, fixing regression in DB model 2015-09-27 14:36:20 +02:00
El RIDO
694138c5d4 mostly finished with data model refactoring 2015-09-27 03:03:55 +02:00
El RIDO
211d3e4622 preparing unit test for model refactoring, refactoring traffic limiter 2015-09-26 17:57:46 +02:00
El RIDO
d04eab52c9 refactoring how attachments are stored 2015-09-26 12:29:27 +02:00
El RIDO
22b4c89227 adapting configGenerator for 0.21 2015-09-22 23:45:52 +02:00
El RIDO
6d24ff824e refactoring configuration 2015-09-22 23:21:31 +02:00
El RIDO
9f68658106 incrementing version number, updating changelog 2015-09-21 22:43:00 +02:00
El RIDO
0de9f868fa improving unit tests, fixing #38 2015-09-21 22:32:52 +02:00
El RIDO
0686087cfd fixing mobile navbar triggered issue and slight adjustement to bootstrap
template for using full width of browser for navbar
2015-09-20 20:05:48 +02:00
60 changed files with 12119 additions and 1119 deletions

View File

@@ -1,5 +1,25 @@
# ZeroBin version history # # ZeroBin version history #
* **0.22 (2015-11-09)**:
* ADDED: Tab character input support
* ADDED: Dark bootstrap theme
* ADDED: Option to hide clone button on expiring pastes
* ADDED: Option to set a different default language then English and/or enforce it as the only language
* ADDED: Database now contains version to allow automatic update of structure, only if necessary; removing database structure check on each request
* ADDED: Favicons
* FIXING: Regressions in database layer, prohibiting pastes from being stored
* FIXING: Fixing "missing" comments when they were posted during the same second to the same paste
* FIXING: JS failing when password input disabled
* CHANGED: Switching positions of "New" and "Send" button, highlighting the latter to improve workflow
* CHANGED: Renamed config file to make updates easier
* CHANGED: Switching to JSON-based REST-API
* CHANGED: Database structure to store attachments, allowing larger attachments to be stored (depending on maximum BLOB size of database backend)
* CHANGED: Refactored data model, traffic limiting & request handling
* **0.21.1 (2015-09-21)**:
* FIXING: lost meta data when using DB model instead of flat files
* FIXING: mobile navbar getting triggered on load
* CHANGED: database table "paste" gets automatically extended with a "meta" column
* CHANGED: navbar of "bootstrap" template now spans full width of view port on large screens
* **0.21 (2015-09-19)**: * **0.21 (2015-09-19)**:
* ADDED: Translations for German, French and Polish, language selection menu (optional) * ADDED: Translations for German, French and Polish, language selection menu (optional)
* ADDED: File upload and image display support (optional) * ADDED: File upload and image display support (optional)

View File

@@ -8,6 +8,8 @@ MrKooky - HTML5 markup, CSS cleanup
Simon Rupf - MVC refactoring, configuration, i18n and unit tests Simon Rupf - MVC refactoring, configuration, i18n and unit tests
Hexalyse - Password protection Hexalyse - Password protection
Viktor Stanchev - File upload support Viktor Stanchev - File upload support
azlux - Tab character input support
Adam Fisher - Favicons
Translations: Translations:
Hexalyse - French Hexalyse - French

View File

@@ -16,8 +16,10 @@ and extract it in your web hosts folder were you want to install your ZeroBin in
### Configuration ### Configuration
In the file `cfg/conf.ini` you can configure ZeroBin. The config file is divided In the file `cfg/conf.ini` you can configure ZeroBin. A `cfg/conf.ini.sample`
into multiple sections, which are enclosed in square brackets. is provided containing all options on default values. You can copy it to
`cfg/conf.ini` and adapt it as needed. 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 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 limit of stored pastes and comments in bytes. The `[traffic]` section lets you
@@ -74,20 +76,31 @@ any experience running ZeroBin on other RDBMS, please let us know.
For reference or if you want to create the table schema for yourself: For reference or if you want to create the table schema for yourself:
CREATE TABLE prefix_paste ( CREATE TABLE prefix_paste (
dataid CHAR(16), dataid CHAR(16) NOT NULL,
data TEXT, data BLOB,
postdate INT, postdate INT,
expiredate INT, expiredate INT,
opendiscussion INT, opendiscussion INT,
burnafterreading INT burnafterreading INT,
meta TEXT,
attachment MEDIUMBLOB,
attachmentname BLOB,
PRIMARY KEY (dataid)
); );
CREATE TABLE prefix_comment ( CREATE TABLE prefix_comment (
dataid CHAR(16), dataid CHAR(16),
pasteid CHAR(16), pasteid CHAR(16),
parentid CHAR(16), parentid CHAR(16),
data TEXT, data BLOB,
nickname VARCHAR(255), nickname BLOB,
vizhash TEXT, vizhash BLOB,
postdate INT postdate INT,
PRIMARY KEY (dataid)
); );
CREATE INDEX parent ON prefix_comment(pasteid);
CREATE TABLE prefix_config (
id CHAR(16) NOT NULL, value TEXT, PRIMARY KEY (id)
);
INSERT INTO prefix_config VALUES('VERSION', '0.22');

View File

@@ -1,4 +1,4 @@
# ZeroBin 0.21 # ZeroBin 0.22
ZeroBin is a minimalist, opensource online pastebin where the server has zero ZeroBin is a minimalist, opensource online pastebin where the server has zero
knowledge of pasted data. knowledge of pasted data.
@@ -29,6 +29,15 @@ without loosing any data.
- As a user you have to trust the server administrator, your internet provider - As a user you have to trust the server administrator, your internet provider
and any country the traffic passes not to inject any malicious javascript code. and any country the traffic passes not to inject any malicious javascript code.
Ideally, the ZeroBin installation used would provide HTTPS, secured by
[HSTS](https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security) and
[HKPH](https://en.wikipedia.org/wiki/HTTP_Public_Key_Pinning) using a
certificate either validated by a trusted third party (check the certificate
when first using a new ZeroBin instance) or self-signed by the server operator,
validated using a
[DNSSEC](https://en.wikipedia.org/wiki/Domain_Name_System_Security_Extensions) protected
[DANE](https://en.wikipedia.org/wiki/DNS-based_Authentication_of_Named_Entities)
record.
- The "key" used to encrypt the paste is part of the URL. If you publicly post - The "key" used to encrypt the paste is part of the URL. If you publicly post
the URL of a paste that is not password-protected, everybody can read it. the URL of a paste that is not password-protected, everybody can read it.

1
cfg/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/conf.ini

View File

@@ -5,7 +5,7 @@
; @link http://sebsauvage.net/wiki/doku.php?id=php:zerobin ; @link http://sebsauvage.net/wiki/doku.php?id=php:zerobin
; @copyright 2012 Sébastien SAUVAGE (sebsauvage.net) ; @copyright 2012 Sébastien SAUVAGE (sebsauvage.net)
; @license http://www.opensource.org/licenses/zlib-license.php The zlib/libpng License ; @license http://www.opensource.org/licenses/zlib-license.php The zlib/libpng License
; @version 0.21 ; @version 0.22
[main] [main]
; enable or disable the discussion feature, defaults to true ; enable or disable the discussion feature, defaults to true
@@ -48,11 +48,19 @@ base64version = "2.1.9"
; a session cookie to store the choice until the browser is closed. ; a session cookie to store the choice until the browser is closed.
languageselection = false languageselection = false
; set the language your installs defaults to, defaults to English
; if this is set and language selection is disabled, this will be the only language
; languagedefault = "en"
[expire] [expire]
; expire value that is selected per default ; expire value that is selected per default
; make sure the value exists in [expire_options] ; make sure the value exists in [expire_options]
default = "1week" default = "1week"
; optionally the "clone" button can be disabled on expiring pastes
; note that this only hides the button, copy & paste is still possible
; clone = false
[expire_options] [expire_options]
; Set each one of these to the number of seconds in the expiration period, ; Set each one of these to the number of seconds in the expiration period,
; or 0 if it should never expire ; or 0 if it should never expire

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
/* ZeroBin 0.21 - http://sebsauvage.net/wiki/doku.php?id=php:zerobin */ /* ZeroBin 0.22 - http://sebsauvage.net/wiki/doku.php?id=php:zerobin */
body { body {
@@ -10,7 +10,7 @@ body.navbar-spacing {
} }
.navbar-nav { .navbar-nav {
margin: 0; margin: 0 8px;
} }
.dropdown-menu > li > label, .dropdown-menu > li > div { .dropdown-menu > li > label, .dropdown-menu > li > div {

View File

@@ -1,4 +1,4 @@
/* ZeroBin 0.21 - http://sebsauvage.net/wiki/doku.php?id=php:zerobin */ /* ZeroBin 0.22 - http://sebsauvage.net/wiki/doku.php?id=php:zerobin */
/* CSS Reset from YUI 3.4.1 (build 4118) - Copyright 2011 Yahoo! Inc. All rights reserved. /* CSS Reset from YUI 3.4.1 (build 4118) - Copyright 2011 Yahoo! Inc. All rights reserved.
@@ -394,6 +394,10 @@ img.vizhash {
font-size: 1.2em; font-size: 1.2em;
} }
#prettyprint.prettyprinted {
overflow: auto;
}
#cleartext { #cleartext {
padding: 10px; padding: 10px;
} }

BIN
favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 819 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 373 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 666 B

View File

@@ -7,7 +7,7 @@
* @link http://sebsauvage.net/wiki/doku.php?id=php:zerobin * @link http://sebsauvage.net/wiki/doku.php?id=php:zerobin
* @copyright 2012 Sébastien SAUVAGE (sebsauvage.net) * @copyright 2012 Sébastien SAUVAGE (sebsauvage.net)
* @license http://www.opensource.org/licenses/zlib-license.php The zlib/libpng License * @license http://www.opensource.org/licenses/zlib-license.php The zlib/libpng License
* @version 0.21 * @version 0.21.1
*/ */
// change this, if your php files and data is outside of your webservers document root // change this, if your php files and data is outside of your webservers document root

16
js/comment.jsonld Normal file
View File

@@ -0,0 +1,16 @@
{
"@context": {
"so": "http://schema.org/",
"status": "so:Integer",
"id": "so:name",
"parentid": "so:name",
"url: {
"@id": "so:url",
"@type": "@id"
},
"data": "so:Text",
"meta": {
"@id": "?jsonld=commentmeta"
}
}
}

8
js/commentmeta.jsonld Normal file
View File

@@ -0,0 +1,8 @@
{
"@context": {
"so": "http://schema.org/",
"postdate": "so:Integer",
"nickname": "so:Text",
"vizhash": "so:Text"
}
}

24
js/paste.jsonld Normal file
View File

@@ -0,0 +1,24 @@
{
"@context": {
"so": "http://schema.org/",
"status": {"@id": "so:Integer"},
"id": {"@id": "so:name"},
"deletetoken": {"@id": "so:Text"},
"url": {
"@type": "@id",
"@id": "so:url"
},
"data": {"@id": "so:Text"},
"attachment": {"@id": "so:Text"},
"attachmentname": {"@id": "so:Text"},
"meta": {
"@id": "?jsonld=pastemeta"
},
"comments": {
"@id": "?jsonld=comment",
"@container": "@list"
},
"comment_count": {"@id": "so:Integer"},
"comment_offset": {"@id": "so:Integer"}
}
}

11
js/pastemeta.jsonld Normal file
View File

@@ -0,0 +1,11 @@
{
"@context": {
"so": "http://schema.org/",
"formatter": {"@id": "so:Text"},
"postdate": {"@id": "so:Integer"},
"opendiscussion": {"@id": "so:True"},
"burnafterreading": {"@id": "so:True"},
"expire_date": {"@id": "so:Integer"},
"remaining_time": {"@id": "so:Integer"}
}
}

View File

@@ -6,7 +6,7 @@
* @link http://sebsauvage.net/wiki/doku.php?id=php:zerobin * @link http://sebsauvage.net/wiki/doku.php?id=php:zerobin
* @copyright 2012 Sébastien SAUVAGE (sebsauvage.net) * @copyright 2012 Sébastien SAUVAGE (sebsauvage.net)
* @license http://www.opensource.org/licenses/zlib-license.php The zlib/libpng License * @license http://www.opensource.org/licenses/zlib-license.php The zlib/libpng License
* @version 0.21 * @version 0.22
*/ */
'use strict'; 'use strict';
@@ -443,7 +443,7 @@ $(function() {
*/ */
cipher: function(key, password, message) cipher: function(key, password, message)
{ {
password = password.trim(); password = (password || '').trim();
if (password.length == 0) if (password.length == 0)
{ {
return sjcl.encrypt(key, this.compress(message)); return sjcl.encrypt(key, this.compress(message));
@@ -482,6 +482,11 @@ $(function() {
}; };
var zerobin = { var zerobin = {
/**
* headers to send in AJAX requests
*/
headers: {'X-Requested-With': 'JSONHttpRequest'},
/** /**
* Get the current script location (without search or hash part of the URL). * Get the current script location (without search or hash part of the URL).
* eg. http://server.com/zero/?aaaa#bbbb --> http://server.com/zero/ * eg. http://server.com/zero/?aaaa#bbbb --> http://server.com/zero/
@@ -578,37 +583,42 @@ $(function() {
helper.urls2links(this.clearText); helper.urls2links(this.clearText);
helper.urls2links(this.prettyPrint); helper.urls2links(this.prettyPrint);
this.clearText.addClass('hidden'); this.clearText.addClass('hidden');
if (format == 'plaintext')
{
this.prettyPrint.css('white-space', 'pre-wrap');
this.prettyPrint.css('word-break', 'normal');
this.prettyPrint.removeClass('prettyprint');
}
this.prettyMessage.removeClass('hidden'); this.prettyMessage.removeClass('hidden');
} }
if (format == 'plaintext') this.prettyPrint.removeClass('prettyprint');
}, },
/** /**
* Show decrypted text in the display area, including discussion (if open) * Show decrypted text in the display area, including discussion (if open)
* *
* @param string key : decryption key * @param string key : decryption key
* @param array comments : Array of messages to display (items = array with keys ('data','meta') * @param object paste : paste object including comments to display (items = array with keys ('data','meta')
*/ */
displayMessages: function(key, comments) displayMessages: function(key, paste)
{ {
// Try to decrypt the paste. // Try to decrypt the paste.
var password = this.passwordInput.val(); var password = this.passwordInput.val();
if (!this.prettyPrint.hasClass('prettyprinted')) { if (!this.prettyPrint.hasClass('prettyprinted')) {
try try
{ {
if (comments[0].attachment) if (paste.attachment)
{ {
var attachment = filter.decipher(key, password, comments[0].attachment); var attachment = filter.decipher(key, password, paste.attachment);
if (attachment.length == 0) if (attachment.length == 0)
{ {
if (password.length == 0) password = this.requestPassword(); if (password.length == 0) password = this.requestPassword();
attachment = filter.decipher(key, password, comments[0].attachment); attachment = filter.decipher(key, password, paste.attachment);
} }
if (attachment.length == 0) throw 'failed to decipher attachment'; if (attachment.length == 0) throw 'failed to decipher attachment';
if (comments[0].attachmentname) if (paste.attachmentname)
{ {
var attachmentname = filter.decipher(key, password, comments[0].attachmentname); var attachmentname = filter.decipher(key, password, paste.attachmentname);
if (attachmentname.length > 0) this.attachmentLink.attr('download', attachmentname); if (attachmentname.length > 0) this.attachmentLink.attr('download', attachmentname);
} }
this.attachmentLink.attr('href', attachment); this.attachmentLink.attr('href', attachment);
@@ -626,20 +636,20 @@ $(function() {
this.image.removeClass('hidden'); this.image.removeClass('hidden');
} }
} }
var cleartext = filter.decipher(key, password, comments[0].data); var cleartext = filter.decipher(key, password, paste.data);
if (cleartext.length == 0 && password.length == 0 && !comments[0].attachment) if (cleartext.length == 0 && password.length == 0 && !paste.attachment)
{ {
password = this.requestPassword(); password = this.requestPassword();
cleartext = filter.decipher(key, password, comments[0].data); cleartext = filter.decipher(key, password, paste.data);
} }
if (cleartext.length == 0 && !comments[0].attachment) throw 'failed to decipher message'; if (cleartext.length == 0 && !paste.attachment) throw 'failed to decipher message';
this.passwordInput.val(password); this.passwordInput.val(password);
if (cleartext.length > 0) if (cleartext.length > 0)
{ {
helper.setElementText(this.clearText, cleartext); helper.setElementText(this.clearText, cleartext);
helper.setElementText(this.prettyPrint, cleartext); helper.setElementText(this.prettyPrint, cleartext);
this.formatPaste(comments[0].meta.formatter); this.formatPaste(paste.meta.formatter);
} }
} }
catch(err) catch(err)
@@ -653,9 +663,9 @@ $(function() {
} }
// Display paste expiration / for your eyes only. // Display paste expiration / for your eyes only.
if (comments[0].meta.expire_date) if (paste.meta.expire_date)
{ {
var expiration = helper.secondsToHuman(comments[0].meta.remaining_time), var expiration = helper.secondsToHuman(paste.meta.remaining_time),
expirationLabel = [ expirationLabel = [
'This document will expire in %d ' + expiration[1] + '.', 'This document will expire in %d ' + expiration[1] + '.',
'This document will expire in %d ' + expiration[1] + 's.' 'This document will expire in %d ' + expiration[1] + 's.'
@@ -664,9 +674,16 @@ $(function() {
this.remainingTime.removeClass('foryoureyesonly') this.remainingTime.removeClass('foryoureyesonly')
.removeClass('hidden'); .removeClass('hidden');
} }
if (comments[0].meta.burnafterreading) if (paste.meta.burnafterreading)
{ {
$.get(this.scriptLocation() + '?pasteid=' + this.pasteID() + '&deletetoken=burnafterreading', 'json') // unfortunately many web servers don't support DELETE (and PUT) out of the box
$.ajax({
type: 'POST',
url: this.scriptLocation() + '?' + this.pasteID(),
data: {deletetoken: 'burnafterreading'},
dataType: 'json',
headers: this.headers
})
.fail(function() { .fail(function() {
zerobin.showError(i18n._('Could not delete the paste, it was not stored in burn after reading mode.')); zerobin.showError(i18n._('Could not delete the paste, it was not stored in burn after reading mode.'));
}); });
@@ -680,15 +697,15 @@ $(function() {
} }
// If the discussion is opened on this paste, display it. // If the discussion is opened on this paste, display it.
if (comments[0].meta.opendiscussion) if (paste.meta.opendiscussion)
{ {
this.comments.html(''); this.comments.html('');
// iterate over comments // iterate over comments
for (var i = 1; i < comments.length; i++) for (var i = 0; i < paste.comments.length; ++i)
{ {
var place = this.comments; var place = this.comments;
var comment=comments[i]; var comment = paste.comments[i];
var cleartext = '[' + i18n._('Could not decrypt comment; Wrong key?') + ']'; var cleartext = '[' + i18n._('Could not decrypt comment; Wrong key?') + ']';
try try
{ {
@@ -697,18 +714,18 @@ $(function() {
catch(err) catch(err)
{} {}
// If parent comment exists, display below (CSS will automatically shift it right.) // If parent comment exists, display below (CSS will automatically shift it right.)
var cname = '#comment_' + comment.meta.parentid; var cname = '#comment_' + comment.parentid;
// If the element exists in page // If the element exists in page
if ($(cname).length) if ($(cname).length)
{ {
place = $(cname); place = $(cname);
} }
var divComment = $('<article><div class="comment" id="comment_' + comment.meta.commentid+'">' var divComment = $('<article><div class="comment" id="comment_' + comment.id + '">'
+ '<div class="commentmeta"><span class="nickname"></span><span class="commentdate"></span></div><div class="commentdata"></div>' + '<div class="commentmeta"><span class="nickname"></span><span class="commentdate"></span></div><div class="commentdata"></div>'
+ '<button class="btn btn-default btn-sm">' + i18n._('Reply') + '</button>' + '<button class="btn btn-default btn-sm">' + i18n._('Reply') + '</button>'
+ '</div></article>'); + '</div></article>');
divComment.find('button').click({commentid: comment.meta.commentid}, $.proxy(this.openReply, this)); divComment.find('button').click({commentid: comment.id}, $.proxy(this.openReply, this));
helper.setElementText(divComment.find('div.commentdata'), cleartext); helper.setElementText(divComment.find('div.commentdata'), cleartext);
// Convert URLs to clickable links in comment. // Convert URLs to clickable links in comment.
helper.urls2links(divComment.find('div.commentdata')); helper.urls2links(divComment.find('div.commentdata'));
@@ -725,7 +742,7 @@ $(function() {
} }
divComment.find('span.commentdate') divComment.find('span.commentdate')
.text(' (' + (new Date(comment.meta.postdate * 1000).toLocaleString()) + ')') .text(' (' + (new Date(comment.meta.postdate * 1000).toLocaleString()) + ')')
.attr('title', 'CommentID: ' + comment.meta.commentid); .attr('title', 'CommentID: ' + comment.id);
// If an avatar is available, display it. // If an avatar is available, display it.
if (comment.meta.vizhash) if (comment.meta.vizhash)
@@ -805,39 +822,52 @@ $(function() {
nickname: ciphernickname nickname: ciphernickname
}; };
$.post(this.scriptLocation(), data_to_send, function(data) $.ajax({
{ type: 'POST',
if (data.status == 0) url: this.scriptLocation(),
data: data_to_send,
dataType: 'json',
headers: this.headers,
success: function(data)
{ {
zerobin.showStatus(i18n._('Comment posted.'), false); if (data.status == 0)
$.get(zerobin.scriptLocation() + '?' + zerobin.pasteID() + '&json', function(data)
{ {
if (data.status == 0) zerobin.showStatus(i18n._('Comment posted.'), false);
{ $.ajax({
zerobin.displayMessages(zerobin.pageKey(), data.messages); type: 'GET',
} url: zerobin.scriptLocation() + '?' + zerobin.pasteID(),
else if (data.status == 1) dataType: 'json',
{ headers: zerobin.headers,
zerobin.showError(i18n._('Could not refresh display: %s', data.message)); success: function(data)
} {
else if (data.status == 0)
{ {
zerobin.showError(i18n._('Could not refresh display: %s', i18n._('unknown status'))); zerobin.displayMessages(zerobin.pageKey(), data);
} }
}, 'json') else if (data.status == 1)
.fail(function() { {
zerobin.showError(i18n._('Could not refresh display: %s', i18n._('server error or not responding'))); zerobin.showError(i18n._('Could not refresh display: %s', data.message));
}); }
else
{
zerobin.showError(i18n._('Could not refresh display: %s', i18n._('unknown status')));
}
}
})
.fail(function() {
zerobin.showError(i18n._('Could not refresh display: %s', i18n._('server error or not responding')));
});
}
else if (data.status == 1)
{
zerobin.showError(i18n._('Could not post comment: %s', data.message));
}
else
{
zerobin.showError(i18n._('Could not post comment: %s', i18n._('unknown status')));
}
} }
else if (data.status == 1) })
{
zerobin.showError(i18n._('Could not post comment: %s', data.message));
}
else
{
zerobin.showError(i18n._('Could not post comment: %s', i18n._('unknown status')));
}
}, 'json')
.fail(function() { .fail(function() {
zerobin.showError(i18n._('Could not post comment: %s', i18n._('server error or not responding'))); zerobin.showError(i18n._('Could not post comment: %s', i18n._('server error or not responding')));
}); });
@@ -932,36 +962,44 @@ $(function() {
data_to_send.attachmentname = cipherdata_attachment_name; data_to_send.attachmentname = cipherdata_attachment_name;
} }
} }
$.post(this.scriptLocation(), data_to_send, function(data) $.ajax({
type: 'POST',
url: this.scriptLocation(),
data: data_to_send,
dataType: 'json',
headers: this.headers,
success: function(data)
{
if (data.status == 0) {
zerobin.stateExistingPaste();
var url = zerobin.scriptLocation() + '?' + data.id + '#' + randomkey;
var deleteUrl = zerobin.scriptLocation() + '?pasteid=' + data.id + '&deletetoken=' + data.deletetoken;
zerobin.showStatus('', false);
zerobin.errorMessage.addClass('hidden');
$('#pastelink').html(i18n._('Your paste is <a id="pasteurl" href="%s">%s</a> <span id="copyhint">(Hit [Ctrl]+[c] to copy)</span>', url, url));
$('#deletelink').html('<a href="' + deleteUrl + '">' + i18n._('Delete data') + '</a>');
zerobin.pasteResult.removeClass('hidden');
// We pre-select the link so that the user only has to [Ctrl]+[c] the link.
helper.selectText('pasteurl');
zerobin.showStatus('', false);
helper.setElementText(zerobin.clearText, zerobin.message.val());
helper.setElementText(zerobin.prettyPrint, zerobin.message.val());
zerobin.formatPaste(data_to_send.formatter);
}
else if (data.status==1)
{
zerobin.showError(i18n._('Could not create paste: %s', data.message));
}
else
{
zerobin.showError(i18n._('Could not create paste: %s', i18n._('unknown status')));
}
}
})
.fail(function()
{ {
if (data.status == 0) {
zerobin.stateExistingPaste();
var url = zerobin.scriptLocation() + '?' + data.id + '#' + randomkey;
var deleteUrl = zerobin.scriptLocation() + '?pasteid=' + data.id + '&deletetoken=' + data.deletetoken;
zerobin.showStatus('', false);
zerobin.errorMessage.addClass('hidden');
$('#pastelink').html(i18n._('Your paste is <a id="pasteurl" href="%s">%s</a> <span id="copyhint">(Hit [Ctrl]+[c] to copy)</span>', url, url));
$('#deletelink').html('<a href="' + deleteUrl + '">' + i18n._('Delete data') + '</a>');
zerobin.pasteResult.removeClass('hidden');
// We pre-select the link so that the user only has to [Ctrl]+[c] the link.
helper.selectText('pasteurl');
zerobin.showStatus('', false);
helper.setElementText(zerobin.clearText, zerobin.message.val());
helper.setElementText(zerobin.prettyPrint, zerobin.message.val());
zerobin.formatPaste(data_to_send.formatter);
}
else if (data.status==1)
{
zerobin.showError(i18n._('Could not create paste: %s', data.message));
}
else
{
zerobin.showError(i18n._('Could not create paste: %s', i18n._('unknown status')));
}
}, 'json')
.fail(function() {
zerobin.showError(i18n._('Could not create paste: %s', i18n._('server error or not responding'))); zerobin.showError(i18n._('Could not create paste: %s', i18n._('server error or not responding')));
}); });
}, },
@@ -1040,7 +1078,7 @@ $(function() {
}, },
/** /**
* Reload the page * Reload the page.
* *
* @param Event event * @param Event event
*/ */
@@ -1051,7 +1089,7 @@ $(function() {
}, },
/** /**
* Return raw text * Return raw text.
* *
* @param Event event * @param Event event
*/ */
@@ -1087,6 +1125,30 @@ $(function() {
$('.navbar-toggle').click(); $('.navbar-toggle').click();
}, },
/**
* Support input of tab character.
*
* @param Event event
*/
supportTabs: function(event)
{
var keyCode = event.keyCode || event.which;
// tab was pressed
if (keyCode === 9)
{
// prevent the textarea to lose focus
event.preventDefault();
// get caret position & selection
var val = this.value,
start = this.selectionStart,
end = this.selectionEnd;
// set textarea value to: text before caret + tab + text after caret
this.value = val.substring(0, start) + '\t' + val.substring(end);
// put caret at right position again
this.selectionStart = this.selectionEnd = start + 1;
}
},
/** /**
* Create a new paste. * Create a new paste.
*/ */
@@ -1095,7 +1157,6 @@ $(function() {
this.stateNewPaste(); this.stateNewPaste();
this.showStatus('', false); this.showStatus('', false);
this.message.text(''); this.message.text('');
$('.navbar-toggle').click();
}, },
/** /**
@@ -1171,6 +1232,7 @@ $(function() {
this.rawTextButton.click($.proxy(this.rawText, this)); this.rawTextButton.click($.proxy(this.rawText, this));
this.fileRemoveButton.click($.proxy(this.removeAttachment, this)); this.fileRemoveButton.click($.proxy(this.removeAttachment, this));
$('.reloadlink').click($.proxy(this.reloadPage, this)); $('.reloadlink').click($.proxy(this.reloadPage, this));
this.message.keydown(this.supportTabs);
}, },
/** /**
@@ -1236,12 +1298,12 @@ $(function() {
} }
// List of messages to display. // List of messages to display.
var messages = $.parseJSON(this.cipherData.text()); var data = $.parseJSON(this.cipherData.text());
// Show proper elements on screen. // Show proper elements on screen.
this.stateExistingPaste(); this.stateExistingPaste();
this.displayMessages(this.pageKey(), messages); this.displayMessages(this.pageKey(), data);
} }
// Display error message from php code. // Display error message from php code.
else if (this.errorMessage.text().length > 1) else if (this.errorMessage.text().length > 1)

View File

@@ -7,7 +7,7 @@
* @link http://sebsauvage.net/wiki/doku.php?id=php:zerobin * @link http://sebsauvage.net/wiki/doku.php?id=php:zerobin
* @copyright 2012 Sébastien SAUVAGE (sebsauvage.net) * @copyright 2012 Sébastien SAUVAGE (sebsauvage.net)
* @license http://www.opensource.org/licenses/zlib-license.php The zlib/libpng License * @license http://www.opensource.org/licenses/zlib-license.php The zlib/libpng License
* @version 0.21 * @version 0.22
*/ */
spl_autoload_register('auto::loader'); spl_autoload_register('auto::loader');

235
lib/configuration.php Normal file
View File

@@ -0,0 +1,235 @@
<?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.22
*/
/**
* configuration
*
* parses configuration file, ensures default values present
*/
class configuration
{
/**
* parsed configuration
*
* @var array
*/
private $_configuration;
/**
* default configuration
*
* @var array
*/
private $_defaults = array(
'main' => array(
'discussion' => true,
'opendiscussion' => false,
'password' => true,
'fileupload' => false,
'burnafterreadingselected' => false,
'defaultformatter' => 'plaintext',
'syntaxhighlightingtheme' => null,
'sizelimit' => 2097152,
'template' => 'bootstrap',
'notice' => '',
'base64version' => '2.1.9',
'languageselection' => false,
'languagedefault' => '',
),
'expire' => array(
'default' => '1week',
'clone' => true,
),
'expire_options' => array(
'5min' => 300,
'10min' => 600,
'1hour' => 3600,
'1day' => 86400,
'1week' => 604800,
'1month' => 2592000,
'1year' => 31536000,
'never' => 0,
),
'formatter_options' => array(
'plaintext' => 'Plain Text',
'syntaxhighlighting' => 'Source Code',
'markdown' => 'Markdown',
),
'traffic' => array(
'limit' => 10,
'header' => null,
'dir' => 'data',
),
'model' => array(
'class' => 'zerobin_data',
),
'model_options' => array(
'dir' => 'data',
),
);
/**
* parse configuration file and ensure default configuration values are present
*
* @throws Exception
*/
public function __construct()
{
$config = array();
$configFile = PATH . 'cfg' . DIRECTORY_SEPARATOR . 'conf.ini';
if (is_readable($configFile))
{
$config = parse_ini_file($configFile, true);
foreach (array('main', 'model') as $section) {
if (!array_key_exists($section, $config)) {
throw new Exception(i18n::_('ZeroBin requires configuration section [%s] to be present in configuration file.', $section), 2);
}
}
}
$opts = '_options';
foreach ($this->_defaults as $section => $values)
{
// fill missing sections with default values
if (!array_key_exists($section, $config) || count($config[$section]) == 0)
{
$this->_configuration[$section] = $values;
if (array_key_exists('dir', $this->_configuration[$section]))
{
$this->_configuration[$section]['dir'] = PATH . $this->_configuration[$section]['dir'];
}
continue;
}
// provide different defaults for database model
elseif ($section == 'model_options' && $this->_configuration['model']['class'] == 'zerobin_db')
{
$values = array(
'dsn' => 'sqlite:' . PATH . 'data/db.sq3',
'tbl' => null,
'usr' => null,
'pwd' => null,
'opt' => array(PDO::ATTR_PERSISTENT => true),
);
}
// "*_options" sections don't require all defaults to be set
if (
$section !== 'model_options' &&
($from = strlen($section) - strlen($opts)) >= 0 &&
strpos($section, $opts, $from) !== false
)
{
if (is_int(current($values)))
{
$config[$section] = array_map('intval', $config[$section]);
}
$this->_configuration[$section] = $config[$section];
}
// check for missing keys and set defaults if necessary
else
{
foreach ($values as $key => $val)
{
if ($key == 'dir')
{
$val = PATH . $val;
}
$result = $val;
if (array_key_exists($key, $config[$section]))
{
if ($val === null)
{
$result = $config[$section][$key];
}
elseif (is_bool($val))
{
$val = strtolower($config[$section][$key]);
if (in_array($val, array('true', 'yes', 'on')))
{
$result = true;
}
elseif (in_array($val, array('false', 'no', 'off')))
{
$result = false;
}
else
{
$result = (bool) $config[$section][$key];
}
}
elseif (is_int($val))
{
$result = (int) $config[$section][$key];
}
elseif (is_string($val) && !empty($config[$section][$key]))
{
$result = (string) $config[$section][$key];
}
}
$this->_configuration[$section][$key] = $result;
}
}
}
// ensure a valid expire default key is set
if (!array_key_exists($this->_configuration['expire']['default'], $this->_configuration['expire_options']))
{
$this->_configuration['expire']['default'] = key($this->_configuration['expire_options']);
}
}
/**
* get configuration as array
*
* return array
*/
public function get()
{
return $this->_configuration;
}
/**
* get a key from the configuration, typically the main section or all keys
*
* @param string $key
* @param string $section defaults to main
* @throws Exception
* return mixed
*/
public function getKey($key, $section = 'main')
{
$options = $this->getSection($section);
if (!array_key_exists($key, $options))
{
throw new Exception(i18n::_('Invalid data.') . " $section / $key", 4);
}
return $this->_configuration[$section][$key];
}
/**
* get a key from the configuration, typically the main section or all keys
*
* @param string $key if empty, return all configuration options
* @param string $section defaults to main
* @throws Exception
* return mixed
*/
public function getSection($section)
{
if (!array_key_exists($section, $this->_configuration))
{
throw new Exception(i18n::_('ZeroBin requires configuration section [%s] to be present in configuration file.', $section), 3);
}
return $this->_configuration[$section];
}
}

View File

@@ -7,7 +7,7 @@
* @link http://sebsauvage.net/wiki/doku.php?id=php:zerobin * @link http://sebsauvage.net/wiki/doku.php?id=php:zerobin
* @copyright 2012 Sébastien SAUVAGE (sebsauvage.net) * @copyright 2012 Sébastien SAUVAGE (sebsauvage.net)
* @license http://www.opensource.org/licenses/zlib-license.php The zlib/libpng License * @license http://www.opensource.org/licenses/zlib-license.php The zlib/libpng License
* @version 0.21 * @version 0.22
*/ */
/** /**
@@ -80,19 +80,6 @@ class filter
return number_format($size, ($i ? 2 : 0), '.', ' ') . ' ' . i18n::_($iec[$i]); return number_format($size, ($i ? 2 : 0), '.', ' ') . ' ' . i18n::_($iec[$i]);
} }
/**
* validate paste ID
*
* @access public
* @static
* @param string $dataid
* @return bool
*/
public static function is_valid_paste_id($dataid)
{
return (bool) preg_match('#\A[a-f\d]{16}\z#', $dataid);
}
/** /**
* fixed time string comparison operation to prevent timing attacks * fixed time string comparison operation to prevent timing attacks
* https://crackstation.net/hashing-security.htm?=rd#slowequals * https://crackstation.net/hashing-security.htm?=rd#slowequals

View File

@@ -7,7 +7,7 @@
* @link http://sebsauvage.net/wiki/doku.php?id=php:zerobin * @link http://sebsauvage.net/wiki/doku.php?id=php:zerobin
* @copyright 2012 Sébastien SAUVAGE (sebsauvage.net) * @copyright 2012 Sébastien SAUVAGE (sebsauvage.net)
* @license http://www.opensource.org/licenses/zlib-license.php The zlib/libpng License * @license http://www.opensource.org/licenses/zlib-license.php The zlib/libpng License
* @version 0.21 * @version 0.22
*/ */
/** /**
@@ -26,6 +26,15 @@ class i18n
*/ */
protected static $_language = 'en'; protected static $_language = 'en';
/**
* language fallback
*
* @access protected
* @static
* @var string
*/
protected static $_languageFallback = 'en';
/** /**
* language labels * language labels
* *
@@ -248,6 +257,20 @@ class i18n
return array_intersect_key(self::$_languageLabels, array_flip($languages)); return array_intersect_key(self::$_languageLabels, array_flip($languages));
} }
/**
* set the default language
*
* @access public
* @static
* @param string $lang
* @return void
*/
public static function setLanguageFallback($lang)
{
if (in_array($lang, self::getAvailableLanguages()))
self::$_languageFallback = $lang;
}
/** /**
* get language file path * get language file path
* *
@@ -339,7 +362,7 @@ class i18n
} }
if (count($matches) === 0) if (count($matches) === 0)
{ {
return 'en'; return self::$_languageFallback;
} }
krsort($matches); krsort($matches);
$topmatches = current($matches); $topmatches = current($matches);

71
lib/model.php Normal file
View File

@@ -0,0 +1,71 @@
<?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.22
*/
/**
* model
*
* Factory of ZeroBin instance models.
*/
class model
{
/**
* Configuration.
*
* @var configuration
*/
private $_conf;
/**
* Data storage.
*
* @var zerobin_abstract
*/
private $_store = null;
/**
* Factory constructor.
*
* @param configuration $conf
*/
public function __construct(configuration $conf)
{
$this->_conf = $conf;
}
/**
* Get a paste, optionally a specific instance.
*
* @param string $pasteId
* @return model_paste
*/
public function getPaste($pasteId = null)
{
$paste = new model_paste($this->_conf, $this->_getStore());
if ($pasteId !== null) $paste->setId($pasteId);
return $paste;
}
/**
* Gets, and creates if neccessary, a store object
*/
private function _getStore()
{
if ($this->_store === null)
{
$this->_store = forward_static_call(
array($this->_conf->getKey('class', 'model'), 'getInstance'),
$this->_conf->getSection('model_options')
);
}
return $this->_store;
}
}

157
lib/model/abstract.php Normal file
View File

@@ -0,0 +1,157 @@
<?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.22
*/
/**
* model_abstract
*
* Abstract model for ZeroBin objects.
*/
abstract class model_abstract
{
/**
* Instance ID.
*
* @access protected
* @var string
*/
protected $_id = '';
/**
* Instance data.
*
* @access protected
* @var stdClass
*/
protected $_data;
/**
* Configuration.
*
* @access protected
* @var configuration
*/
protected $_conf;
/**
* Data storage.
*
* @access protected
* @var zerobin_abstract
*/
protected $_store;
/**
* Instance constructor.
*
* @access public
* @param configuration $configuration
* @param zerobin_abstract $storage
* @return void
*/
public function __construct(configuration $configuration, zerobin_abstract $storage)
{
$this->_conf = $configuration;
$this->_store = $storage;
$this->_data = new stdClass;
$this->_data->meta = new stdClass;
}
/**
* Get ID.
*
* @access public
* @return string
*/
public function getId()
{
return $this->_id;
}
/**
* Set ID.
*
* @access public
* @param string $id
* @throws Exception
* @return void
*/
public function setId($id)
{
if (!self::isValidId($id)) throw new Exception('Invalid paste ID.', 60);
$this->_id = $id;
}
/**
* Set data and recalculate ID.
*
* @access public
* @param string $data
* @throws Exception
* @return void
*/
public function setData($data)
{
if (!sjcl::isValid($data)) throw new Exception('Invalid data.', 61);
$this->_data->data = $data;
// We just want a small hash to avoid collisions:
// Half-MD5 (64 bits) will do the trick
$this->setId(substr(hash('md5', $data), 0, 16));
}
/**
* Get instance data.
*
* @access public
* @return stdObject
*/
abstract public function get();
/**
* Store the instance's data.
*
* @access public
* @throws Exception
* @return void
*/
abstract public function store();
/**
* Delete the current instance.
*
* @access public
* @throws Exception
* @return void
*/
abstract public function delete();
/**
* Test if current instance exists in store.
*
* @access public
* @return bool
*/
abstract public function exists();
/**
* Validate ID.
*
* @access public
* @static
* @param string $id
* @return bool
*/
public static function isValidId($id)
{
return (bool) preg_match('#\A[a-f\d]{16}\z#', (string) $id);
}
}

189
lib/model/comment.php Normal file
View File

@@ -0,0 +1,189 @@
<?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.22
*/
/**
* model_comment
*
* Model of a ZeroBin comment.
*/
class model_comment extends model_abstract
{
/**
* Instance's parent.
*
* @access private
* @var model_paste
*/
private $_paste;
/**
* Get comment data.
*
* @access public
* @throws Exception
* @return stdObject
*/
public function get()
{
// @todo add support to read specific comment
$comments = $this->_store->readComments($this->getPaste()->getId());
foreach ($comments as $comment) {
if (
$comment->parentid == $this->getParentId() &&
$comment->id == $this->getId()
) {
$this->_data = $comment;
break;
}
}
return $this->_data;
}
/**
* Store the comment's data.
*
* @access public
* @throws Exception
* @return void
*/
public function store()
{
// Make sure paste exists.
$pasteid = $this->getPaste()->getId();
if (!$this->getPaste()->exists())
throw new Exception('Invalid data.', 67);
// Make sure the discussion is opened in this paste and in configuration.
if (!$this->getPaste()->isOpendiscussion() || !$this->_conf->getKey('discussion'))
throw new Exception('Invalid data.', 68);
// Check for improbable collision.
if ($this->exists())
throw new Exception('You are unlucky. Try again.', 69);
$this->_data->meta->postdate = time();
// store comment
if (
$this->_store->createComment(
$pasteid,
$this->getParentId(),
$this->getId(),
json_decode(json_encode($this->_data), true)
) === false
) throw new Exception('Error saving comment. Sorry.', 70);
}
/**
* Delete the comment.
*
* @access public
* @throws Exception
* @return void
*/
public function delete()
{
throw new Exception('To delete a comment, delete its parent paste', 64);
}
/**
* Test if comment exists in store.
*
* @access public
* @return bool
*/
public function exists()
{
return $this->_store->existsComment(
$this->getPaste()->getId(),
$this->getParentId(),
$this->getId()
);
}
/**
* Set paste.
*
* @access public
* @param model_paste $paste
* @throws Exception
* @return void
*/
public function setPaste(model_paste $paste)
{
$this->_paste = $paste;
$this->_data->meta->pasteid = $paste->getId();
}
/**
* Get paste.
*
* @access public
* @return model_paste
*/
public function getPaste()
{
return $this->_paste;
}
/**
* Set parent ID.
*
* @access public
* @param string $id
* @throws Exception
* @return void
*/
public function setParentId($id)
{
if (!self::isValidId($id)) throw new Exception('Invalid paste ID.', 65);
$this->_data->meta->parentid = $id;
}
/**
* Get parent ID.
*
* @access public
* @return string
*/
public function getParentId()
{
if (!property_exists($this->_data->meta, 'parentid')) $this->_data->meta->parentid = '';
return $this->_data->meta->parentid;
}
/**
* Set nickname.
*
* @access public
* @param string $nickname
* @throws Exception
* @return void
*/
public function setNickname($nickname)
{
if (!sjcl::isValid($nickname)) throw new Exception('Invalid data.', 66);
$this->_data->meta->nickname = $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.)
$vh = new vizhash16x16();
$pngdata = $vh->generate(trafficlimiter::getIp());
if ($pngdata != '')
{
$this->_data->meta->vizhash = 'data:image/png;base64,' . base64_encode($pngdata);
}
// Once the avatar is generated, we do not keep the IP address, nor its hash.
}
}

303
lib/model/paste.php Normal file
View File

@@ -0,0 +1,303 @@
<?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.22
*/
/**
* model_paste
*
* Model of a ZeroBin paste.
*/
class model_paste extends model_abstract
{
/**
* Get paste data.
*
* @access public
* @throws Exception
* @return stdObject
*/
public function get()
{
$this->_data = $this->_store->read($this->getId());
// See if paste has expired and delete it if neccessary.
if (property_exists($this->_data->meta, 'expire_date'))
{
if ($this->_data->meta->expire_date < time())
{
$this->delete();
throw new Exception(zerobin::GENERIC_ERROR, 63);
}
// We kindly provide the remaining time before expiration (in seconds)
$this->_data->meta->remaining_time = $this->_data->meta->expire_date - time();
}
// set formatter for for the view.
if (!property_exists($this->_data->meta, 'formatter'))
{
// support < 0.21 syntax highlighting
if (property_exists($this->_data->meta, 'syntaxcoloring') && $this->_data->meta->syntaxcoloring === true)
{
$this->_data->meta->formatter = 'syntaxhighlighting';
}
else
{
$this->_data->meta->formatter = $this->_conf->getKey('defaultformatter');
}
}
$this->_data->comments = array_values($this->getComments());
$this->_data->comment_count = count($this->_data->comments);
$this->_data->comment_offset = 0;
$this->_data->{'@context'} = 'js/paste.jsonld';
return $this->_data;
}
/**
* Store the paste's data.
*
* @access public
* @throws Exception
* @return void
*/
public function store()
{
// Check for improbable collision.
if ($this->exists())
throw new Exception('You are unlucky. Try again.', 75);
$this->_data->meta->postdate = time();
// store paste
if (
$this->_store->create(
$this->getId(),
json_decode(json_encode($this->_data), true)
) === false
) throw new Exception('Error saving paste. Sorry.', 76);
}
/**
* Delete the paste.
*
* @access public
* @throws Exception
* @return void
*/
public function delete()
{
$this->_store->delete($this->getId());
}
/**
* Test if paste exists in store.
*
* @access public
* @return bool
*/
public function exists()
{
return $this->_store->exists($this->getId());
}
/**
* Get a comment, optionally a specific instance.
*
* @access public
* @param string $parentId
* @param string $commentId
* @throws Exception
* @return model_comment
*/
public function getComment($parentId, $commentId = null)
{
if (!$this->exists())
{
throw new Exception('Invalid data.', 62);
}
$comment = new model_comment($this->_conf, $this->_store);
$comment->setPaste($this);
$comment->setParentId($parentId);
if ($commentId !== null) $comment->setId($commentId);
return $comment;
}
/**
* Get all comments, if any.
*
* @access public
* @return array
*/
public function getComments()
{
return $this->_store->readComments($this->getId());
}
/**
* Generate the "delete" token.
*
* The token is the hmac of the pastes ID signed with the server salt.
* The paste can be deleted by calling:
* http://example.com/zerobin/?pasteid=<pasteid>&deletetoken=<deletetoken>
*
* @access public
* @return string
*/
public function getDeleteToken()
{
return hash_hmac('sha1', $this->getId(), serversalt::get());
}
/**
* Set paste's attachment.
*
* @access public
* @param string $attachment
* @throws Exception
* @return void
*/
public function setAttachment($attachment)
{
if (!$this->_conf->getKey('fileupload') || !sjcl::isValid($attachment))
throw new Exception('Invalid attachment.', 71);
$this->_data->meta->attachment = $attachment;
}
/**
* Set paste's attachment name.
*
* @access public
* @param string $attachmentname
* @throws Exception
* @return void
*/
public function setAttachmentName($attachmentname)
{
if (!$this->_conf->getKey('fileupload') || !sjcl::isValid($attachmentname))
throw new Exception('Invalid attachment.', 72);
$this->_data->meta->attachmentname = $attachmentname;
}
/**
* Set paste expiration.
*
* @access public
* @param string $expiration
* @return void
*/
public function setExpiration($expiration)
{
$expire_options = $this->_conf->getSection('expire_options');
if (array_key_exists($expiration, $expire_options))
{
$expire = $expire_options[$expiration];
}
else
{
// using getKey() to ensure a default value is present
$expire = $this->_conf->getKey($this->_conf->getKey('default', 'expire'), 'expire_options');
}
if ($expire > 0) $this->_data->meta->expire_date = time() + $expire;
}
/**
* Set paste's burn-after-reading type.
*
* @access public
* @param string $burnafterreading
* @throws Exception
* @return void
*/
public function setBurnafterreading($burnafterreading = '1')
{
if ($burnafterreading === '0')
{
$this->_data->meta->burnafterreading = false;
}
else
{
if ($burnafterreading !== '1')
throw new Exception('Invalid data.', 73);
$this->_data->meta->burnafterreading = true;
$this->_data->meta->opendiscussion = false;
}
}
/**
* Set paste's discussion state.
*
* @access public
* @param string $opendiscussion
* @throws Exception
* @return void
*/
public function setOpendiscussion($opendiscussion = '1')
{
if (
!$this->_conf->getKey('discussion') ||
$this->isBurnafterreading() ||
$opendiscussion === '0'
)
{
$this->_data->meta->opendiscussion = false;
}
else
{
if ($opendiscussion !== '1')
throw new Exception('Invalid data.', 74);
$this->_data->meta->opendiscussion = true;
}
}
/**
* Set paste's format.
*
* @access public
* @param string $format
* @throws Exception
* @return void
*/
public function setFormatter($format)
{
if (!array_key_exists($format, $this->_conf->getSection('formatter_options')))
{
$format = $this->_conf->getKey('defaultformatter');
}
$this->_data->meta->formatter = $format;
}
/**
* Check if paste is of burn-after-reading type.
*
* @access public
* @throws Exception
* @return boolean
*/
public function isBurnafterreading()
{
if (!property_exists($this->_data, 'data')) $this->get();
return property_exists($this->_data->meta, 'burnafterreading') &&
$this->_data->meta->burnafterreading === true;
}
/**
* Check if paste has discussions enabled.
*
* @access public
* @throws Exception
* @return boolean
*/
public function isOpendiscussion()
{
if (!property_exists($this->_data, 'data')) $this->get();
return property_exists($this->_data->meta, 'opendiscussion') &&
$this->_data->meta->opendiscussion === true;
}
}

View File

@@ -7,7 +7,7 @@
* @link http://sebsauvage.net/wiki/doku.php?id=php:zerobin * @link http://sebsauvage.net/wiki/doku.php?id=php:zerobin
* @copyright 2012 Sébastien SAUVAGE (sebsauvage.net) * @copyright 2012 Sébastien SAUVAGE (sebsauvage.net)
* @license http://www.opensource.org/licenses/zlib-license.php The zlib/libpng License * @license http://www.opensource.org/licenses/zlib-license.php The zlib/libpng License
* @version 0.21 * @version 0.22
*/ */
/** /**

171
lib/request.php Normal file
View File

@@ -0,0 +1,171 @@
<?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.22
*/
/**
* request
*
* parses request parameters and provides helper functions for routing
*/
class request
{
/**
* Input stream to use for PUT parameter parsing.
*
* @access private
* @var string
*/
private static $_inputStream = 'php://input';
/**
* Operation to perform.
*
* @access private
* @var string
*/
private $_operation = 'view';
/**
* Request parameters.
*
* @access private
* @var array
*/
private $_params = array();
/**
* If we are in a JSON API context.
*
* @access private
* @var bool
*/
private $_isJsonApi = false;
/**
* Constructor.
*
* @access public
* @return void
*/
public function __construct()
{
// in case stupid admin has left magic_quotes enabled in php.ini (for PHP < 5.4)
if (function_exists('get_magic_quotes_gpc') && 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);
}
// decide if we are in JSON API or HTML context
if (
(array_key_exists('HTTP_X_REQUESTED_WITH', $_SERVER) &&
$_SERVER['HTTP_X_REQUESTED_WITH'] == 'JSONHttpRequest') ||
(array_key_exists('HTTP_ACCEPT', $_SERVER) &&
strpos($_SERVER['HTTP_ACCEPT'], 'application/json') !== false)
)
{
$this->_isJsonApi = true;
}
// parse parameters, depending on request type
switch (array_key_exists('REQUEST_METHOD', $_SERVER) ? $_SERVER['REQUEST_METHOD'] : 'GET')
{
case 'DELETE':
case 'PUT':
parse_str(file_get_contents(self::$_inputStream), $this->_params);
break;
case 'POST':
$this->_params = $_POST;
break;
default:
$this->_params = $_GET;
}
if (
!array_key_exists('pasteid', $this->_params) &&
!array_key_exists('jsonld', $this->_params) &&
array_key_exists('QUERY_STRING', $_SERVER) &&
!empty($_SERVER['QUERY_STRING'])
)
{
$this->_params['pasteid'] = $_SERVER['QUERY_STRING'];
}
// prepare operation, depending on current parameters
if (
(array_key_exists('data', $this->_params) && !empty($this->_params['data'])) ||
(array_key_exists('attachment', $this->_params) && !empty($this->_params['attachment']))
)
{
$this->_operation = 'create';
}
elseif (array_key_exists('pasteid', $this->_params) && !empty($this->_params['pasteid']))
{
if (array_key_exists('deletetoken', $this->_params) && !empty($this->_params['deletetoken']))
{
$this->_operation = 'delete';
}
else
{
$this->_operation = 'read';
}
}
elseif (array_key_exists('jsonld', $this->_params) && !empty($this->_params['jsonld']))
{
$this->_operation = 'jsonld';
}
}
/**
* Get current operation.
*
* @access public
* @return string
*/
public function getOperation()
{
return $this->_operation;
}
/**
* Get a request parameter.
*
* @access public
* @param string $param
* @param string $default
* @return string
*/
public function getParam($param, $default = '')
{
return array_key_exists($param, $this->_params) ? $this->_params[$param] : $default;
}
/**
* If we are in a JSON API context.
*
* @access public
* @return bool
*/
public function isJsonApiCall()
{
return $this->_isJsonApi;
}
/**
* Override the default input stream source, used for unit testing.
*
* @param string $input
*/
public static function setInputStream($input)
{
self::$_inputStream = $input;
}
}

View File

@@ -7,7 +7,7 @@
* @link http://sebsauvage.net/wiki/doku.php?id=php:zerobin * @link http://sebsauvage.net/wiki/doku.php?id=php:zerobin
* @copyright 2012 Sébastien SAUVAGE (sebsauvage.net) * @copyright 2012 Sébastien SAUVAGE (sebsauvage.net)
* @license http://www.opensource.org/licenses/zlib-license.php The zlib/libpng License * @license http://www.opensource.org/licenses/zlib-license.php The zlib/libpng License
* @version 0.21 * @version 0.22
*/ */
/** /**

View File

@@ -7,7 +7,7 @@
* @link http://sebsauvage.net/wiki/doku.php?id=php:zerobin * @link http://sebsauvage.net/wiki/doku.php?id=php:zerobin
* @copyright 2012 Sébastien SAUVAGE (sebsauvage.net) * @copyright 2012 Sébastien SAUVAGE (sebsauvage.net)
* @license http://www.opensource.org/licenses/zlib-license.php The zlib/libpng License * @license http://www.opensource.org/licenses/zlib-license.php The zlib/libpng License
* @version 0.21 * @version 0.22
*/ */
/** /**

View File

@@ -7,7 +7,7 @@
* @link http://sebsauvage.net/wiki/doku.php?id=php:zerobin * @link http://sebsauvage.net/wiki/doku.php?id=php:zerobin
* @copyright 2012 Sébastien SAUVAGE (sebsauvage.net) * @copyright 2012 Sébastien SAUVAGE (sebsauvage.net)
* @license http://www.opensource.org/licenses/zlib-license.php The zlib/libpng License * @license http://www.opensource.org/licenses/zlib-license.php The zlib/libpng License
* @version 0.21 * @version 0.22
*/ */
/** /**
@@ -26,6 +26,15 @@ class trafficlimiter extends persistence
*/ */
private static $_limit = 10; private static $_limit = 10;
/**
* key to fetch IP address
*
* @access private
* @static
* @var string
*/
private static $_ipKey = 'REMOTE_ADDR';
/** /**
* set the time limit in seconds * set the time limit in seconds
* *
@@ -39,6 +48,40 @@ class trafficlimiter extends persistence
self::$_limit = $limit; self::$_limit = $limit;
} }
/**
* set configuration options of the traffic limiter
*
* @access public
* @static
* @param configuration $conf
* @return void
*/
public static function setConfiguration(configuration $conf)
{
self::setLimit($conf->getKey('limit', 'traffic'));
self::setPath($conf->getKey('dir', 'traffic'));
if (($option = $conf->getKey('header', 'traffic')) !== null)
{
$httpHeader = 'HTTP_' . $option;
if (array_key_exists($httpHeader, $_SERVER) && !empty($_SERVER[$httpHeader]))
{
self::$_ipKey = $httpHeader;
}
}
}
/**
* get the current visitors IP address
*
* @access public
* @static
* @return string
*/
public static function getIp()
{
return $_SERVER[self::$_ipKey];
}
/** /**
* traffic limiter * traffic limiter
* *
@@ -46,14 +89,15 @@ class trafficlimiter extends persistence
* *
* @access public * @access public
* @static * @static
* @param string $ip
* @throws Exception * @throws Exception
* @return bool * @return bool
*/ */
public static function canPass($ip) public static function canPass()
{ {
// disable limits if set to less then 1 $ip = self::getIp();
if (self::$_limit < 1) return true;
// disable limits if set to less then 1
if (self::$_limit < 1) return true;
$file = 'traffic_limiter.php'; $file = 'traffic_limiter.php';
if (!self::_exists($file)) if (!self::_exists($file))

View File

@@ -8,7 +8,7 @@
* @link http://sebsauvage.net/wiki/doku.php?id=php:vizhash_gd * @link http://sebsauvage.net/wiki/doku.php?id=php:vizhash_gd
* @copyright 2012 Sébastien SAUVAGE (sebsauvage.net) * @copyright 2012 Sébastien SAUVAGE (sebsauvage.net)
* @license http://www.opensource.org/licenses/zlib-license.php The zlib/libpng License * @license http://www.opensource.org/licenses/zlib-license.php The zlib/libpng License
* @version 0.0.4 beta ZeroBin 0.21 * @version 0.0.4 beta ZeroBin 0.22
*/ */
/** /**

View File

@@ -7,7 +7,7 @@
* @link http://sebsauvage.net/wiki/doku.php?id=php:zerobin * @link http://sebsauvage.net/wiki/doku.php?id=php:zerobin
* @copyright 2012 Sébastien SAUVAGE (sebsauvage.net) * @copyright 2012 Sébastien SAUVAGE (sebsauvage.net)
* @license http://www.opensource.org/licenses/zlib-license.php The zlib/libpng License * @license http://www.opensource.org/licenses/zlib-license.php The zlib/libpng License
* @version 0.21 * @version 0.22
*/ */
/** /**
@@ -22,7 +22,7 @@ class zerobin
* *
* @const string * @const string
*/ */
const VERSION = '0.21'; const VERSION = '0.22';
/** /**
* show the same error message if the paste expired or does not exist * show the same error message if the paste expired or does not exist
@@ -32,14 +32,12 @@ class zerobin
const GENERIC_ERROR = 'Paste does not exist, has expired or has been deleted.'; const GENERIC_ERROR = 'Paste does not exist, has expired or has been deleted.';
/** /**
* configuration array * configuration
* *
* @access private * @access private
* @var array * @var configuration
*/ */
private $_conf = array( private $_conf;
'model' => 'zerobin_data',
);
/** /**
* data * data
@@ -49,6 +47,14 @@ class zerobin
*/ */
private $_data = ''; private $_data = '';
/**
* does the paste expire
*
* @access private
* @var bool
*/
private $_doesExpire = false;
/** /**
* formatter * formatter
* *
@@ -82,13 +88,29 @@ class zerobin
private $_json = ''; private $_json = '';
/** /**
* data storage model * Factory of instance models
* *
* @access private * @access private
* @var zerobin_abstract * @var model
*/ */
private $_model; private $_model;
/**
* request
*
* @access private
* @var request
*/
private $_request;
/**
* URL base
*
* @access private
* @var string
*/
private $_urlbase;
/** /**
* constructor * constructor
* *
@@ -104,40 +126,35 @@ class zerobin
throw new Exception(i18n::_('ZeroBin requires php 5.2.6 or above to work. Sorry.'), 1); throw new Exception(i18n::_('ZeroBin requires php 5.2.6 or above to work. Sorry.'), 1);
} }
// 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 // load config from ini file
$this->_init(); $this->_init();
// create new paste or comment switch ($this->_request->getOperation())
if (
(array_key_exists('data', $_POST) && !empty($_POST['data'])) ||
(array_key_exists('attachment', $_POST) && !empty($_POST['attachment']))
)
{ {
$this->_create(); case 'create':
} $this->_create();
// delete an existing paste break;
elseif (!empty($_GET['deletetoken']) && !empty($_GET['pasteid'])) case 'delete':
{ $this->_delete(
$this->_delete($_GET['pasteid'], $_GET['deletetoken']); $this->_request->getParam('pasteid'),
} $this->_request->getParam('deletetoken')
// display an existing paste );
elseif (!empty($_SERVER['QUERY_STRING'])) break;
{ case 'read':
$this->_read($_SERVER['QUERY_STRING']); $this->_read($this->_request->getParam('pasteid'));
break;
case 'jsonld':
$this->_jsonld($this->_request->getParam('jsonld'));
return;
} }
// output JSON or HTML // output JSON or HTML
if (strlen($this->_json)) if ($this->_request->isJsonApiCall())
{ {
header('Content-type: application/json'); header('Content-type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE');
header('Access-Control-Allow-Headers: X-Requested-With, Content-Type');
echo $this->_json; echo $this->_json;
} }
else else
@@ -164,31 +181,20 @@ class zerobin
); );
} }
$this->_conf = parse_ini_file(PATH . 'cfg' . DIRECTORY_SEPARATOR . 'conf.ini', true); $this->_conf = new configuration;
foreach (array('main', 'model') as $section) { $this->_model = new model($this->_conf);
if (!array_key_exists($section, $this->_conf)) { $this->_request = new request;
throw new Exception(i18n::_('ZeroBin requires configuration section [%s] to be present in configuration file.', $section), 2); $this->_urlbase = array_key_exists('REQUEST_URI', $_SERVER) ? $_SERVER['REQUEST_URI'] : '/';
}
}
$this->_model = $this->_conf['model']['class'];
}
/** // set default language
* get the model, create one if needed $lang = $this->_conf->getKey('languagedefault');
* i18n::setLanguageFallback($lang);
* @access private // force default language, if language selection is disabled and a default is set
* @return zerobin_abstract if (!$this->_conf->getKey('languageselection') && strlen($lang) == 2)
*/ {
private function _model() $_COOKIE['lang'] = $lang;
{ setcookie('lang', $lang);
// 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;
} }
/** /**
@@ -203,6 +209,7 @@ class zerobin
* formatter (optional) = format to display the paste as (plaintext,syntaxhighlighting,markdown) (default:syntaxhighlighting) * formatter (optional) = format to display the paste as (plaintext,syntaxhighlighting,markdown) (default:syntaxhighlighting)
* burnafterreading (optional) = if this paste may only viewed once ? (0/1) (default:0) * burnafterreading (optional) = if this paste may only viewed once ? (0/1) (default:0)
* opendiscusssion (optional) = is the discussion allowed on this paste ? (0/1) (default:0) * opendiscusssion (optional) = is the discussion allowed on this paste ? (0/1) (default:0)
* attachmentname = json encoded SJCL encrypted text (containing keys: iv,v,iter,ks,ts,mode,adata,cipher,salt,ct)
* nickname (optional) = in discussion, encoded SJCL encrypted text nickname of author of comment (containing keys: iv,v,iter,ks,ts,mode,adata,cipher,salt,ct) * nickname (optional) = in discussion, encoded SJCL encrypted text nickname of author of comment (containing keys: iv,v,iter,ks,ts,mode,adata,cipher,salt,ct)
* parentid (optional) = in discussion, which comment this comment replies to. * parentid (optional) = in discussion, which comment this comment replies to.
* pasteid (optional) = in discussion, which paste this comment belongs to. * pasteid (optional) = in discussion, which paste this comment belongs to.
@@ -214,34 +221,21 @@ class zerobin
{ {
$error = false; $error = false;
$has_attachment = array_key_exists('attachment', $_POST); // Ensure last paste from visitors IP address was more than configured amount of seconds ago.
$has_attachmentname = $has_attachment && array_key_exists('attachmentname', $_POST) && !empty($_POST['attachmentname']); trafficlimiter::setConfiguration($this->_conf);
$data = array_key_exists('data', $_POST) ? $_POST['data'] : ''; if (!trafficlimiter::canPass()) return $this->_return_message(
$attachment = $has_attachment ? $_POST['attachment'] : ''; 1, i18n::_(
$attachmentname = $has_attachmentname ? $_POST['attachmentname'] : '';
// 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']);
$ipKey = 'REMOTE_ADDR';
if (array_key_exists('header', $this->_conf['traffic']))
{
$header = 'HTTP_' . $this->_conf['traffic']['header'];
if (array_key_exists($header, $_SERVER) && !empty($_SERVER[$header]))
{
$ipKey = $header;
}
}
if (!trafficlimiter::canPass($_SERVER[$ipKey])) return $this->_return_message(
1,
i18n::_(
'Please wait %d seconds between each post.', 'Please wait %d seconds between each post.',
$this->_conf['traffic']['limit'] $this->_conf->getKey('limit', 'traffic')
) )
); );
// Make sure content is not too big. $data = $this->_request->getParam('data');
$sizelimit = (int) $this->_getMainConfig('sizelimit', 2097152); $attachment = $this->_request->getParam('attachment');
$attachmentname = $this->_request->getParam('attachmentname');
// Ensure content is not too big.
$sizelimit = $this->_conf->getKey('sizelimit');
if ( if (
strlen($data) + strlen($attachment) + strlen($attachmentname) > $sizelimit strlen($data) + strlen($attachment) + strlen($attachmentname) > $sizelimit
) return $this->_return_message( ) return $this->_return_message(
@@ -252,181 +246,62 @@ class zerobin
) )
); );
// Make sure format is correct.
if (!sjcl::isValid($data)) return $this->_return_message(1, 'Invalid data.');
// Make sure attachments are enabled and format is correct.
if($has_attachment)
{
if (
!$this->_getMainConfig('fileupload', false) ||
!sjcl::isValid($attachment) ||
!($has_attachmentname && sjcl::isValid($attachmentname))
) return $this->_return_message(1, 'Invalid attachment.');
}
// Read additional meta-information.
$meta = array();
// Read expiration date
if (array_key_exists('expire', $_POST) && !empty($_POST['expire']))
{
$selected_expire = (string) $_POST['expire'];
if (array_key_exists($selected_expire, $this->_conf['expire_options']))
{
$expire = $this->_conf['expire_options'][$selected_expire];
}
else
{
$expire = $this->_conf['expire_options'][$this->_conf['expire']['default']];
}
if ($expire > 0) $meta['expire_date'] = time() + $expire;
}
// Destroy the paste when it is read.
if (array_key_exists('burnafterreading', $_POST) && !empty($_POST['burnafterreading']))
{
$burnafterreading = $_POST['burnafterreading'];
if ($burnafterreading !== '0')
{
if ($burnafterreading !== '1') $error = true;
$meta['burnafterreading'] = true;
}
}
// Read open discussion flag.
if (
$this->_getMainConfig('discussion', true) &&
array_key_exists('opendiscussion', $_POST) &&
!empty($_POST['opendiscussion'])
)
{
$opendiscussion = $_POST['opendiscussion'];
if ($opendiscussion !== '0')
{
if ($opendiscussion !== '1') $error = true;
$meta['opendiscussion'] = true;
}
}
// Read formatter flag.
if (array_key_exists('formatter', $_POST) && !empty($_POST['formatter']))
{
$formatter = $_POST['formatter'];
if (!array_key_exists($formatter, $this->_conf['formatter_options']))
{
$formatter = $this->_getMainConfig('defaultformatter', 'plaintext');
}
$meta['formatter'] = $formatter;
}
// 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) return $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. // The user posts a comment.
if ( $pasteid = $this->_request->getParam('pasteid');
!empty($_POST['parentid']) && $parentid = $this->_request->getParam('parentid');
!empty($_POST['pasteid']) if (!empty($pasteid) && !empty($parentid))
)
{ {
$pasteid = (string) $_POST['pasteid']; $paste = $this->_model->getPaste($pasteid);
$parentid = (string) $_POST['parentid']; if ($paste->exists()) {
if ( try {
!filter::is_valid_paste_id($pasteid) || $comment = $paste->getComment($parentid);
!filter::is_valid_paste_id($parentid)
) return $this->_return_message(1, 'Invalid data.');
// Comments do not expire (it's the paste that expires) $nickname = $this->_request->getParam('nickname');
unset($storage['expire_date']); if (!empty($nickname)) $comment->setNickname($nickname);
unset($storage['opendiscussion']);
// Make sure paste exists. $comment->setData($data);
if ( $comment->store();
!$this->_model()->exists($pasteid) } catch(Exception $e) {
) return $this->_return_message(1, 'Invalid data.'); return $this->_return_message(1, $e->getMessage());
}
// Make sure the discussion is opened in this paste. $this->_return_message(0, $comment->getId());
$paste = $this->_model()->read($pasteid); }
if ( else
!$paste->meta->opendiscussion {
) return $this->_return_message(1, 'Invalid data.'); $this->_return_message(1, 'Invalid data.');
}
// Check for improbable collision.
if (
$this->_model()->existsComment($pasteid, $parentid, $dataid)
) return $this->_return_message(1, 'You are unlucky. Try again.');
// New comment
if (
$this->_model()->createComment($pasteid, $parentid, $dataid, $storage) === false
) return $this->_return_message(1, 'Error saving comment. Sorry.');
// 0 = no error
return $this->_return_message(0, $dataid);
} }
// The user posts a standard paste. // The user posts a standard paste.
else else
{ {
// Check for improbable collision. $paste = $this->_model->getPaste();
if ( try {
$this->_model()->exists($dataid) $paste->setData($data);
) return $this->_return_message(1, 'You are unlucky. Try again.');
// Add attachment and its name, if one was sent if (!empty($attachment))
if ($has_attachment) $storage['attachment'] = $attachment; {
if ($has_attachmentname) $storage['attachmentname'] = $attachmentname; $paste->setAttachment($attachment);
if (!empty($attachmentname))
$paste->setAttachmentName($attachmentname);
}
// New paste $expire = $this->_request->getParam('expire');
if ( if (!empty($expire)) $paste->setExpiration($expire);
$this->_model()->create($dataid, $storage) === false
) return $this->_return_message(1, 'Error saving paste. Sorry.');
// Generate the "delete" token. $burnafterreading = $this->_request->getParam('burnafterreading');
// The token is the hmac of the pasteid signed with the server salt. if (!empty($burnafterreading)) $paste->setBurnafterreading($burnafterreading);
// The paste can be delete by calling http://example.com/zerobin/?pasteid=<pasteid>&deletetoken=<deletetoken>
$deletetoken = hash_hmac('sha1', $dataid, serversalt::get());
// 0 = no error $opendiscussion = $this->_request->getParam('opendiscussion');
return $this->_return_message(0, $dataid, array('deletetoken' => $deletetoken)); if (!empty($opendiscussion)) $paste->setOpendiscussion($opendiscussion);
$formatter = $this->_request->getParam('formatter');
if (!empty($formatter)) $paste->setFormatter($formatter);
$paste->store();
} catch (Exception $e) {
return $this->_return_message(1, $e->getMessage());
}
$this->_return_message(0, $paste->getId(), array('deletetoken' => $paste->getDeleteToken()));
} }
} }
@@ -440,63 +315,48 @@ class zerobin
*/ */
private function _delete($dataid, $deletetoken) private function _delete($dataid, $deletetoken)
{ {
// Is this a valid paste identifier? try {
if (!filter::is_valid_paste_id($dataid)) $paste = $this->_model->getPaste($dataid);
{ if ($paste->exists())
$this->_error = 'Invalid paste ID.';
return;
}
// Check that paste exists.
if (!$this->_model()->exists($dataid))
{
$this->_error = self::GENERIC_ERROR;
return;
}
// 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 = self::GENERIC_ERROR;
return;
}
if ($deletetoken == 'burnafterreading') {
if (
isset($paste->meta->burnafterreading) &&
$paste->meta->burnafterreading
)
{ {
// Delete the paste // accessing this property ensures that the paste would be
$this->_model()->delete($dataid); // deleted if it has already expired
$this->_return_message(0, $dataid); $burnafterreading = $paste->isBurnafterreading();
if ($deletetoken == 'burnafterreading')
{
if ($burnafterreading)
{
$paste->delete();
$this->_return_message(0, $dataid);
}
else
{
$this->_return_message(1, 'Paste is not of burn-after-reading type.');
}
}
else
{
// Make sure the token is valid.
serversalt::setPath($this->_conf->getKey('dir', 'traffic'));
if (filter::slow_equals($deletetoken, $paste->getDeleteToken()))
{
// Paste exists and deletion token is valid: Delete the paste.
$paste->delete();
$this->_status = 'Paste was properly deleted.';
}
else
{
$this->_error = 'Wrong deletion token. Paste was not deleted.';
}
}
} }
else else
{ {
$this->_return_message(1, 'Paste is not of burn-after-reading type.'); $this->_error = self::GENERIC_ERROR;
} }
return; } catch (Exception $e) {
$this->_error = $e->getMessage();
} }
// Make sure token is valid.
serversalt::setPath($this->_conf['traffic']['dir']);
if (!filter::slow_equals($deletetoken, hash_hmac('sha1', $dataid, serversalt::get())))
{
$this->_error = 'Wrong deletion token. Paste was not deleted.';
return;
}
// Paste exists and deletion token is valid: Delete the paste.
$this->_model()->delete($dataid);
$this->_status = 'Paste was properly deleted.';
} }
/** /**
@@ -508,80 +368,23 @@ class zerobin
*/ */
private function _read($dataid) private function _read($dataid)
{ {
$isJson = false; try {
if (($pos = strpos($dataid, '&json')) !== false) { $paste = $this->_model->getPaste($dataid);
$isJson = true; if ($paste->exists())
$dataid = substr($dataid, 0, $pos);
}
// Is this a valid paste identifier?
if (!filter::is_valid_paste_id($dataid))
{
$this->_error = 'Invalid paste ID.';
return;
}
// 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 $data = $paste->get();
$this->_model()->delete($dataid); $this->_doesExpire = property_exists($data, 'meta') && property_exists($data->meta, 'expire_date');
$this->_error = self::GENERIC_ERROR; $this->_data = json_encode($data);
} }
// If no error, return the paste.
else else
{ {
// We kindly provide the remaining time before expiration (in seconds) $this->_error = self::GENERIC_ERROR;
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)
);
}
// set formatter for for the view.
if (!property_exists($paste->meta, 'formatter'))
{
// support < 0.21 syntax highlighting
if (property_exists($paste->meta, 'syntaxcoloring') && $paste->meta->syntaxcoloring === true)
{
$paste->meta->formatter = 'syntaxhighlighting';
}
else
{
$paste->meta->formatter = $this->_getMainConfig('defaultformatter', 'plaintext');
}
}
$this->_data = json_encode($messages);
} }
} catch (Exception $e) {
$this->_error = $e->getMessage();
} }
else
{ if ($this->_request->isJsonApiCall())
$this->_error = self::GENERIC_ERROR;
}
if ($isJson)
{ {
if (strlen($this->_error)) if (strlen($this->_error))
{ {
@@ -589,7 +392,7 @@ class zerobin
} }
else else
{ {
$this->_return_message(0, $dataid, array('messages' => $messages)); $this->_return_message(0, $dataid, json_decode($this->_data, true));
} }
} }
} }
@@ -612,17 +415,17 @@ class zerobin
// label all the expiration options // label all the expiration options
$expire = array(); $expire = array();
foreach ($this->_conf['expire_options'] as $time => $seconds) foreach ($this->_conf->getSection('expire_options') as $time => $seconds)
{ {
$expire[$time] = ($seconds == 0) ? i18n::_(ucfirst($time)): filter::time_humanreadable($time); $expire[$time] = ($seconds == 0) ? i18n::_(ucfirst($time)): filter::time_humanreadable($time);
} }
// translate all the formatter options // translate all the formatter options
$formatters = array_map(array('i18n', 'translate'), $this->_conf['formatter_options']); $formatters = array_map(array('i18n', 'translate'), $this->_conf->getSection('formatter_options'));
// set language cookie if that functionality was enabled // set language cookie if that functionality was enabled
$languageselection = ''; $languageselection = '';
if ($this->_getMainConfig('languageselection', false)) if ($this->_conf->getKey('languageselection'))
{ {
$languageselection = i18n::getLanguage(); $languageselection = i18n::getLanguage();
setcookie('lang', $languageselection); setcookie('lang', $languageselection);
@@ -635,42 +438,61 @@ class zerobin
$page->assign('ERROR', i18n::_($this->_error)); $page->assign('ERROR', i18n::_($this->_error));
$page->assign('STATUS', i18n::_($this->_status)); $page->assign('STATUS', i18n::_($this->_status));
$page->assign('VERSION', self::VERSION); $page->assign('VERSION', self::VERSION);
$page->assign('DISCUSSION', $this->_getMainConfig('discussion', true)); $page->assign('DISCUSSION', $this->_conf->getKey('discussion'));
$page->assign('OPENDISCUSSION', $this->_getMainConfig('opendiscussion', true)); $page->assign('OPENDISCUSSION', $this->_conf->getKey('opendiscussion'));
$page->assign('MARKDOWN', array_key_exists('markdown', $formatters)); $page->assign('MARKDOWN', array_key_exists('markdown', $formatters));
$page->assign('SYNTAXHIGHLIGHTING', array_key_exists('syntaxhighlighting', $formatters)); $page->assign('SYNTAXHIGHLIGHTING', array_key_exists('syntaxhighlighting', $formatters));
$page->assign('SYNTAXHIGHLIGHTINGTHEME', $this->_getMainConfig('syntaxhighlightingtheme', '')); $page->assign('SYNTAXHIGHLIGHTINGTHEME', $this->_conf->getKey('syntaxhighlightingtheme'));
$page->assign('FORMATTER', $formatters); $page->assign('FORMATTER', $formatters);
$page->assign('FORMATTERDEFAULT', $this->_getMainConfig('defaultformatter', 'plaintext')); $page->assign('FORMATTERDEFAULT', $this->_conf->getKey('defaultformatter'));
$page->assign('NOTICE', i18n::_($this->_getMainConfig('notice', ''))); $page->assign('NOTICE', i18n::_($this->_conf->getKey('notice')));
$page->assign('BURNAFTERREADINGSELECTED', $this->_getMainConfig('burnafterreadingselected', false)); $page->assign('BURNAFTERREADINGSELECTED', $this->_conf->getKey('burnafterreadingselected'));
$page->assign('PASSWORD', $this->_getMainConfig('password', true)); $page->assign('PASSWORD', $this->_conf->getKey('password'));
$page->assign('FILEUPLOAD', $this->_getMainConfig('fileupload', false)); $page->assign('FILEUPLOAD', $this->_conf->getKey('fileupload'));
$page->assign('BASE64JSVERSION', $this->_getMainConfig('base64version', '2.1.9')); $page->assign('BASE64JSVERSION', $this->_conf->getKey('base64version'));
$page->assign('LANGUAGESELECTION', $languageselection); $page->assign('LANGUAGESELECTION', $languageselection);
$page->assign('LANGUAGES', i18n::getLanguageLabels(i18n::getAvailableLanguages())); $page->assign('LANGUAGES', i18n::getLanguageLabels(i18n::getAvailableLanguages()));
$page->assign('EXPIRE', $expire); $page->assign('EXPIRE', $expire);
$page->assign('EXPIREDEFAULT', $this->_conf['expire']['default']); $page->assign('EXPIREDEFAULT', $this->_conf->getKey('default', 'expire'));
$page->draw($this->_getMainConfig('template', 'page')); $page->assign('EXPIRECLONE', !$this->_doesExpire || ($this->_doesExpire && $this->_conf->getKey('clone', 'expire')));
$page->draw($this->_conf->getKey('template'));
} }
/** /**
* get configuration option from [main] section, optionally set a default * outputs requested JSON-LD context
* *
* @access private * @access private
* @param string $option * @param string $type
* @param mixed $default (optional) * @return void
* @return mixed
*/ */
private function _getMainConfig($option, $default = false) private function _jsonld($type)
{ {
return array_key_exists($option, $this->_conf['main']) ? if (
$this->_conf['main'][$option] : $type !== 'paste' && $type !== 'comment' &&
$default; $type !== 'pastemeta' && $type !== 'commentmeta'
)
{
$type = '';
}
$content = '{}';
$file = PUBLIC_PATH . DIRECTORY_SEPARATOR . 'js' . DIRECTORY_SEPARATOR . $type . '.jsonld';
if (is_readable($file))
{
$content = str_replace(
'?jsonld=',
$this->_urlbase . '?jsonld=',
file_get_contents($file)
);
}
header('Content-type: application/ld+json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET');
echo $content;
} }
/** /**
* return JSON encoded message and exit * prepares JSON encoded status message
* *
* @access private * @access private
* @param bool $status * @param bool $status
@@ -688,6 +510,7 @@ class zerobin
else else
{ {
$result['id'] = $message; $result['id'] = $message;
$result['url'] = $this->_urlbase . '?' . $message;
} }
$result += $other; $result += $other;
$this->_json = json_encode($result); $this->_json = json_encode($result);

View File

@@ -7,7 +7,7 @@
* @link http://sebsauvage.net/wiki/doku.php?id=php:zerobin * @link http://sebsauvage.net/wiki/doku.php?id=php:zerobin
* @copyright 2012 Sébastien SAUVAGE (sebsauvage.net) * @copyright 2012 Sébastien SAUVAGE (sebsauvage.net)
* @license http://www.opensource.org/licenses/zlib-license.php The zlib/libpng License * @license http://www.opensource.org/licenses/zlib-license.php The zlib/libpng License
* @version 0.21 * @version 0.22
*/ */
/** /**
@@ -17,12 +17,12 @@
*/ */
abstract class zerobin_abstract abstract class zerobin_abstract
{ {
/** /**
* singleton instance * singleton instance
* *
* @access protected * @access protected
* @static * @static
* @var zerobin * @var zerobin_abstract
*/ */
protected static $_instance = null; protected static $_instance = null;
@@ -87,7 +87,7 @@ abstract class zerobin_abstract
* *
* @access public * @access public
* @param string $dataid * @param string $dataid
* @return void * @return bool
*/ */
abstract public function exists($pasteid); abstract public function exists($pasteid);
@@ -122,4 +122,24 @@ abstract class zerobin_abstract
* @return void * @return void
*/ */
abstract public function existsComment($pasteid, $parentid, $commentid); abstract public function existsComment($pasteid, $parentid, $commentid);
/**
* Get next free slot for comment from postdate.
*
* @access public
* @param array $comments
* @param int|string $postdate
* @return void
*/
protected function getOpenSlot(&$comments, $postdate)
{
if (array_key_exists($postdate, $comments))
{
$parts = explode('.', $postdate, 2);
if (!array_key_exists(1, $parts)) $parts[1] = 0;
++$parts[1];
return $this->getOpenSlot($comments, implode('.', $parts));
}
return $postdate;
}
} }

View File

@@ -7,7 +7,7 @@
* @link http://sebsauvage.net/wiki/doku.php?id=php:zerobin * @link http://sebsauvage.net/wiki/doku.php?id=php:zerobin
* @copyright 2012 Sébastien SAUVAGE (sebsauvage.net) * @copyright 2012 Sébastien SAUVAGE (sebsauvage.net)
* @license http://www.opensource.org/licenses/zlib-license.php The zlib/libpng License * @license http://www.opensource.org/licenses/zlib-license.php The zlib/libpng License
* @version 0.21 * @version 0.22
*/ */
/** /**
@@ -75,9 +75,20 @@ class zerobin_data extends zerobin_abstract
public function read($pasteid) public function read($pasteid)
{ {
if(!$this->exists($pasteid)) return false; if(!$this->exists($pasteid)) return false;
return json_decode( $paste = json_decode(
file_get_contents(self::_dataid2path($pasteid) . $pasteid) file_get_contents(self::_dataid2path($pasteid) . $pasteid)
); );
if (property_exists($paste->meta, 'attachment'))
{
$paste->attachment = $paste->meta->attachment;
unset($paste->meta->attachment);
if (property_exists($paste->meta, 'attachmentname'))
{
$paste->attachmentname = $paste->meta->attachmentname;
unset($paste->meta->attachmentname);
}
}
return $paste;
} }
/** /**
@@ -90,7 +101,7 @@ class zerobin_data extends zerobin_abstract
public function delete($pasteid) public function delete($pasteid)
{ {
// Delete the paste itself. // Delete the paste itself.
unlink(self::_dataid2path($pasteid) . $pasteid); @unlink(self::_dataid2path($pasteid) . $pasteid);
// Delete discussion if it exists. // Delete discussion if it exists.
$discdir = self::_dataid2discussionpath($pasteid); $discdir = self::_dataid2discussionpath($pasteid);
@@ -100,7 +111,7 @@ class zerobin_data extends zerobin_abstract
$dir = dir($discdir); $dir = dir($discdir);
while (false !== ($filename = $dir->read())) while (false !== ($filename = $dir->read()))
{ {
if (is_file($discdir.$filename)) unlink($discdir.$filename); if (is_file($discdir.$filename)) @unlink($discdir.$filename);
} }
$dir->close(); $dir->close();
@@ -161,16 +172,17 @@ class zerobin_data extends zerobin_abstract
// - pasteid is the paste this reply belongs to. // - pasteid is the paste this reply belongs to.
// - commentid is the comment identifier itself. // - commentid is the comment identifier itself.
// - parentid is the comment this comment replies to (It can be pasteid) // - parentid is the comment this comment replies to (It can be pasteid)
if (is_file($discdir.$filename)) if (is_file($discdir . $filename))
{ {
$comment = json_decode(file_get_contents($discdir.$filename)); $comment = json_decode(file_get_contents($discdir . $filename));
$items = explode('.', $filename); $items = explode('.', $filename);
// Add some meta information not contained in file. // Add some meta information not contained in file.
$comment->meta->commentid=$items[1]; $comment->id = $items[1];
$comment->meta->parentid=$items[2]; $comment->parentid = $items[2];
// Store in array // Store in array
$comments[$comment->meta->postdate]=$comment; $key = $this->getOpenSlot($comments, (int) $comment->meta->postdate);
$comments[$key] = $comment;
} }
} }
$dir->close(); $dir->close();

View File

@@ -7,7 +7,7 @@
* @link http://sebsauvage.net/wiki/doku.php?id=php:zerobin * @link http://sebsauvage.net/wiki/doku.php?id=php:zerobin
* @copyright 2012 Sébastien SAUVAGE (sebsauvage.net) * @copyright 2012 Sébastien SAUVAGE (sebsauvage.net)
* @license http://www.opensource.org/licenses/zlib-license.php The zlib/libpng License * @license http://www.opensource.org/licenses/zlib-license.php The zlib/libpng License
* @version 0.21 * @version 0.22
*/ */
/** /**
@@ -80,96 +80,63 @@ class zerobin_db extends zerobin_abstract
array_key_exists('opt', $options) array_key_exists('opt', $options)
) )
{ {
// check if the database contains the required tables // set default options
$options['opt'][PDO::ATTR_ERRMODE] = PDO::ERRMODE_EXCEPTION;
$options['opt'][PDO::ATTR_EMULATE_PREPARES] = false;
$options['opt'][PDO::ATTR_PERSISTENT] = true;
$db_tables_exist = true;
// setup type and dabase connection
self::$_type = strtolower( self::$_type = strtolower(
substr($options['dsn'], 0, strpos($options['dsn'], ':')) substr($options['dsn'], 0, strpos($options['dsn'], ':'))
); );
switch(self::$_type) $tableQuery = self::_getTableQuery(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.',
5
);
}
self::$_db = new PDO( self::$_db = new PDO(
$options['dsn'], $options['dsn'],
$options['usr'], $options['usr'],
$options['pwd'], $options['pwd'],
$options['opt'] $options['opt']
); );
$statement = self::$_db->query($sql);
$tables = $statement->fetchAll(PDO::FETCH_COLUMN, 0);
// create paste table if needed // check if the database contains the required tables
if (!array_key_exists(self::$_prefix . 'paste', $tables)) $tables = self::$_db->query($tableQuery)->fetchAll(PDO::FETCH_COLUMN, 0);
// create paste table if necessary
if (!in_array(self::$_prefix . 'paste', $tables))
{ {
self::$_db->exec( self::_createPasteTable();
'CREATE TABLE ' . self::$_prefix . 'paste ( ' . $db_tables_exist = false;
'dataid CHAR(16), ' .
'data TEXT, ' .
'postdate INT, ' .
'expiredate INT, ' .
'opendiscussion INT, ' .
'burnafterreading INT );'
);
} }
// create comment table if needed // create comment table if necessary
if (!array_key_exists(self::$_prefix . 'comment', $tables)) if (!in_array(self::$_prefix . 'comment', $tables))
{ {
self::$_db->exec( self::_createCommentTable();
'CREATE TABLE ' . self::$_prefix . 'comment ( ' . $db_tables_exist = false;
'dataid CHAR(16), ' . }
'pasteid CHAR(16), ' .
'parentid CHAR(16), ' . // create config table if necessary
'data TEXT, ' . $db_version = zerobin::VERSION;
'nickname VARCHAR(255), ' . if (!in_array(self::$_prefix . 'config', $tables))
'vizhash TEXT, ' . {
'postdate INT );' self::_createConfigTable();
); // if we only needed to create the config table, the DB is older then 0.22
if ($db_tables_exist) $db_version = '0.21';
}
else
{
$db_version = self::_getConfig('VERSION');
}
// update database structure if necessary
if (version_compare($db_version, zerobin::VERSION, '<'))
{
self::_upgradeDatabase($db_version);
} }
} }
} }
return parent::$_instance; return self::$_instance;
} }
/** /**
@@ -183,7 +150,7 @@ class zerobin_db extends zerobin_abstract
public function create($pasteid, $paste) public function create($pasteid, $paste)
{ {
if ( if (
array_key_exists($pasteid, self::$_cache) array_key_exists($pasteid, self::$_cache)
) { ) {
if(false !== self::$_cache[$pasteid]) { if(false !== self::$_cache[$pasteid]) {
return false; return false;
@@ -192,21 +159,48 @@ class zerobin_db extends zerobin_abstract
} }
} }
if ( $opendiscussion = $burnafterreading = false;
!array_key_exists('opendiscussion', $paste['meta']) $attachment = $attachmentname = '';
) $paste['meta']['opendiscussion'] = false; $meta = $paste['meta'];
if ( unset($meta['postdate']);
!array_key_exists('burnafterreading', $paste['meta']) $expire_date = 0;
) $paste['meta']['burnafterreading'] = false; if (array_key_exists('expire_date', $paste['meta']))
{
$expire_date = (int) $paste['meta']['expire_date'];
unset($meta['expire_date']);
}
if (array_key_exists('opendiscussion', $paste['meta']))
{
$opendiscussion = (bool) $paste['meta']['opendiscussion'];
unset($meta['opendiscussion']);
}
if (array_key_exists('burnafterreading', $paste['meta']))
{
$burnafterreading = (bool) $paste['meta']['burnafterreading'];
unset($meta['burnafterreading']);
}
if (array_key_exists('attachment', $paste['meta']))
{
$attachment = $paste['meta']['attachment'];
unset($meta['attachment']);
}
if (array_key_exists('attachmentname', $paste['meta']))
{
$attachmentname = $paste['meta']['attachmentname'];
unset($meta['attachmentname']);
}
return self::_exec( return self::_exec(
'INSERT INTO ' . self::$_prefix . 'paste VALUES(?,?,?,?,?,?)', 'INSERT INTO ' . self::$_prefix . 'paste VALUES(?,?,?,?,?,?,?,?,?)',
array( array(
$pasteid, $pasteid,
$paste['data'], $paste['data'],
$paste['meta']['postdate'], $paste['meta']['postdate'],
$paste['meta']['expire_date'], $expire_date,
(int) $paste['meta']['opendiscussion'], (int) $opendiscussion,
(int) $paste['meta']['burnafterreading'], (int) $burnafterreading,
json_encode($meta),
$attachment,
$attachmentname,
) )
); );
} }
@@ -233,9 +227,36 @@ class zerobin_db extends zerobin_abstract
// create object // create object
self::$_cache[$pasteid] = new stdClass; self::$_cache[$pasteid] = new stdClass;
self::$_cache[$pasteid]->data = $paste['data']; self::$_cache[$pasteid]->data = $paste['data'];
self::$_cache[$pasteid]->meta = new stdClass;
$meta = json_decode($paste['meta']);
if (!is_object($meta)) $meta = new stdClass;
// support older attachments
if (property_exists($meta, 'attachment'))
{
self::$_cache[$pasteid]->attachment = $meta->attachment;
unset($meta->attachment);
if (property_exists($meta, 'attachmentname'))
{
self::$_cache[$pasteid]->attachmentname = $meta->attachmentname;
unset($meta->attachmentname);
}
}
// support current attachments
elseif (array_key_exists('attachment', $paste) && strlen($paste['attachment']))
{
self::$_cache[$pasteid]->attachment = $paste['attachment'];
if (array_key_exists('attachmentname', $paste) && strlen($paste['attachmentname']))
{
self::$_cache[$pasteid]->attachmentname = $paste['attachmentname'];
}
}
self::$_cache[$pasteid]->meta = $meta;
self::$_cache[$pasteid]->meta->postdate = (int) $paste['postdate']; self::$_cache[$pasteid]->meta->postdate = (int) $paste['postdate'];
self::$_cache[$pasteid]->meta->expire_date = (int) $paste['expiredate']; $expire_date = (int) $paste['expiredate'];
if (
$expire_date > 0
) self::$_cache[$pasteid]->meta->expire_date = $expire_date;
if ( if (
$paste['opendiscussion'] $paste['opendiscussion']
) self::$_cache[$pasteid]->meta->opendiscussion = true; ) self::$_cache[$pasteid]->meta->opendiscussion = true;
@@ -325,24 +346,23 @@ class zerobin_db extends zerobin_abstract
array($pasteid) array($pasteid)
); );
// create object
$commentTemplate = new stdClass;
$commentTemplate->meta = new stdClass;
// create comment list // create comment list
$comments = array(); $comments = array();
if (count($rows)) if (count($rows))
{ {
foreach ($rows as $row) foreach ($rows as $row)
{ {
$i = (int) $row['postdate']; $i = $this->getOpenSlot($comments, (int) $row['postdate']);
$comments[$i] = clone $commentTemplate; $comments[$i] = new stdClass;
$comments[$i]->id = $row['dataid'];
$comments[$i]->parentid = $row['parentid'];
$comments[$i]->data = $row['data']; $comments[$i]->data = $row['data'];
$comments[$i]->meta->nickname = $row['nickname']; $comments[$i]->meta = new stdClass;
$comments[$i]->meta->vizhash = $row['vizhash']; $comments[$i]->meta->postdate = (int) $row['postdate'];
$comments[$i]->meta->postdate = $i; if (array_key_exists('nickname', $row))
$comments[$i]->meta->commentid = $row['dataid']; $comments[$i]->meta->nickname = $row['nickname'];
$comments[$i]->meta->parentid = $row['parentid']; if (array_key_exists('vizhash', $row))
$comments[$i]->meta->vizhash = $row['vizhash'];
} }
ksort($comments); ksort($comments);
} }
@@ -406,4 +426,227 @@ class zerobin_db extends zerobin_abstract
$statement->closeCursor(); $statement->closeCursor();
return $result; return $result;
} }
/**
* get table list query, depending on the database type
*
* @access private
* @static
* @param string $type
* @throws Exception
* @return string
*/
private static function _getTableQuery($type)
{
switch($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 $type is currently not supported.", 5
);
}
return $sql;
}
/**
* get a value by key from the config table
*
* @access private
* @static
* @param string $key
* @throws PDOException
* @return string
*/
private static function _getConfig($key)
{
$row = self::_select(
'SELECT value FROM ' . self::$_prefix . 'config WHERE id = ?',
array($key), true
);
return $row['value'];
}
/**
* get the primary key clauses, depending on the database driver
*
* @access private
* @static
* @param string $key
* @return array
*/
private static function _getPrimaryKeyClauses($key = 'dataid')
{
$main_key = $after_key = '';
if (self::$_type === 'mysql')
{
$after_key = ", PRIMARY KEY ($key)";
}
else
{
$main_key = ' PRIMARY KEY';
}
return array($main_key, $after_key);
}
/**
* create the paste table
*
* @access private
* @static
* @return void
*/
private static function _createPasteTable()
{
list($main_key, $after_key) = self::_getPrimaryKeyClauses();
self::$_db->exec(
'CREATE TABLE ' . self::$_prefix . 'paste ( ' .
"dataid CHAR(16) NOT NULL$main_key, " .
'data BLOB, ' .
'postdate INT, ' .
'expiredate INT, ' .
'opendiscussion INT, ' .
'burnafterreading INT, ' .
'meta TEXT, ' .
'attachment MEDIUMBLOB, ' .
"attachmentname BLOB$after_key );"
);
}
/**
* create the paste table
*
* @access private
* @static
* @return void
*/
private static function _createCommentTable()
{
list($main_key, $after_key) = self::_getPrimaryKeyClauses();
self::$_db->exec(
'CREATE TABLE ' . self::$_prefix . 'comment ( ' .
"dataid CHAR(16) NOT NULL$main_key, " .
'pasteid CHAR(16), ' .
'parentid CHAR(16), ' .
'data BLOB, ' .
'nickname BLOB, ' .
'vizhash BLOB, ' .
"postdate INT$after_key );"
);
self::$_db->exec(
'CREATE INDEX parent ON ' . self::$_prefix . 'comment(pasteid);'
);
}
/**
* create the paste table
*
* @access private
* @static
* @return void
*/
private static function _createConfigTable()
{
list($main_key, $after_key) = self::_getPrimaryKeyClauses('id');
self::$_db->exec(
'CREATE TABLE ' . self::$_prefix . 'config ( ' .
"id CHAR(16) NOT NULL$main_key, value TEXT$after_key );"
);
self::_exec(
'INSERT INTO ' . self::$_prefix . 'config VALUES(?,?)',
array('VERSION', zerobin::VERSION)
);
}
/**
* upgrade the database schema from an old version
*
* @access private
* @static
* @param string $oldversion
* @return void
*/
private static function _upgradeDatabase($oldversion)
{
switch ($oldversion)
{
case '0.21':
// create the meta column if necessary (pre 0.21 change)
try {
self::$_db->exec('SELECT meta FROM ' . self::$_prefix . 'paste LIMIT 1;', array());
} catch (PDOException $e) {
self::$_db->exec('ALTER TABLE ' . self::$_prefix . 'paste ADD COLUMN meta TEXT;');
}
// SQLite only allows one ALTER statement at a time...
self::$_db->exec(
'ALTER TABLE ' . self::$_prefix . 'paste ADD COLUMN attachment MEDIUMBLOB;'
);
self::$_db->exec(
'ALTER TABLE ' . self::$_prefix . 'paste ADD COLUMN attachmentname BLOB;'
);
// SQLite doesn't support MODIFY, but it allows TEXT of similar
// size as BLOB, so there is no need to change it there
if (self::$_type !== 'sqlite')
{
self::$_db->exec(
'ALTER TABLE ' . self::$_prefix . 'paste ' .
'ADD PRIMARY KEY (dataid),' .
'MODIFY COLUMN data BLOB;'
);
self::$_db->exec(
'ALTER TABLE ' . self::$_prefix . 'comment ' .
'ADD PRIMARY KEY (dataid),' .
'MODIFY COLUMN data BLOB, ' .
'MODIFY COLUMN nickname BLOB, ' .
'MODIFY COLUMN vizhash BLOB;'
);
}
else
{
self::$_db->exec(
'CREATE UNIQUE INDEX primary ON ' . self::$_prefix . 'paste(dataid);'
);
self::$_db->exec(
'CREATE UNIQUE INDEX primary ON ' . self::$_prefix . 'comment(dataid);'
);
}
self::$_db->exec(
'CREATE INDEX parent ON ' . self::$_prefix . 'comment(pasteid);'
);
}
}
} }

View File

@@ -23,6 +23,11 @@
<!--[if lt IE 10]> <!--[if lt IE 10]>
<style type="text/css">#ienotice {display:block !important;} #oldienotice {display:block !important;}</style> <style type="text/css">#ienotice {display:block !important;} #oldienotice {display:block !important;}</style>
<![endif]--> <![endif]-->
<link rel="apple-touch-icon" sizes="180x180" href="img/favicons/apple-touch-icon.png?{$VERSION|rawurlencode}" />
<link rel="icon" type="image/png" href="img/favicons/android-chrome-192x192.png?{$VERSION|rawurlencode}" sizes="192x192" />
<link rel="icon" type="image/png" href="img/favicons/favicon-32x32.png?{$VERSION|rawurlencode}" sizes="32x32" />
<link rel="icon" type="image/png" href="img/favicons/favicon-96x96.png?{$VERSION|rawurlencode}" sizes="96x96" />
<link rel="icon" type="image/png" href="img/favicons/favicon-16x16.png?{$VERSION|rawurlencode}" sizes="16x16" />
</head> </head>
<body role="document" class="navbar-spacing"> <body role="document" class="navbar-spacing">
<nav class="navbar navbar-default navbar-fixed-top"> <nav class="navbar navbar-default navbar-fixed-top">
@@ -38,13 +43,13 @@
</div> </div>
<div id="navbar" class="navbar-collapse collapse"> <div id="navbar" class="navbar-collapse collapse">
<ul class="nav navbar-nav"> <ul class="nav navbar-nav">
<li class="pr"> <li>
<button id="sendbutton" type="button" class="hidden btn btn-default navbar-btn"> <button id="sendbutton" type="button" class="hidden btn btn-default navbar-btn">
<span class="glyphicon glyphicon-upload" aria-hidden="true"></span> {function="t('Send')"} <span class="glyphicon glyphicon-upload" aria-hidden="true"></span> {function="t('Send')"}
</button> </button>{if="$EXPIRECLONE"}
<button id="clonebutton" type="button" class="hidden btn btn-default navbar-btn"> <button id="clonebutton" type="button" class="hidden btn btn-default navbar-btn">
<span class="glyphicon glyphicon-duplicate" aria-hidden="true"></span> {function="t('Clone')"} <span class="glyphicon glyphicon-duplicate" aria-hidden="true"></span> {function="t('Clone')"}
</button> </button>{/if}
<button id="rawtextbutton" type="button" class="hidden btn btn-default navbar-btn"> <button id="rawtextbutton" type="button" class="hidden btn btn-default navbar-btn">
<span class="glyphicon glyphicon-text-background" aria-hidden="true"></span> {function="t('Raw text')"} <span class="glyphicon glyphicon-text-background" aria-hidden="true"></span> {function="t('Raw text')"}
</button> </button>

View File

@@ -0,0 +1,198 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex" />
<title>{function="t('ZeroBin')"}</title>
<link type="text/css" rel="stylesheet" href="css/bootstrap/bootstrap-theme-3.3.5.css" />
<link type="text/css" rel="stylesheet" href="css/bootstrap/darkstrap-0.9.3.css" />
<link type="text/css" rel="stylesheet" href="css/bootstrap/zerobin.css?{$VERSION|rawurlencode}" />{if="$SYNTAXHIGHLIGHTING"}
<link type="text/css" rel="stylesheet" href="css/prettify/prettify.css?{$VERSION|rawurlencode}" />{if="strlen($SYNTAXHIGHLIGHTINGTHEME)"}
<link type="text/css" rel="stylesheet" href="css/prettify/{$SYNTAXHIGHLIGHTINGTHEME}.css?{$VERSION|rawurlencode}" />{/if}{/if}
<script type="text/javascript" src="js/jquery-1.11.3.js"></script>
<script type="text/javascript" src="js/sjcl-1.0.2.js"></script>
<script type="text/javascript" src="js/base64-{$BASE64JSVERSION}.js"></script>
<script type="text/javascript" src="js/rawdeflate-0.5.js"></script>
<script type="text/javascript" src="js/rawinflate-0.3.js"></script>
<script type="text/javascript" src="js/bootstrap-3.3.5.js"></script>{if="$SYNTAXHIGHLIGHTING"}
<script type="text/javascript" src="js/prettify.js?{$VERSION|rawurlencode}"></script>{/if}{if="$MARKDOWN"}
<script type="text/javascript" src="js/showdown.js?{$VERSION|rawurlencode}"></script>{/if}
<script type="text/javascript" src="js/zerobin.js?{$VERSION|rawurlencode}"></script>
<!--[if lt IE 10]>
<style type="text/css">#ienotice {display:block !important;} #oldienotice {display:block !important;}</style>
<![endif]-->
<link rel="apple-touch-icon" sizes="180x180" href="img/favicons/apple-touch-icon.png?{$VERSION|rawurlencode}" />
<link rel="icon" type="image/png" href="img/favicons/android-chrome-192x192.png?{$VERSION|rawurlencode}" sizes="192x192" />
<link rel="icon" type="image/png" href="img/favicons/favicon-32x32.png?{$VERSION|rawurlencode}" sizes="32x32" />
<link rel="icon" type="image/png" href="img/favicons/favicon-96x96.png?{$VERSION|rawurlencode}" sizes="96x96" />
<link rel="icon" type="image/png" href="img/favicons/favicon-16x16.png?{$VERSION|rawurlencode}" sizes="16x16" />
</head>
<body role="document">
<nav class="navbar navbar-inverse navbar-static-top">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
<span class="sr-only">{function="t('Toggle navigation')"}</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="reloadlink navbar-brand" href="/">{function="t('ZeroBin')"}</a>
</div>
<div id="navbar" class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li>
<button id="sendbutton" type="button" class="hidden btn btn-warning navbar-btn">
<span class="glyphicon glyphicon-upload" aria-hidden="true"></span> {function="t('Send')"}
</button>{if="$EXPIRECLONE"}
<button id="clonebutton" type="button" class="hidden btn btn-warning navbar-btn">
<span class="glyphicon glyphicon-duplicate" aria-hidden="true"></span> {function="t('Clone')"}
</button>{/if}
<button id="rawtextbutton" type="button" class="hidden btn btn-warning navbar-btn">
<span class="glyphicon glyphicon-text-background" aria-hidden="true"></span> {function="t('Raw text')"}
</button>
</li>
<li class="dropdown">
<select id="pasteExpiration" name="pasteExpiration" class="hidden">
{loop="EXPIRE"}
<option value="{$key}"{if="$key == $EXPIREDEFAULT"} selected="selected"{/if}>{$value}</option>{/loop}
</select>
<a id="expiration" href="#" class="hidden dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">{function="t('Expires')"}: <span id="pasteExpirationDisplay">{$EXPIRE[$EXPIREDEFAULT]}</span> <span class="caret"></span></a>
<ul class="dropdown-menu">
{loop="EXPIRE"}
<li>
<a href="#" onclick="$('#pasteExpiration').val('{$key}');$('#pasteExpirationDisplay').text('{$value}');return false;">
{$value}
</a>
</li>{/loop}
</ul>
</li>
<li>
<div id="burnafterreadingoption" class="navbar-text checkbox hidden">
<label>
<input type="checkbox" id="burnafterreading" name="burnafterreading" {if="$BURNAFTERREADINGSELECTED"} checked="checked"{/if} />
{function="t('Burn after reading')"}
</label>
</div>
</li>{if="$DISCUSSION"}
<li>
<div id="opendisc" class="navbar-text checkbox hidden">
<label>
<input type="checkbox" id="opendiscussion" name="opendiscussion" {if="$OPENDISCUSSION"} checked="checked"{/if} />
{function="t('Open discussion')"}
</label>
</div>
</li>{/if}{if="$PASSWORD"}
<li>
<div id="password" class="navbar-form hidden">
<input type="password" id="passwordinput" placeholder="{function="t('Password (recommended)')"}" class="form-control" size="19"/>
</div>
</li>{/if}{if="$FILEUPLOAD"}
<li id="attach" class="hidden dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">{function="t('Attach a file')"} <span class="caret"></span></a>
<ul class="dropdown-menu">
<li id="filewrap">
<div>
<input type="file" id="file" name="file" />
</div>
</li>
<li>
<a id="fileremovebutton" href="#">
{function="t('Remove attachment')"}
</a>
</li>
</ul>
</li>{/if}
<li class="dropdown">
<select id="pasteFormatter" name="pasteFormatter" class="hidden">
{loop="FORMATTER"}
<option value="{$key}"{if="$key == $FORMATTERDEFAULT"} selected="selected"{/if}>{$value}</option>{/loop}
</select>
<a id="formatter" href="#" class="hidden dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">{function="t('Format')"}: <span id="pasteFormatterDisplay">{$FORMATTER[$FORMATTERDEFAULT]}</span> <span class="caret"></span></a>
<ul class="dropdown-menu">
{loop="FORMATTER"}
<li>
<a href="#" onclick="$('#pasteFormatter').val('{$key}');$('#pasteFormatterDisplay').text('{$value}');return false;">
{$value}
</a>
</li>{/loop}
</ul>
</li>
</ul>
<ul class="nav navbar-nav pull-right">{if="strlen($LANGUAGESELECTION)"}
<li id="language" class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"><span class="glyphicon glyphicon-flag" aria-hidden="true"></span> {$LANGUAGES[$LANGUAGESELECTION][0]} <span class="caret"></span></a>
<ul class="dropdown-menu">
{loop="LANGUAGES"}
<li>
<a href="#" class="reloadlink" onclick="document.cookie='lang={$key}';">
{$value[0]} ({$value[1]})
</a>
</li>{/loop}
</ul>
</li>{/if}
<li>
<button id="newbutton" type="button" class="reloadlink hidden btn btn-warning navbar-btn">
<span class="glyphicon glyphicon-file" aria-hidden="true"></span> {function="t('New')"}
</button>
</li>
</ul>
</div>
</nav>
<header class="container">{if="strlen($NOTICE)"}
<div role="alert" class="alert alert-info">
<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span> {$NOTICE|htmlspecialchars}
</div>{/if}
<div id="remainingtime" role="alert" class="hidden alert alert-info">
<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
</div>{if="$FILEUPLOAD"}
<div id="attachment" role="alert" class="hidden alert alert-info">
<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span> <a>{function="t('Download attachment')"}</a> <span id="clonedfile" class="hidden">{function="t('Cloned file attached.')"}</span>
</div>{/if}{if="strlen($STATUS)"}
<div id="status" role="alert" class="alert alert-success">
<span class="glyphicon glyphicon-ok" aria-hidden="true"></span> {$STATUS|htmlspecialchars}
</div>{/if}
<div id="errormessage" role="alert" class="{if="!strlen($ERROR)"}hidden {/if}alert alert-danger"><span class="glyphicon glyphicon-alert" aria-hidden="true"></span> {$ERROR|htmlspecialchars}</div>
<div id="noscript" role="alert" class="nonworking alert alert-error"><span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span> {function="t('Javascript is required for ZeroBin to work.<br />Sorry for the inconvenience.')"}</div>
<div id="oldienotice" role="alert" class="hidden nonworking alert alert-danger"><span class="glyphicon glyphicon-alert" aria-hidden="true"></span> {function="t('ZeroBin requires a modern browser to work.')"}</div>
<div id="ienotice" role="alert" class="hidden alert alert-error"><span class="glyphicon glyphicon-question-sign" aria-hidden="true"></span> {function="t('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>
<div id="pasteresult" role="alert" class="hidden alert alert-success">
<span class="glyphicon glyphicon-ok" aria-hidden="true"></span>
<div id="deletelink"></div>
<div id="pastelink"></div>
</div>
</header>
<section class="container">
<article class="row">
<div id="image" class="col-md-12 text-center hidden"></div>
<div id="prettymessage" class="col-md-12 hidden">
<pre id="prettyprint" class="col-md-12 prettyprint linenums:1"></pre>
</div>
<div id="cleartext" class="col-md-12 hidden"></div>
<p class="col-md-12"><textarea id="message" name="message" cols="80" rows="25" class="form-control hidden"></textarea></p>
</article>
</section>
<section class="container">
<div id="discussion" class="hidden">
<h4>{function="t('Discussion')"}</h4>
<div id="comments"></div>
</div>
</section>
<footer class="container">
<div class="row">
<h4 class="col-md-5 col-xs-8">{function="t('ZeroBin')"} <small>- {function="t('Because ignorance is bliss')"}</small></h4>
<p class="col-md-1 col-xs-4 text-center">{$VERSION}</p>
<p id="aboutbox" class="col-md-6 col-xs-12">
{function="t('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="https://github.com/elrido/ZeroBin/wiki">project page</a>.')"}
</p>
</div>
</footer>
<div id="cipherdata" class="hidden">{$CIPHERDATA}</div>
</body>
</html>

198
tpl/bootstrap-dark.html Normal file
View File

@@ -0,0 +1,198 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex" />
<title>{function="t('ZeroBin')"}</title>
<link type="text/css" rel="stylesheet" href="css/bootstrap/bootstrap-theme-3.3.5.css" />
<link type="text/css" rel="stylesheet" href="css/bootstrap/darkstrap-0.9.3.css" />
<link type="text/css" rel="stylesheet" href="css/bootstrap/zerobin.css?{$VERSION|rawurlencode}" />{if="$SYNTAXHIGHLIGHTING"}
<link type="text/css" rel="stylesheet" href="css/prettify/prettify.css?{$VERSION|rawurlencode}" />{if="strlen($SYNTAXHIGHLIGHTINGTHEME)"}
<link type="text/css" rel="stylesheet" href="css/prettify/{$SYNTAXHIGHLIGHTINGTHEME}.css?{$VERSION|rawurlencode}" />{/if}{/if}
<script type="text/javascript" src="js/jquery-1.11.3.js"></script>
<script type="text/javascript" src="js/sjcl-1.0.2.js"></script>
<script type="text/javascript" src="js/base64-{$BASE64JSVERSION}.js"></script>
<script type="text/javascript" src="js/rawdeflate-0.5.js"></script>
<script type="text/javascript" src="js/rawinflate-0.3.js"></script>
<script type="text/javascript" src="js/bootstrap-3.3.5.js"></script>{if="$SYNTAXHIGHLIGHTING"}
<script type="text/javascript" src="js/prettify.js?{$VERSION|rawurlencode}"></script>{/if}{if="$MARKDOWN"}
<script type="text/javascript" src="js/showdown.js?{$VERSION|rawurlencode}"></script>{/if}
<script type="text/javascript" src="js/zerobin.js?{$VERSION|rawurlencode}"></script>
<!--[if lt IE 10]>
<style type="text/css">#ienotice {display:block !important;} #oldienotice {display:block !important;}</style>
<![endif]-->
<link rel="apple-touch-icon" sizes="180x180" href="img/favicons/apple-touch-icon.png?{$VERSION|rawurlencode}" />
<link rel="icon" type="image/png" href="img/favicons/android-chrome-192x192.png?{$VERSION|rawurlencode}" sizes="192x192" />
<link rel="icon" type="image/png" href="img/favicons/favicon-32x32.png?{$VERSION|rawurlencode}" sizes="32x32" />
<link rel="icon" type="image/png" href="img/favicons/favicon-96x96.png?{$VERSION|rawurlencode}" sizes="96x96" />
<link rel="icon" type="image/png" href="img/favicons/favicon-16x16.png?{$VERSION|rawurlencode}" sizes="16x16" />
</head>
<body role="document">
<nav class="navbar navbar-inverse navbar-static-top">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
<span class="sr-only">{function="t('Toggle navigation')"}</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="reloadlink navbar-brand" href="/">{function="t('ZeroBin')"}</a>
</div>
<div id="navbar" class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li>
<button id="newbutton" type="button" class="reloadlink hidden btn btn-warning navbar-btn">
<span class="glyphicon glyphicon-file" aria-hidden="true"></span> {function="t('New')"}
</button>{if="$EXPIRECLONE"}
<button id="clonebutton" type="button" class="hidden btn btn-warning navbar-btn">
<span class="glyphicon glyphicon-duplicate" aria-hidden="true"></span> {function="t('Clone')"}
</button>{/if}
<button id="rawtextbutton" type="button" class="hidden btn btn-warning navbar-btn">
<span class="glyphicon glyphicon-text-background" aria-hidden="true"></span> {function="t('Raw text')"}
</button>
</li>
<li class="dropdown">
<select id="pasteExpiration" name="pasteExpiration" class="hidden">
{loop="EXPIRE"}
<option value="{$key}"{if="$key == $EXPIREDEFAULT"} selected="selected"{/if}>{$value}</option>{/loop}
</select>
<a id="expiration" href="#" class="hidden dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">{function="t('Expires')"}: <span id="pasteExpirationDisplay">{$EXPIRE[$EXPIREDEFAULT]}</span> <span class="caret"></span></a>
<ul class="dropdown-menu">
{loop="EXPIRE"}
<li>
<a href="#" onclick="$('#pasteExpiration').val('{$key}');$('#pasteExpirationDisplay').text('{$value}');return false;">
{$value}
</a>
</li>{/loop}
</ul>
</li>
<li>
<div id="burnafterreadingoption" class="navbar-text checkbox hidden">
<label>
<input type="checkbox" id="burnafterreading" name="burnafterreading" {if="$BURNAFTERREADINGSELECTED"} checked="checked"{/if} />
{function="t('Burn after reading')"}
</label>
</div>
</li>{if="$DISCUSSION"}
<li>
<div id="opendisc" class="navbar-text checkbox hidden">
<label>
<input type="checkbox" id="opendiscussion" name="opendiscussion" {if="$OPENDISCUSSION"} checked="checked"{/if} />
{function="t('Open discussion')"}
</label>
</div>
</li>{/if}{if="$PASSWORD"}
<li>
<div id="password" class="navbar-form hidden">
<input type="password" id="passwordinput" placeholder="{function="t('Password (recommended)')"}" class="form-control" size="19"/>
</div>
</li>{/if}{if="$FILEUPLOAD"}
<li id="attach" class="hidden dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">{function="t('Attach a file')"} <span class="caret"></span></a>
<ul class="dropdown-menu">
<li id="filewrap">
<div>
<input type="file" id="file" name="file" />
</div>
</li>
<li>
<a id="fileremovebutton" href="#">
{function="t('Remove attachment')"}
</a>
</li>
</ul>
</li>{/if}
<li class="dropdown">
<select id="pasteFormatter" name="pasteFormatter" class="hidden">
{loop="FORMATTER"}
<option value="{$key}"{if="$key == $FORMATTERDEFAULT"} selected="selected"{/if}>{$value}</option>{/loop}
</select>
<a id="formatter" href="#" class="hidden dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">{function="t('Format')"}: <span id="pasteFormatterDisplay">{$FORMATTER[$FORMATTERDEFAULT]}</span> <span class="caret"></span></a>
<ul class="dropdown-menu">
{loop="FORMATTER"}
<li>
<a href="#" onclick="$('#pasteFormatter').val('{$key}');$('#pasteFormatterDisplay').text('{$value}');return false;">
{$value}
</a>
</li>{/loop}
</ul>
</li>
</ul>
<ul class="nav navbar-nav pull-right">{if="strlen($LANGUAGESELECTION)"}
<li id="language" class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"><span class="glyphicon glyphicon-flag" aria-hidden="true"></span> {$LANGUAGES[$LANGUAGESELECTION][0]} <span class="caret"></span></a>
<ul class="dropdown-menu">
{loop="LANGUAGES"}
<li>
<a href="#" class="reloadlink" onclick="document.cookie='lang={$key}';">
{$value[0]} ({$value[1]})
</a>
</li>{/loop}
</ul>
</li>{/if}
<li>
<button id="sendbutton" type="button" class="hidden btn btn-warning navbar-btn">
<span class="glyphicon glyphicon-upload" aria-hidden="true"></span> {function="t('Send')"}
</button>
</li>
</ul>
</div>
</nav>
<header class="container">{if="strlen($NOTICE)"}
<div role="alert" class="alert alert-info">
<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span> {$NOTICE|htmlspecialchars}
</div>{/if}
<div id="remainingtime" role="alert" class="hidden alert alert-info">
<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
</div>{if="$FILEUPLOAD"}
<div id="attachment" role="alert" class="hidden alert alert-info">
<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span> <a>{function="t('Download attachment')"}</a> <span id="clonedfile" class="hidden">{function="t('Cloned file attached.')"}</span>
</div>{/if}{if="strlen($STATUS)"}
<div id="status" role="alert" class="alert alert-success">
<span class="glyphicon glyphicon-ok" aria-hidden="true"></span> {$STATUS|htmlspecialchars}
</div>{/if}
<div id="errormessage" role="alert" class="{if="!strlen($ERROR)"}hidden {/if}alert alert-danger"><span class="glyphicon glyphicon-alert" aria-hidden="true"></span> {$ERROR|htmlspecialchars}</div>
<div id="noscript" role="alert" class="nonworking alert alert-error"><span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span> {function="t('Javascript is required for ZeroBin to work.<br />Sorry for the inconvenience.')"}</div>
<div id="oldienotice" role="alert" class="hidden nonworking alert alert-danger"><span class="glyphicon glyphicon-alert" aria-hidden="true"></span> {function="t('ZeroBin requires a modern browser to work.')"}</div>
<div id="ienotice" role="alert" class="hidden alert alert-error"><span class="glyphicon glyphicon-question-sign" aria-hidden="true"></span> {function="t('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>
<div id="pasteresult" role="alert" class="hidden alert alert-success">
<span class="glyphicon glyphicon-ok" aria-hidden="true"></span>
<div id="deletelink"></div>
<div id="pastelink"></div>
</div>
</header>
<section class="container">
<article class="row">
<div id="image" class="col-md-12 text-center hidden"></div>
<div id="prettymessage" class="col-md-12 hidden">
<pre id="prettyprint" class="col-md-12 prettyprint linenums:1"></pre>
</div>
<div id="cleartext" class="col-md-12 hidden"></div>
<p class="col-md-12"><textarea id="message" name="message" cols="80" rows="25" class="form-control hidden"></textarea></p>
</article>
</section>
<section class="container">
<div id="discussion" class="hidden">
<h4>{function="t('Discussion')"}</h4>
<div id="comments"></div>
</div>
</section>
<footer class="container">
<div class="row">
<h4 class="col-md-5 col-xs-8">{function="t('ZeroBin')"} <small>- {function="t('Because ignorance is bliss')"}</small></h4>
<p class="col-md-1 col-xs-4 text-center">{$VERSION}</p>
<p id="aboutbox" class="col-md-6 col-xs-12">
{function="t('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="https://github.com/elrido/ZeroBin/wiki">project page</a>.')"}
</p>
</div>
</footer>
<div id="cipherdata" class="hidden">{$CIPHERDATA}</div>
</body>
</html>

198
tpl/bootstrap-page.html Normal file
View File

@@ -0,0 +1,198 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex" />
<title>{function="t('ZeroBin')"}</title>
<link type="text/css" rel="stylesheet" href="css/bootstrap/bootstrap-3.3.5.css" />
<link type="text/css" rel="stylesheet" href="css/bootstrap/bootstrap-theme-3.3.5.css" />
<link type="text/css" rel="stylesheet" href="css/bootstrap/zerobin.css?{$VERSION|rawurlencode}" />{if="$SYNTAXHIGHLIGHTING"}
<link type="text/css" rel="stylesheet" href="css/prettify/prettify.css?{$VERSION|rawurlencode}" />{if="strlen($SYNTAXHIGHLIGHTINGTHEME)"}
<link type="text/css" rel="stylesheet" href="css/prettify/{$SYNTAXHIGHLIGHTINGTHEME}.css?{$VERSION|rawurlencode}" />{/if}{/if}
<script type="text/javascript" src="js/jquery-1.11.3.js"></script>
<script type="text/javascript" src="js/sjcl-1.0.2.js"></script>
<script type="text/javascript" src="js/base64-{$BASE64JSVERSION}.js"></script>
<script type="text/javascript" src="js/rawdeflate-0.5.js"></script>
<script type="text/javascript" src="js/rawinflate-0.3.js"></script>
<script type="text/javascript" src="js/bootstrap-3.3.5.js"></script>{if="$SYNTAXHIGHLIGHTING"}
<script type="text/javascript" src="js/prettify.js?{$VERSION|rawurlencode}"></script>{/if}{if="$MARKDOWN"}
<script type="text/javascript" src="js/showdown.js?{$VERSION|rawurlencode}"></script>{/if}
<script type="text/javascript" src="js/zerobin.js?{$VERSION|rawurlencode}"></script>
<!--[if lt IE 10]>
<style type="text/css">#ienotice {display:block !important;} #oldienotice {display:block !important;}</style>
<![endif]-->
<link rel="apple-touch-icon" sizes="180x180" href="img/favicons/apple-touch-icon.png?{$VERSION|rawurlencode}" />
<link rel="icon" type="image/png" href="img/favicons/android-chrome-192x192.png?{$VERSION|rawurlencode}" sizes="192x192" />
<link rel="icon" type="image/png" href="img/favicons/favicon-32x32.png?{$VERSION|rawurlencode}" sizes="32x32" />
<link rel="icon" type="image/png" href="img/favicons/favicon-96x96.png?{$VERSION|rawurlencode}" sizes="96x96" />
<link rel="icon" type="image/png" href="img/favicons/favicon-16x16.png?{$VERSION|rawurlencode}" sizes="16x16" />
</head>
<body role="document">
<nav class="navbar navbar-default navbar-static-top">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
<span class="sr-only">{function="t('Toggle navigation')"}</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="reloadlink navbar-brand" href="/">{function="t('ZeroBin')"}</a>
</div>
<div id="navbar" class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li>
<button id="sendbutton" type="button" class="hidden btn btn-default navbar-btn">
<span class="glyphicon glyphicon-upload" aria-hidden="true"></span> {function="t('Send')"}
</button>{if="$EXPIRECLONE"}
<button id="clonebutton" type="button" class="hidden btn btn-default navbar-btn">
<span class="glyphicon glyphicon-duplicate" aria-hidden="true"></span> {function="t('Clone')"}
</button>{/if}
<button id="rawtextbutton" type="button" class="hidden btn btn-default navbar-btn">
<span class="glyphicon glyphicon-text-background" aria-hidden="true"></span> {function="t('Raw text')"}
</button>
</li>
<li class="dropdown">
<select id="pasteExpiration" name="pasteExpiration" class="hidden">
{loop="EXPIRE"}
<option value="{$key}"{if="$key == $EXPIREDEFAULT"} selected="selected"{/if}>{$value}</option>{/loop}
</select>
<a id="expiration" href="#" class="hidden dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">{function="t('Expires')"}: <span id="pasteExpirationDisplay">{$EXPIRE[$EXPIREDEFAULT]}</span> <span class="caret"></span></a>
<ul class="dropdown-menu">
{loop="EXPIRE"}
<li>
<a href="#" onclick="$('#pasteExpiration').val('{$key}');$('#pasteExpirationDisplay').text('{$value}');return false;">
{$value}
</a>
</li>{/loop}
</ul>
</li>
<li>
<div id="burnafterreadingoption" class="navbar-text checkbox hidden">
<label>
<input type="checkbox" id="burnafterreading" name="burnafterreading" {if="$BURNAFTERREADINGSELECTED"} checked="checked"{/if} />
{function="t('Burn after reading')"}
</label>
</div>
</li>{if="$DISCUSSION"}
<li>
<div id="opendisc" class="navbar-text checkbox hidden">
<label>
<input type="checkbox" id="opendiscussion" name="opendiscussion" {if="$OPENDISCUSSION"} checked="checked"{/if} />
{function="t('Open discussion')"}
</label>
</div>
</li>{/if}{if="$PASSWORD"}
<li>
<div id="password" class="navbar-form hidden">
<input type="password" id="passwordinput" placeholder="{function="t('Password (recommended)')"}" class="form-control" size="19"/>
</div>
</li>{/if}{if="$FILEUPLOAD"}
<li id="attach" class="hidden dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">{function="t('Attach a file')"} <span class="caret"></span></a>
<ul class="dropdown-menu">
<li id="filewrap">
<div>
<input type="file" id="file" name="file" />
</div>
</li>
<li>
<a id="fileremovebutton" href="#">
{function="t('Remove attachment')"}
</a>
</li>
</ul>
</li>{/if}
<li class="dropdown">
<select id="pasteFormatter" name="pasteFormatter" class="hidden">
{loop="FORMATTER"}
<option value="{$key}"{if="$key == $FORMATTERDEFAULT"} selected="selected"{/if}>{$value}</option>{/loop}
</select>
<a id="formatter" href="#" class="hidden dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">{function="t('Format')"}: <span id="pasteFormatterDisplay">{$FORMATTER[$FORMATTERDEFAULT]}</span> <span class="caret"></span></a>
<ul class="dropdown-menu">
{loop="FORMATTER"}
<li>
<a href="#" onclick="$('#pasteFormatter').val('{$key}');$('#pasteFormatterDisplay').text('{$value}');return false;">
{$value}
</a>
</li>{/loop}
</ul>
</li>
</ul>
<ul class="nav navbar-nav pull-right">{if="strlen($LANGUAGESELECTION)"}
<li id="language" class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"><span class="glyphicon glyphicon-flag" aria-hidden="true"></span> {$LANGUAGES[$LANGUAGESELECTION][0]} <span class="caret"></span></a>
<ul class="dropdown-menu">
{loop="LANGUAGES"}
<li>
<a href="#" class="reloadlink" onclick="document.cookie='lang={$key}';">
{$value[0]} ({$value[1]})
</a>
</li>{/loop}
</ul>
</li>{/if}
<li>
<button id="newbutton" type="button" class="reloadlink hidden btn btn-default navbar-btn">
<span class="glyphicon glyphicon-file" aria-hidden="true"></span> {function="t('New')"}
</button>
</li>
</ul>
</div>
</nav>
<header class="container">{if="strlen($NOTICE)"}
<div role="alert" class="alert alert-info">
<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span> {$NOTICE|htmlspecialchars}
</div>{/if}
<div id="remainingtime" role="alert" class="hidden alert alert-info">
<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
</div>{if="$FILEUPLOAD"}
<div id="attachment" role="alert" class="hidden alert alert-info">
<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span> <a>{function="t('Download attachment')"}</a> <span id="clonedfile" class="hidden">{function="t('Cloned file attached.')"}</span>
</div>{/if}{if="strlen($STATUS)"}
<div id="status" role="alert" class="alert alert-success">
<span class="glyphicon glyphicon-ok" aria-hidden="true"></span> {$STATUS|htmlspecialchars}
</div>{/if}
<div id="errormessage" role="alert" class="{if="!strlen($ERROR)"}hidden {/if}alert alert-danger"><span class="glyphicon glyphicon-alert" aria-hidden="true"></span> {$ERROR|htmlspecialchars}</div>
<div id="noscript" role="alert" class="nonworking alert alert-warning"><span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span> {function="t('Javascript is required for ZeroBin to work.<br />Sorry for the inconvenience.')"}</div>
<div id="oldienotice" role="alert" class="hidden nonworking alert alert-danger"><span class="glyphicon glyphicon-alert" aria-hidden="true"></span> {function="t('ZeroBin requires a modern browser to work.')"}</div>
<div id="ienotice" role="alert" class="hidden alert alert-warning"><span class="glyphicon glyphicon-question-sign" aria-hidden="true"></span> {function="t('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>
<div id="pasteresult" role="alert" class="hidden alert alert-success">
<span class="glyphicon glyphicon-ok" aria-hidden="true"></span>
<div id="deletelink"></div>
<div id="pastelink"></div>
</div>
</header>
<section class="container">
<article class="row">
<div id="image" class="col-md-12 text-center hidden"></div>
<div id="prettymessage" class="col-md-12 hidden">
<pre id="prettyprint" class="col-md-12 prettyprint linenums:1"></pre>
</div>
<div id="cleartext" class="col-md-12 hidden"></div>
<p class="col-md-12"><textarea id="message" name="message" cols="80" rows="25" class="form-control hidden"></textarea></p>
</article>
</section>
<section class="container">
<div id="discussion" class="hidden">
<h4>{function="t('Discussion')"}</h4>
<div id="comments"></div>
</div>
</section>
<footer class="container">
<div class="row">
<h4 class="col-md-5 col-xs-8">{function="t('ZeroBin')"} <small>- {function="t('Because ignorance is bliss')"}</small></h4>
<p class="col-md-1 col-xs-4 text-center">{$VERSION}</p>
<p id="aboutbox" class="col-md-6 col-xs-12">
{function="t('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="https://github.com/elrido/ZeroBin/wiki">project page</a>.')"}
</p>
</div>
</footer>
<div id="cipherdata" class="hidden">{$CIPHERDATA}</div>
</body>
</html>

View File

@@ -23,10 +23,14 @@
<!--[if lt IE 10]> <!--[if lt IE 10]>
<style type="text/css">#ienotice {display:block !important;} #oldienotice {display:block !important;}</style> <style type="text/css">#ienotice {display:block !important;} #oldienotice {display:block !important;}</style>
<![endif]--> <![endif]-->
<link rel="apple-touch-icon" sizes="180x180" href="img/favicons/apple-touch-icon.png?{$VERSION|rawurlencode}" />
<link rel="icon" type="image/png" href="img/favicons/android-chrome-192x192.png?{$VERSION|rawurlencode}" sizes="192x192" />
<link rel="icon" type="image/png" href="img/favicons/favicon-32x32.png?{$VERSION|rawurlencode}" sizes="32x32" />
<link rel="icon" type="image/png" href="img/favicons/favicon-96x96.png?{$VERSION|rawurlencode}" sizes="96x96" />
<link rel="icon" type="image/png" href="img/favicons/favicon-16x16.png?{$VERSION|rawurlencode}" sizes="16x16" />
</head> </head>
<body role="document"> <body role="document">
<nav class="navbar navbar-default navbar-static-top"> <nav class="navbar navbar-default navbar-static-top">
<div class="container">
<div class="navbar-header"> <div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar"> <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
<span class="sr-only">{function="t('Toggle navigation')"}</span> <span class="sr-only">{function="t('Toggle navigation')"}</span>
@@ -38,13 +42,13 @@
</div> </div>
<div id="navbar" class="navbar-collapse collapse"> <div id="navbar" class="navbar-collapse collapse">
<ul class="nav navbar-nav"> <ul class="nav navbar-nav">
<li class="pr"> <li>
<button id="sendbutton" type="button" class="hidden btn btn-default navbar-btn"> <button id="newbutton" type="button" class="reloadlink hidden btn btn-default navbar-btn">
<span class="glyphicon glyphicon-upload" aria-hidden="true"></span> {function="t('Send')"} <span class="glyphicon glyphicon-file" aria-hidden="true"></span> {function="t('New')"}
</button> </button>{if="$EXPIRECLONE"}
<button id="clonebutton" type="button" class="hidden btn btn-default navbar-btn"> <button id="clonebutton" type="button" class="hidden btn btn-default navbar-btn">
<span class="glyphicon glyphicon-duplicate" aria-hidden="true"></span> {function="t('Clone')"} <span class="glyphicon glyphicon-duplicate" aria-hidden="true"></span> {function="t('Clone')"}
</button> </button>{/if}
<button id="rawtextbutton" type="button" class="hidden btn btn-default navbar-btn"> <button id="rawtextbutton" type="button" class="hidden btn btn-default navbar-btn">
<span class="glyphicon glyphicon-text-background" aria-hidden="true"></span> {function="t('Raw text')"} <span class="glyphicon glyphicon-text-background" aria-hidden="true"></span> {function="t('Raw text')"}
</button> </button>
@@ -123,18 +127,17 @@
{loop="LANGUAGES"} {loop="LANGUAGES"}
<li> <li>
<a href="#" class="reloadlink" onclick="document.cookie='lang={$key}';"> <a href="#" class="reloadlink" onclick="document.cookie='lang={$key}';">
{$value[0]} ({$value[1]}) {$value[0]} ({$value[1]})
</a> </a>
</li>{/loop} </li>{/loop}
</ul> </ul>
</li>{/if} </li>{/if}
<li> <li>
<button id="newbutton" type="button" class="reloadlink hidden btn btn-default navbar-btn"> <button id="sendbutton" type="button" class="hidden btn btn-primary navbar-btn">
<span class="glyphicon glyphicon-file" aria-hidden="true"></span> {function="t('New')"} <span class="glyphicon glyphicon-upload" aria-hidden="true"></span> {function="t('Send')"}
</button> </button>
</li> </li>
</ul> </ul>
</div>
</div> </div>
</nav> </nav>
<header class="container">{if="strlen($NOTICE)"} <header class="container">{if="strlen($NOTICE)"}

View File

@@ -18,6 +18,11 @@
<!--[if lt IE 10]> <!--[if lt IE 10]>
<style type="text/css">body {padding-left:60px;padding-right:60px;} #ienotice {display:block;} #oldienotice {display:block;}</style> <style type="text/css">body {padding-left:60px;padding-right:60px;} #ienotice {display:block;} #oldienotice {display:block;}</style>
<![endif]--> <![endif]-->
<link rel="apple-touch-icon" sizes="180x180" href="img/favicons/apple-touch-icon.png?{$VERSION|rawurlencode}" />
<link rel="icon" type="image/png" href="img/favicons/android-chrome-192x192.png?{$VERSION|rawurlencode}" sizes="192x192" />
<link rel="icon" type="image/png" href="img/favicons/favicon-32x32.png?{$VERSION|rawurlencode}" sizes="32x32" />
<link rel="icon" type="image/png" href="img/favicons/favicon-96x96.png?{$VERSION|rawurlencode}" sizes="96x96" />
<link rel="icon" type="image/png" href="img/favicons/favicon-16x16.png?{$VERSION|rawurlencode}" sizes="16x16" />
</head> </head>
<body> <body>
<header> <header>
@@ -43,8 +48,8 @@
<div id="errormessage" class="hidden">{$ERROR|htmlspecialchars}</div> <div id="errormessage" class="hidden">{$ERROR|htmlspecialchars}</div>
<div id="toolbar"> <div id="toolbar">
<button id="newbutton" class="reloadlink hidden"><img src="img/icon_new.png" width="11" height="15" alt="" />{function="t('New')"}</button> <button id="newbutton" class="reloadlink hidden"><img src="img/icon_new.png" width="11" height="15" alt="" />{function="t('New')"}</button>
<button id="sendbutton" class="hidden"><img src="img/icon_send.png" width="18" height="15" alt="" />{function="t('Send')"}</button> <button id="sendbutton" class="hidden"><img src="img/icon_send.png" width="18" height="15" alt="" />{function="t('Send')"}</button>{if="$EXPIRECLONE"}
<button id="clonebutton" class="hidden"><img src="img/icon_clone.png" width="15" height="17" alt="" />{function="t('Clone')"}</button> <button id="clonebutton" class="hidden"><img src="img/icon_clone.png" width="15" height="17" alt="" />{function="t('Clone')"}</button>{/if}
<button id="rawtextbutton" class="hidden"><img src="img/icon_raw.png" width="15" height="15" alt="" />{function="t('Raw text')"}</button> <button id="rawtextbutton" class="hidden"><img src="img/icon_raw.png" width="15" height="15" alt="" />{function="t('Raw text')"}</button>
<div id="expiration" class="hidden button">{function="t('Expires')"}: <div id="expiration" class="hidden button">{function="t('Expires')"}:
<select id="pasteExpiration" name="pasteExpiration"> <select id="pasteExpiration" name="pasteExpiration">

2
tst/.gitignore vendored
View File

@@ -1 +1 @@
/configuration.php /configurationCombinations.php

View File

@@ -1,8 +1,6 @@
<?php <?php
class RainTPLTest extends PHPUnit_Framework_TestCase class RainTPLTest extends PHPUnit_Framework_TestCase
{ {
private static $data = '{"iv":"EN39/wd5Nk8HAiSG2K5AsQ","salt":"QKN1DBXe5PI","ct":"8hA83xDdXjD7K2qfmw5NdA"}';
private static $error = 'foo bar'; private static $error = 'foo bar';
private static $status = '!*#@?$+'; private static $status = '!*#@?$+';
@@ -27,7 +25,7 @@ class RainTPLTest extends PHPUnit_Framework_TestCase
$page::$path_replace = false; $page::$path_replace = false;
// We escape it here because ENT_NOQUOTES can't be used in RainTPL templates. // We escape it here because ENT_NOQUOTES can't be used in RainTPL templates.
$page->assign('CIPHERDATA', htmlspecialchars(self::$data, ENT_NOQUOTES)); $page->assign('CIPHERDATA', htmlspecialchars(helper::getPaste()['data'], ENT_NOQUOTES));
$page->assign('ERROR', self::$error); $page->assign('ERROR', self::$error);
$page->assign('STATUS', self::$status); $page->assign('STATUS', self::$status);
$page->assign('VERSION', self::$version); $page->assign('VERSION', self::$version);
@@ -45,6 +43,7 @@ class RainTPLTest extends PHPUnit_Framework_TestCase
$page->assign('LANGUAGES', i18n::getLanguageLabels(i18n::getAvailableLanguages())); $page->assign('LANGUAGES', i18n::getLanguageLabels(i18n::getAvailableLanguages()));
$page->assign('EXPIRE', self::$expire); $page->assign('EXPIRE', self::$expire);
$page->assign('EXPIREDEFAULT', self::$expire_default); $page->assign('EXPIREDEFAULT', self::$expire_default);
$page->assign('EXPIRECLONE', true);
ob_start(); ob_start();
$page->draw('page'); $page->draw('page');
$this->_content = ob_get_contents(); $this->_content = ob_get_contents();
@@ -65,7 +64,7 @@ class RainTPLTest extends PHPUnit_Framework_TestCase
$this->assertTag( $this->assertTag(
array( array(
'id' => 'cipherdata', 'id' => 'cipherdata',
'content' => htmlspecialchars(self::$data, ENT_NOQUOTES) 'content' => htmlspecialchars(helper::getPaste()['data'], ENT_NOQUOTES)
), ),
$this->_content, $this->_content,
'outputs data correctly' 'outputs data correctly'

View File

@@ -3,12 +3,143 @@ error_reporting( E_ALL | E_STRICT );
// change this, if your php files and data is outside of your webservers document root // change this, if your php files and data is outside of your webservers document root
if (!defined('PATH')) define('PATH', '..' . DIRECTORY_SEPARATOR); if (!defined('PATH')) define('PATH', '..' . DIRECTORY_SEPARATOR);
if (!defined('CONF')) define('CONF', PATH . 'cfg' . DIRECTORY_SEPARATOR . 'conf.ini');
if (!defined('PUBLIC_PATH')) define('PUBLIC_PATH', '..'); if (!defined('PUBLIC_PATH')) define('PUBLIC_PATH', '..');
require PATH . 'lib/auto.php'; require PATH . 'lib/auto.php';
class helper class helper
{ {
/**
* example ID of a paste
*
* @var string
*/
private static $pasteid = '5e9bc25c89fb3bf9';
/**
* example paste
*
* @var array
*/
private static $paste = array(
'data' => '{"iv":"EN39/wd5Nk8HAiSG2K5AsQ","v":1,"iter":1000,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"QKN1DBXe5PI","ct":"8hA83xDdXjD7K2qfmw5NdA"}',
'attachment' => '{"iv":"Pd4pOKWkmDTT9uPwVwd5Ag","v":1,"iter":1000,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"ZIUhFTliVz4","ct":"6nOCU3peNDclDDpFtJEBKA"}',
'attachmentname' => '{"iv":"76MkAtOGC4oFogX/aSMxRA","v":1,"iter":1000,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"ZIUhFTliVz4","ct":"b6Ae/U1xJdsX/+lATud4sQ"}',
'meta' => array(
'formatter' => 'plaintext',
'postdate' => 1344803344,
'opendiscussion' => true,
),
);
/**
* example ID of a comment
*
* @var string
*/
private static $commentid = '5a52eebf11c4c94b';
/**
* example comment
*
* @var array
*/
private static $comment = array(
'data' => '{"iv":"Pd4pOKWkmDTT9uPwVwd5Ag","v":1,"iter":1000,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"ZIUhFTliVz4","ct":"6nOCU3peNDclDDpFtJEBKA"}',
'meta' => array(
'nickname' => '{"iv":"76MkAtOGC4oFogX/aSMxRA","v":1,"iter":1000,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","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,
),
);
/**
* get example paste ID
*
* @return string
*/
public static function getPasteId()
{
return self::$pasteid;
}
/**
* get example paste
*
* @return array
*/
public static function getPaste($meta = array())
{
$example = self::getPasteWithAttachment($meta);
unset($example['attachment'], $example['attachmentname']);
return $example;
}
/**
* get example paste
*
* @return array
*/
public static function getPasteWithAttachment($meta = array())
{
$example = self::$paste;
$example['meta'] = array_merge($example['meta'], $meta);
return $example;
}
/**
* get example paste
*
* @return array
*/
public static function getPasteAsJson($meta = array())
{
$example = self::getPaste();
if (count($meta))
$example['meta'] = $meta;
$example['comments'] = array();
$example['comment_count'] = 0;
$example['comment_offset'] = 0;
$example['@context'] = 'js/paste.jsonld';
return json_encode($example);
}
/**
* get example paste ID
*
* @return string
*/
public static function getCommentId()
{
return self::$commentid;
}
/**
* get example comment
*
* @return array
*/
public static function getComment($meta = array())
{
$example = self::$comment;
$example['meta'] = array_merge($example['meta'], $meta);
return $example;
}
/**
* get example comment
*
* @return array
*/
public static function getCommentPost($meta = array())
{
$example = self::getComment($meta);
$example['nickname'] = $example['meta']['nickname'];
unset($example['meta']['nickname']);
return $example;
}
/** /**
* delete directory and all its contents recursively * delete directory and all its contents recursively
* *
@@ -36,6 +167,28 @@ class helper
} }
} }
/**
* create a backup of the config file
*
* @return void
*/
public static function confBackup()
{
if (!is_file(CONF . '.bak') && is_file(CONF))
rename(CONF, CONF . '.bak');
}
/**
* restor backup of the config file
*
* @return void
*/
public static function confRestore()
{
if (is_file(CONF . '.bak'))
rename(CONF . '.bak', CONF);
}
/** /**
* create ini file * create ini file
* *
@@ -54,6 +207,18 @@ class helper
continue; continue;
} elseif (is_string($setting)) { } elseif (is_string($setting)) {
$setting = '"' . $setting . '"'; $setting = '"' . $setting . '"';
} elseif (is_array($setting)) {
foreach ($setting as $key => $value) {
if (is_null($value)) {
$value = 'null';
} elseif (is_string($value)) {
$value = '"' . $value . '"';
} else {
$value = var_export($value, true);
}
fwrite($ini, $option . "[$key] = $value" . PHP_EOL);
}
continue;
} else { } else {
$setting = var_export($setting, true); $setting = var_export($setting, true);
} }

View File

@@ -44,7 +44,7 @@ new configurationTestGenerator(array(
'settings' => array('$_POST["opendiscussion"] = "neither 1 nor 0"'), 'settings' => array('$_POST["opendiscussion"] = "neither 1 nor 0"'),
'type' => 'False', 'type' => 'False',
'args' => array( 'args' => array(
'$this->_model->exists(self::$pasteid)', '$this->_model->exists(helper::getPasteId())',
'when discussions are enabled, but invalid flag posted, paste is not created' 'when discussions are enabled, but invalid flag posted, paste is not created'
), ),
), ),
@@ -108,134 +108,6 @@ new configurationTestGenerator(array(
'affects' => $vrd 'affects' => $vrd
), ),
), ),
'main/syntaxhighlighting' => array(
array(
'setting' => true,
'tests' => array(
array(
'type' => 'Tag',
'args' => array(
array(
'tag' => 'link',
'attributes' => array(
'type' => 'text/css',
'rel' => 'stylesheet',
'href' => 'regexp:#css/prettify/prettify\.css#',
),
),
'$content',
'outputs prettify stylesheet correctly',
),
), array(
'type' => 'Tag',
'args' => array(
array(
'tag' => 'script',
'attributes' => array(
'type' => 'text/javascript',
'src' => 'regexp:#js/prettify\.js#'
),
),
'$content',
'outputs prettify javascript correctly',
),
),
),
'affects' => $vrd,
), array(
'setting' => false,
'tests' => array(
array(
'type' => 'NotTag',
'args' => array(
array(
'tag' => 'link',
'attributes' => array(
'type' => 'text/css',
'rel' => 'stylesheet',
'href' => 'regexp:#css/prettify/prettify\.css#',
),
),
'$content',
'removes prettify stylesheet correctly',
),
), array(
'type' => 'NotTag',
'args' => array(
array(
'tag' => 'script',
'attributes' => array(
'type' => 'text/javascript',
'src' => 'regexp:#js/prettify\.js#',
),
),
'$content',
'removes prettify javascript correctly',
),
),
),
'affects' => $vrd,
),
),
'main/syntaxhighlightingtheme' => array(
array(
'setting' => 'sons-of-obsidian',
'tests' => array(
array(
'conditions' => array('main/syntaxhighlighting' => true),
'type' => 'Tag',
'args' => array(
array(
'tag' => 'link',
'attributes' => array(
'type' => 'text/css',
'rel' => 'stylesheet',
'href' => 'regexp:#css/prettify/sons-of-obsidian\.css#',
),
),
'$content',
'outputs prettify theme stylesheet correctly',
),
), array(
'conditions' => array('main/syntaxhighlighting' => false),
'type' => 'NotTag',
'args' => array(
array(
'tag' => 'link',
'attributes' => array(
'type' => 'text/css',
'rel' => 'stylesheet',
'href' => 'regexp:#css/prettify/sons-of-obsidian\.css#',
),
),
'$content',
'removes prettify theme stylesheet correctly',
),
),
),
'affects' => $vrd,
), array(
'setting' => null, // option not set
'tests' => array(
array(
'type' => 'NotTag',
'args' => array(
array(
'tag' => 'link',
'attributes' => array(
'type' => 'text/css',
'rel' => 'stylesheet',
'href' => 'regexp:#css/prettify/sons-of-obsidian\.css#',
),
),
'$content',
'removes prettify theme stylesheet correctly',
),
),
),
'affects' => $vrd,
),
),
'main/burnafterreadingselected' => array( 'main/burnafterreadingselected' => array(
array( array(
'setting' => true, 'setting' => true,
@@ -528,7 +400,7 @@ class configurationTestGenerator
*/ */
private function _writeConfigurationTest() private function _writeConfigurationTest()
{ {
$defaultOptions = parse_ini_file(PATH . 'cfg' . DIRECTORY_SEPARATOR . 'conf.ini', true); $defaultOptions = parse_ini_file(CONF, true);
$code = $this->_getHeader(); $code = $this->_getHeader();
foreach ($this->_configurations as $key => $conf) { foreach ($this->_configurations as $key => $conf) {
$fullOptions = array_replace_recursive($defaultOptions, $conf['options']); $fullOptions = array_replace_recursive($defaultOptions, $conf['options']);
@@ -576,7 +448,7 @@ class configurationTestGenerator
} }
} }
$code .= '}' . PHP_EOL; $code .= '}' . PHP_EOL;
file_put_contents('configuration.php', $code); file_put_contents('configurationCombinations.php', $code);
} }
/** /**
@@ -589,20 +461,10 @@ class configurationTestGenerator
return <<<'EOT' return <<<'EOT'
<?php <?php
/** /**
* DO NOT EDIT: This file is automatically generated by configGenerator.php * DO NOT EDIT: This file is generated automatically using configGenerator.php
*/ */
class configurationTest extends PHPUnit_Framework_TestCase class configurationCombinationsTest extends PHPUnit_Framework_TestCase
{ {
private static $pasteid = '5e9bc25c89fb3bf9';
private static $paste = array(
'data' => '{"iv":"EN39/wd5Nk8HAiSG2K5AsQ","v":1,"iter":1000,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"QKN1DBXe5PI","ct":"8hA83xDdXjD7K2qfmw5NdA"}',
'meta' => array(
'postdate' => 1344803344,
'opendiscussion' => true,
),
);
private $_model; private $_model;
private $_conf; private $_conf;
@@ -610,9 +472,7 @@ class configurationTest extends PHPUnit_Framework_TestCase
public function setUp() public function setUp()
{ {
/* Setup Routine */ /* Setup Routine */
$this->_conf = PATH . 'cfg' . DIRECTORY_SEPARATOR . 'conf.ini'; helper::confBackup();
if (!is_file($this->_conf . '.bak') && is_file($this->_conf))
rename($this->_conf, $this->_conf . '.bak');
$this->_model = zerobin_data::getInstance(array('dir' => PATH . 'data')); $this->_model = zerobin_data::getInstance(array('dir' => PATH . 'data'));
serversalt::setPath(PATH . 'data'); serversalt::setPath(PATH . 'data');
@@ -622,7 +482,7 @@ class configurationTest extends PHPUnit_Framework_TestCase
public function tearDown() public function tearDown()
{ {
/* Tear Down Routine */ /* Tear Down Routine */
rename($this->_conf . '.bak', $this->_conf); helper::confRestore();
} }
public function reset($configuration = array()) public function reset($configuration = array())
@@ -630,9 +490,9 @@ class configurationTest extends PHPUnit_Framework_TestCase
$_POST = array(); $_POST = array();
$_GET = array(); $_GET = array();
$_SERVER = array(); $_SERVER = array();
if ($this->_model->exists(self::$pasteid)) if ($this->_model->exists(helper::getPasteId()))
$this->_model->delete(self::$pasteid); $this->_model->delete(helper::getPasteId());
helper::createIniFile($this->_conf, $configuration); helper::createIniFile(CONF, $configuration);
} }
@@ -678,22 +538,24 @@ EOT;
switch ($step) { switch ($step) {
case 'Create': case 'Create':
$code .= PHP_EOL . <<<'EOT' $code .= PHP_EOL . <<<'EOT'
$_POST = self::$paste; $_POST = helper::getPaste();
$_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
$_SERVER['REQUEST_METHOD'] = 'POST';
$_SERVER['REMOTE_ADDR'] = '::1'; $_SERVER['REMOTE_ADDR'] = '::1';
EOT; EOT;
break; break;
case 'Read': case 'Read':
$code .= PHP_EOL . <<<'EOT' $code .= PHP_EOL . <<<'EOT'
$this->_model->create(self::$pasteid, self::$paste); $this->_model->create(helper::getPasteId(), helper::getPaste());
$_SERVER['QUERY_STRING'] = self::$pasteid; $_SERVER['QUERY_STRING'] = helper::getPasteId();
EOT; EOT;
break; break;
case 'Delete': case 'Delete':
$code .= PHP_EOL . <<<'EOT' $code .= PHP_EOL . <<<'EOT'
$this->_model->create(self::$pasteid, self::$paste); $this->_model->create(helper::getPasteId(), helper::getPaste());
$this->assertTrue($this->_model->exists(self::$pasteid), 'paste exists before deleting data'); $this->assertTrue($this->_model->exists(helper::getPasteId()), 'paste exists before deleting data');
$_GET['pasteid'] = self::$pasteid; $_GET['pasteid'] = helper::getPasteId();
$_GET['deletetoken'] = hash_hmac('sha1', self::$pasteid, serversalt::get()); $_GET['deletetoken'] = hash_hmac('sha1', helper::getPasteId(), serversalt::get());
EOT; EOT;
break; break;
} }
@@ -720,7 +582,7 @@ EOT;
$this->assertTag( $this->assertTag(
array( array(
'id' => 'cipherdata', 'id' => 'cipherdata',
'content' => htmlspecialchars(json_encode(self::$paste), ENT_NOQUOTES) 'content' => htmlspecialchars(helper::getPasteAsJson(), ENT_NOQUOTES)
), ),
$content, $content,
'outputs data correctly' 'outputs data correctly'
@@ -738,7 +600,7 @@ EOT;
$content, $content,
'outputs deleted status correctly' 'outputs deleted status correctly'
); );
$this->assertFalse($this->_model->exists(self::$pasteid), 'paste successfully deleted'); $this->assertFalse($this->_model->exists(helper::getPasteId()), 'paste successfully deleted');
EOT; EOT;
break; break;
} }

161
tst/configuration.php Normal file
View File

@@ -0,0 +1,161 @@
<?php
class configurationTest extends PHPUnit_Framework_TestCase
{
private $_options = array(
'main' => array(
'discussion' => true,
'opendiscussion' => false,
'password' => true,
'fileupload' => false,
'burnafterreadingselected' => false,
'defaultformatter' => 'plaintext',
'syntaxhighlightingtheme' => null,
'sizelimit' => 2097152,
'template' => 'bootstrap',
'notice' => '',
'base64version' => '2.1.9',
'languageselection' => false,
'languagedefault' => '',
),
'expire' => array(
'default' => '1week',
'clone' => true,
),
'expire_options' => array(
'5min' => 300,
'10min' => 600,
'1hour' => 3600,
'1day' => 86400,
'1week' => 604800,
'1month' => 2592000,
'1year' => 31536000,
'never' => 0,
),
'formatter_options' => array(
'plaintext' => 'Plain Text',
'syntaxhighlighting' => 'Source Code',
'markdown' => 'Markdown',
),
'traffic' => array(
'limit' => 10,
'header' => null,
'dir' => '../data',
),
'model' => array(
'class' => 'zerobin_data',
),
'model_options' => array(
'dir' => '../data',
),
);
public function setUp()
{
/* Setup Routine */
helper::confBackup();
}
public function tearDown()
{
/* Tear Down Routine */
helper::confRestore();
}
public function testDefaultConfigFile()
{
$this->assertTrue(copy(CONF . '.bak', CONF), 'copy default configuration file');
$conf = new configuration;
$this->assertEquals($this->_options, $conf->get(), 'default configuration is correct');
}
public function testHandleFreshConfigFile()
{
helper::createIniFile(CONF, $this->_options);
$conf = new configuration;
$this->assertEquals($this->_options, $conf->get(), 'newly generated configuration is correct');
}
public function testHandleMissingConfigFile()
{
@unlink(CONF);
$conf = new configuration;
$this->assertEquals($this->_options, $conf->get(), 'returns correct defaults on missing file');
}
/**
* @expectedException Exception
* @expectedExceptionCode 2
*/
public function testHandleBlankConfigFile()
{
file_put_contents(CONF, '');
$conf = new configuration;
}
public function testHandleMinimalConfigFile()
{
file_put_contents(CONF, '[main]' . PHP_EOL . '[model]');
$conf = new configuration;
$this->assertEquals($this->_options, $conf->get(), 'returns correct defaults on empty file');
}
/**
* @expectedException Exception
* @expectedExceptionCode 3
*/
public function testHandleInvalidSection()
{
file_put_contents(CONF, '[main]' . PHP_EOL . '[model]');
$conf = new configuration;
$conf->getKey('foo', 'bar');
}
/**
* @expectedException Exception
* @expectedExceptionCode 4
*/
public function testHandleInvalidKey()
{
file_put_contents(CONF, '[main]' . PHP_EOL . '[model]');
$conf = new configuration;
$conf->getKey('foo');
}
public function testHandleGetKey()
{
file_put_contents(CONF, '[main]' . PHP_EOL . '[model]');
$conf = new configuration;
$this->assertEquals($this->_options['main']['sizelimit'], $conf->getKey('sizelimit'), 'get default size');
}
public function testHandleWrongTypes()
{
$original_options = $this->_options;
$original_options['main']['syntaxhighlightingtheme'] = 'foo';
$options = $original_options;
$options['main']['discussion'] = 'true';
$options['main']['opendiscussion'] = 0;
$options['main']['password'] = -1; // evaluates to TRUE
$options['main']['fileupload'] = 'false';
$options['expire_options']['foo'] = 'bar';
$options['formatter_options'][] = 'foo';
helper::createIniFile(CONF, $options);
$conf = new configuration;
$original_options['expire_options']['foo'] = intval('bar');
$original_options['formatter_options'][0] = 'foo';
$this->assertEquals($original_options, $conf->get(), 'incorrect types are corrected');
}
public function testHandleMissingSubKeys()
{
$options = $this->_options;
unset($options['expire_options']['1week']);
unset($options['expire_options']['1year']);
unset($options['expire_options']['never']);
helper::createIniFile(CONF, $options);
$conf = new configuration;
$options['expire']['default'] = '5min';
$this->assertEquals($options, $conf->get(), 'not overriding "missing" subkeys');
}
}

View File

@@ -62,13 +62,6 @@ class filterTest extends PHPUnit_Framework_TestCase
$this->assertEquals('1.21 YiB', filter::size_humanreadable(1234 * $exponent)); $this->assertEquals('1.21 YiB', filter::size_humanreadable(1234 * $exponent));
} }
public function testPasteIdValidation()
{
$this->assertTrue(filter::is_valid_paste_id('a242ab7bdfb2581a'), 'valid paste id');
$this->assertFalse(filter::is_valid_paste_id('foo'), 'invalid hex values');
$this->assertFalse(filter::is_valid_paste_id('../bar/baz'), 'path attack');
}
public function testSlowEquals() public function testSlowEquals()
{ {
$this->assertTrue(filter::slow_equals('foo', 'foo'), 'same string'); $this->assertTrue(filter::slow_equals('foo', 'foo'), 'same string');

257
tst/jsonApi.php Normal file
View File

@@ -0,0 +1,257 @@
<?php
class jsonApiTest extends PHPUnit_Framework_TestCase
{
protected $_model;
public function setUp()
{
/* Setup Routine */
$this->_model = zerobin_data::getInstance(array('dir' => PATH . 'data'));
serversalt::setPath(PATH . 'data');
$this->reset();
}
public function tearDown()
{
/* Tear Down Routine */
helper::confRestore();
}
public function reset()
{
$_POST = array();
$_GET = array();
$_SERVER = array();
if ($this->_model->exists(helper::getPasteId()))
$this->_model->delete(helper::getPasteId());
helper::confRestore();
}
/**
* @runInSeparateProcess
*/
public function testCreate()
{
$this->reset();
$options = parse_ini_file(CONF, true);
$options['traffic']['limit'] = 0;
helper::confBackup();
helper::createIniFile(CONF, $options);
$_POST = helper::getPaste();
$_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
$_SERVER['REQUEST_METHOD'] = 'POST';
$_SERVER['REMOTE_ADDR'] = '::1';
ob_start();
new zerobin;
$content = ob_get_contents();
$response = json_decode($content, true);
$this->assertEquals(0, $response['status'], 'outputs status');
$this->assertEquals(
hash_hmac('sha1', $response['id'], serversalt::get()),
$response['deletetoken'],
'outputs valid delete token'
);
$this->assertStringEndsWith('?' . $response['id'], $response['url'], 'returned URL points to new paste');
$this->assertTrue($this->_model->exists($response['id']), 'paste exists after posting data');
}
/**
* @runInSeparateProcess
*/
public function testPut()
{
$this->reset();
$options = parse_ini_file(CONF, true);
$options['traffic']['limit'] = 0;
helper::confBackup();
helper::createIniFile(CONF, $options);
$paste = helper::getPaste();
unset($paste['meta']);
$file = tempnam(sys_get_temp_dir(), 'FOO');
file_put_contents($file, http_build_query($paste));
request::setInputStream($file);
$_SERVER['QUERY_STRING'] = helper::getPasteId();
$_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
$_SERVER['REQUEST_METHOD'] = 'PUT';
$_SERVER['REMOTE_ADDR'] = '::1';
ob_start();
new zerobin;
$content = ob_get_contents();
$response = json_decode($content, true);
$this->assertEquals(0, $response['status'], 'outputs status');
$this->assertEquals(helper::getPasteId(), $response['id'], 'outputted paste ID matches input');
$this->assertEquals(
hash_hmac('sha1', $response['id'], serversalt::get()),
$response['deletetoken'],
'outputs valid delete token'
);
$this->assertStringEndsWith('?' . $response['id'], $response['url'], 'returned URL points to new paste');
$this->assertTrue($this->_model->exists($response['id']), 'paste exists after posting data');
}
/**
* @runInSeparateProcess
*/
public function testDelete()
{
$this->reset();
$this->_model->create(helper::getPasteId(), helper::getPaste());
$this->assertTrue($this->_model->exists(helper::getPasteId()), 'paste exists before deleting data');
$file = tempnam(sys_get_temp_dir(), 'FOO');
file_put_contents($file, http_build_query(array(
'deletetoken' => hash_hmac('sha1', helper::getPasteId(), serversalt::get()),
)));
request::setInputStream($file);
$_SERVER['QUERY_STRING'] = helper::getPasteId();
$_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
$_SERVER['REQUEST_METHOD'] = 'DELETE';
ob_start();
new zerobin;
$content = ob_get_contents();
$response = json_decode($content, true);
$this->assertEquals(0, $response['status'], 'outputs status');
$this->assertFalse($this->_model->exists(helper::getPasteId()), 'paste successfully deleted');
}
/**
* @runInSeparateProcess
*/
public function testDeleteWithPost()
{
$this->reset();
$this->_model->create(helper::getPasteId(), helper::getPaste());
$this->assertTrue($this->_model->exists(helper::getPasteId()), 'paste exists before deleting data');
$_POST = array(
'action' => 'delete',
'deletetoken' => hash_hmac('sha1', helper::getPasteId(), serversalt::get()),
);
$_SERVER['QUERY_STRING'] = helper::getPasteId();
$_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
$_SERVER['REQUEST_METHOD'] = 'POST';
ob_start();
new zerobin;
$content = ob_get_contents();
$response = json_decode($content, true);
$this->assertEquals(0, $response['status'], 'outputs status');
$this->assertFalse($this->_model->exists(helper::getPasteId()), 'paste successfully deleted');
}
/**
* @runInSeparateProcess
*/
public function testRead()
{
$this->reset();
$paste = helper::getPasteWithAttachment();
$this->_model->create(helper::getPasteId(), $paste);
$_SERVER['QUERY_STRING'] = helper::getPasteId();
$_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
ob_start();
new zerobin;
$content = ob_get_contents();
$response = json_decode($content, true);
$this->assertEquals(0, $response['status'], 'outputs success status');
$this->assertEquals(helper::getPasteId(), $response['id'], 'outputs data correctly');
$this->assertStringEndsWith('?' . $response['id'], $response['url'], 'returned URL points to new paste');
$this->assertEquals($paste['data'], $response['data'], 'outputs data correctly');
$this->assertEquals($paste['attachment'], $response['attachment'], 'outputs attachment correctly');
$this->assertEquals($paste['attachmentname'], $response['attachmentname'], 'outputs attachmentname correctly');
$this->assertEquals($paste['meta']['formatter'], $response['meta']['formatter'], 'outputs format correctly');
$this->assertEquals($paste['meta']['postdate'], $response['meta']['postdate'], 'outputs postdate correctly');
$this->assertEquals($paste['meta']['opendiscussion'], $response['meta']['opendiscussion'], 'outputs opendiscussion correctly');
$this->assertEquals(0, $response['comment_count'], 'outputs comment_count correctly');
$this->assertEquals(0, $response['comment_offset'], 'outputs comment_offset correctly');
}
/**
* @runInSeparateProcess
*/
public function testJsonLdPaste()
{
$this->reset();
$paste = helper::getPasteWithAttachment();
$this->_model->create(helper::getPasteId(), $paste);
$_GET['jsonld'] = 'paste';
ob_start();
new zerobin;
$content = ob_get_contents();
$this->assertEquals(str_replace(
'?jsonld=',
'/?jsonld=',
file_get_contents(PUBLIC_PATH . '/js/paste.jsonld')
), $content, 'outputs data correctly');
}
/**
* @runInSeparateProcess
*/
public function testJsonLdComment()
{
$this->reset();
$paste = helper::getPasteWithAttachment();
$this->_model->create(helper::getPasteId(), $paste);
$_GET['jsonld'] = 'comment';
ob_start();
new zerobin;
$content = ob_get_contents();
$this->assertEquals(str_replace(
'?jsonld=',
'/?jsonld=',
file_get_contents(PUBLIC_PATH . '/js/comment.jsonld')
), $content, 'outputs data correctly');
}
/**
* @runInSeparateProcess
*/
public function testJsonLdPasteMeta()
{
$this->reset();
$paste = helper::getPasteWithAttachment();
$this->_model->create(helper::getPasteId(), $paste);
$_GET['jsonld'] = 'pastemeta';
ob_start();
new zerobin;
$content = ob_get_contents();
$this->assertEquals(str_replace(
'?jsonld=',
'/?jsonld=',
file_get_contents(PUBLIC_PATH . '/js/pastemeta.jsonld')
), $content, 'outputs data correctly');
}
/**
* @runInSeparateProcess
*/
public function testJsonLdCommentMeta()
{
$this->reset();
$paste = helper::getPasteWithAttachment();
$this->_model->create(helper::getPasteId(), $paste);
$_GET['jsonld'] = 'commentmeta';
ob_start();
new zerobin;
$content = ob_get_contents();
$this->assertEquals(str_replace(
'?jsonld=',
'/?jsonld=',
file_get_contents(PUBLIC_PATH . '/js/commentmeta.jsonld')
), $content, 'outputs data correctly');
}
/**
* @runInSeparateProcess
*/
public function testJsonLdInvalid()
{
$this->reset();
$paste = helper::getPasteWithAttachment();
$this->_model->create(helper::getPasteId(), $paste);
$_GET['jsonld'] = '../cfg/conf.ini';
ob_start();
new zerobin;
$content = ob_get_contents();
$this->assertEquals('{}', $content, 'does not output nasty data');
}
}

211
tst/model.php Normal file
View File

@@ -0,0 +1,211 @@
<?php
class modelTest extends PHPUnit_Framework_TestCase
{
private $_conf;
private $_model;
public function setUp()
{
/* Setup Routine */
$options = parse_ini_file(CONF, true);
$options['model'] = array(
'class' => 'zerobin_db',
);
$options['model_options'] = array(
'dsn' => 'sqlite::memory:',
'usr' => null,
'pwd' => null,
'opt' => array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION),
);
helper::confBackup();
helper::createIniFile(CONF, $options);
$this->_conf = new configuration;
$this->_model = new model($this->_conf);
$_SERVER['REMOTE_ADDR'] = '::1';
}
public function tearDown()
{
/* Tear Down Routine */
}
public function testBasicWorkflow()
{
// storing pastes
$pasteData = helper::getPaste();
$this->_model->getPaste(helper::getPasteId())->delete();
$paste = $this->_model->getPaste(helper::getPasteId());
$this->assertFalse($paste->exists(), 'paste does not yet exist');
$paste = $this->_model->getPaste();
$paste->setData($pasteData['data']);
$paste->setOpendiscussion();
$paste->setFormatter($pasteData['meta']['formatter']);
$paste->store();
$paste = $this->_model->getPaste(helper::getPasteId());
$this->assertTrue($paste->exists(), 'paste exists after storing it');
$paste = $paste->get();
$this->assertEquals($pasteData['data'], $paste->data);
foreach (array('opendiscussion', 'formatter') as $key) {
$this->assertEquals($pasteData['meta'][$key], $paste->meta->$key);
}
// storing comments
$commentData = helper::getComment();
$paste = $this->_model->getPaste(helper::getPasteId());
$comment = $paste->getComment(helper::getPasteId(), helper::getCommentId());
$this->assertFalse($comment->exists(), 'comment does not yet exist');
$comment = $paste->getComment(helper::getPasteId());
$comment->setData($commentData['data']);
$comment->setNickname($commentData['meta']['nickname']);
$comment->store();
$comment = $paste->getComment(helper::getPasteId(), helper::getCommentId());
$this->assertTrue($comment->exists(), 'comment exists after storing it');
$comment = $comment->get();
$this->assertEquals($commentData['data'], $comment->data);
$this->assertEquals($commentData['meta']['nickname'], $comment->meta->nickname);
// deleting pastes
$this->_model->getPaste(helper::getPasteId())->delete();
$paste = $this->_model->getPaste(helper::getPasteId());
$this->assertFalse($paste->exists(), 'paste successfully deleted');
$this->assertEquals(array(), $paste->getComments(), 'comment was deleted with paste');
}
/**
* @expectedException Exception
* @expectedExceptionCode 75
*/
public function testPasteDuplicate()
{
$pasteData = helper::getPaste();
$this->_model->getPaste(helper::getPasteId())->delete();
$paste = $this->_model->getPaste();
$paste->setData($pasteData['data']);
$paste->setOpendiscussion();
$paste->setFormatter($pasteData['meta']['formatter']);
$paste->store();
$paste = $this->_model->getPaste();
$paste->setData($pasteData['data']);
$paste->setOpendiscussion();
$paste->setFormatter($pasteData['meta']['formatter']);
$paste->store();
}
/**
* @expectedException Exception
* @expectedExceptionCode 69
*/
public function testCommentDuplicate()
{
$pasteData = helper::getPaste();
$commentData = helper::getComment();
$this->_model->getPaste(helper::getPasteId())->delete();
$paste = $this->_model->getPaste();
$paste->setData($pasteData['data']);
$paste->setOpendiscussion();
$paste->setFormatter($pasteData['meta']['formatter']);
$paste->store();
$comment = $paste->getComment(helper::getPasteId());
$comment->setData($commentData['data']);
$comment->setNickname($commentData['meta']['nickname']);
$comment->store();
$comment = $paste->getComment(helper::getPasteId());
$comment->setData($commentData['data']);
$comment->setNickname($commentData['meta']['nickname']);
$comment->store();
}
public function testImplicitDefaults()
{
$pasteData = helper::getPaste();
$commentData = helper::getComment();
$this->_model->getPaste(helper::getPasteId())->delete();
$paste = $this->_model->getPaste();
$paste->setData($pasteData['data']);
$paste->setBurnafterreading();
$paste->setOpendiscussion();
// not setting a formatter, should use default one
$paste->store();
$paste = $this->_model->getPaste(helper::getPasteId())->get(); // ID was set based on data
$this->assertEquals(true, property_exists($paste->meta, 'burnafterreading') && $paste->meta->burnafterreading, 'burn after reading takes precendence');
$this->assertEquals(false, property_exists($paste->meta, 'opendiscussion') && $paste->meta->opendiscussion, 'opendiscussion is disabled');
$this->assertEquals($this->_conf->getKey('defaultformatter'), $paste->meta->formatter, 'default formatter is set');
$this->_model->getPaste(helper::getPasteId())->delete();
$paste = $this->_model->getPaste();
$paste->setData($pasteData['data']);
$paste->setBurnafterreading('0');
$paste->setOpendiscussion();
$paste->store();
$vz = new vizhash16x16();
$pngdata = 'data:image/png;base64,' . base64_encode($vz->generate($_SERVER['REMOTE_ADDR']));
$comment = $paste->getComment(helper::getPasteId());
$comment->setData($commentData['data']);
$comment->setNickname($commentData['meta']['nickname']);
$comment->store();
$comment = $paste->getComment(helper::getPasteId(), helper::getCommentId())->get();
$this->assertEquals($pngdata, $comment->meta->vizhash, 'nickname triggers vizhash to be set');
}
public function testPasteIdValidation()
{
$this->assertTrue(model_paste::isValidId('a242ab7bdfb2581a'), 'valid paste id');
$this->assertFalse(model_paste::isValidId('foo'), 'invalid hex values');
$this->assertFalse(model_paste::isValidId('../bar/baz'), 'path attack');
}
/**
* @expectedException Exception
* @expectedExceptionCode 62
*/
public function testInvalidComment()
{
$paste = $this->_model->getPaste();
$comment = $paste->getComment(helper::getPasteId());
}
public function testExpiration()
{
$pasteData = helper::getPaste();
$this->_model->getPaste(helper::getPasteId())->delete();
$paste = $this->_model->getPaste(helper::getPasteId());
$this->assertFalse($paste->exists(), 'paste does not yet exist');
$paste = $this->_model->getPaste();
$paste->setData($pasteData['data']);
$paste->setExpiration('5min'); // = 300 seconds
$paste->store();
$paste = $paste->get();
$this->assertEquals(300, $paste->meta->remaining_time, 'remaining time is set correctly');
}
/**
* @expectedException Exception
* @expectedExceptionCode 64
*/
public function testCommentDeletion()
{
$pasteData = helper::getPaste();
$this->_model->getPaste(helper::getPasteId())->delete();
$paste = $this->_model->getPaste();
$paste->setData($pasteData['data']);
$paste->store();
$paste->getComment(helper::getPasteId())->delete();
}
}

107
tst/request.php Normal file
View File

@@ -0,0 +1,107 @@
<?php
class requestTest extends PHPUnit_Framework_TestCase
{
private $_request;
public function setUp()
{
/* Setup Routine */
}
public function tearDown()
{
/* Tear Down Routine */
}
public function reset()
{
$_SERVER = array();
$_GET = array();
$_POST = array();
}
public function testView()
{
$this->reset();
$_SERVER['REQUEST_METHOD'] = 'GET';
$request = new request;
$this->assertFalse($request->isJsonApiCall(), 'is HTML call');
$this->assertEquals('view', $request->getOperation());
}
public function testRead()
{
$this->reset();
$_SERVER['REQUEST_METHOD'] = 'GET';
$_SERVER['QUERY_STRING'] = 'foo';
$request = new request;
$this->assertFalse($request->isJsonApiCall(), 'is HTML call');
$this->assertEquals('foo', $request->getParam('pasteid'));
$this->assertEquals('read', $request->getOperation());
}
public function testDelete()
{
$this->reset();
$_SERVER['REQUEST_METHOD'] = 'GET';
$_GET['pasteid'] = 'foo';
$_GET['deletetoken'] = 'bar';
$request = new request;
$this->assertFalse($request->isJsonApiCall(), 'is HTML call');
$this->assertEquals('delete', $request->getOperation());
$this->assertEquals('foo', $request->getParam('pasteid'));
$this->assertEquals('bar', $request->getParam('deletetoken'));
}
public function testApiCreate()
{
$this->reset();
$_SERVER['REQUEST_METHOD'] = 'PUT';
$_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
$file = tempnam(sys_get_temp_dir(), 'FOO');
file_put_contents($file, 'data=foo');
request::setInputStream($file);
$request = new request;
$this->assertTrue($request->isJsonApiCall(), 'is JSON Api call');
$this->assertEquals('create', $request->getOperation());
$this->assertEquals('foo', $request->getParam('data'));
}
public function testApiCreateAlternative()
{
$this->reset();
$_SERVER['REQUEST_METHOD'] = 'POST';
$_SERVER['HTTP_ACCEPT'] = 'application/json, text/javascript, */*; q=0.01';
$_POST['attachment'] = 'foo';
$request = new request;
$this->assertTrue($request->isJsonApiCall(), 'is JSON Api call');
$this->assertEquals('create', $request->getOperation());
$this->assertEquals('foo', $request->getParam('attachment'));
}
public function testApiRead()
{
$this->reset();
$_SERVER['REQUEST_METHOD'] = 'GET';
$_SERVER['HTTP_ACCEPT'] = 'application/json, text/javascript, */*; q=0.01';
$_SERVER['QUERY_STRING'] = 'foo';
$request = new request;
$this->assertTrue($request->isJsonApiCall(), 'is JSON Api call');
$this->assertEquals('foo', $request->getParam('pasteid'));
$this->assertEquals('read', $request->getOperation());
}
public function testApiDelete()
{
$this->reset();
$_SERVER['REQUEST_METHOD'] = 'POST';
$_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
$_SERVER['QUERY_STRING'] = 'foo';
$_POST['deletetoken'] = 'bar';
$request = new request;
$this->assertTrue($request->isJsonApiCall(), 'is JSON Api call');
$this->assertEquals('delete', $request->getOperation());
$this->assertEquals('foo', $request->getParam('pasteid'));
$this->assertEquals('bar', $request->getParam('deletetoken'));
}
}

View File

@@ -3,6 +3,12 @@ class sjclTest extends PHPUnit_Framework_TestCase
{ {
public function testSjclValidatorValidatesCorrectly() public function testSjclValidatorValidatesCorrectly()
{ {
$paste = helper::getPasteWithAttachment();
$this->assertTrue(sjcl::isValid($paste['data']), 'valid sjcl');
$this->assertTrue(sjcl::isValid($paste['attachment']), 'valid sjcl');
$this->assertTrue(sjcl::isValid($paste['attachmentname']), 'valid sjcl');
$this->assertTrue(sjcl::isValid(helper::getComment()['data']), 'valid sjcl');
$this->assertTrue(sjcl::isValid('{"iv":"83Ax/OdUav3SanDW9dcQPg","v":1,"iter":1000,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"Gx1vA2/gQ3U","ct":"j7ImByuE5xCqD2YXm6aSyA"}'), 'valid sjcl'); $this->assertTrue(sjcl::isValid('{"iv":"83Ax/OdUav3SanDW9dcQPg","v":1,"iter":1000,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"Gx1vA2/gQ3U","ct":"j7ImByuE5xCqD2YXm6aSyA"}'), 'valid sjcl');
$this->assertFalse(sjcl::isValid('{"iv":"$","v":1,"iter":1000,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"Gx1vA2/gQ3U","ct":"j7ImByuE5xCqD2YXm6aSyA"}'), 'invalid base64 encoding of iv'); $this->assertFalse(sjcl::isValid('{"iv":"$","v":1,"iter":1000,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"Gx1vA2/gQ3U","ct":"j7ImByuE5xCqD2YXm6aSyA"}'), 'invalid base64 encoding of iv');
$this->assertFalse(sjcl::isValid('{"iv":"83Ax/OdUav3SanDW9dcQPg","v":1,"iter":1000,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"$","ct":"j7ImByuE5xCqD2YXm6aSyA"}'), 'invalid base64 encoding of salt'); $this->assertFalse(sjcl::isValid('{"iv":"83Ax/OdUav3SanDW9dcQPg","v":1,"iter":1000,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"$","ct":"j7ImByuE5xCqD2YXm6aSyA"}'), 'invalid base64 encoding of salt');

View File

@@ -22,12 +22,15 @@ class trafficlimiterTest extends PHPUnit_Framework_TestCase
$file = 'baz'; $file = 'baz';
$this->assertEquals($this->_path . DIRECTORY_SEPARATOR . $file, trafficlimiter::getPath($file)); $this->assertEquals($this->_path . DIRECTORY_SEPARATOR . $file, trafficlimiter::getPath($file));
trafficlimiter::setLimit(4); trafficlimiter::setLimit(4);
$this->assertTrue(trafficlimiter::canPass('127.0.0.1'), 'first request may pass'); $_SERVER['REMOTE_ADDR'] = '127.0.0.1';
$this->assertTrue(trafficlimiter::canPass(), 'first request may pass');
sleep(2); sleep(2);
$this->assertFalse(trafficlimiter::canPass('127.0.0.1'), 'second request is to fast, may not pass'); $this->assertFalse(trafficlimiter::canPass(), 'second request is to fast, may not pass');
sleep(3); sleep(3);
$this->assertTrue(trafficlimiter::canPass('127.0.0.1'), 'third request waited long enough and may pass'); $this->assertTrue(trafficlimiter::canPass(), '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'); $_SERVER['REMOTE_ADDR'] = '2001:1620:2057:dead:beef::cafe:babe';
$this->assertFalse(trafficlimiter::canPass('127.0.0.1'), 'fifth request is to fast, may not pass'); $this->assertTrue(trafficlimiter::canPass(), 'fourth request has different ip and may pass');
$_SERVER['REMOTE_ADDR'] = '127.0.0.1';
$this->assertFalse(trafficlimiter::canPass(), 'fifth request is to fast, may not pass');
} }
} }

View File

@@ -1,44 +1,20 @@
<?php <?php
class zerobinTest extends PHPUnit_Framework_TestCase class zerobinTest extends PHPUnit_Framework_TestCase
{ {
private static $pasteid = '5e9bc25c89fb3bf9'; protected $_model;
private static $paste = array(
'data' => '{"iv":"EN39/wd5Nk8HAiSG2K5AsQ","v":1,"iter":1000,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"QKN1DBXe5PI","ct":"8hA83xDdXjD7K2qfmw5NdA"}',
'meta' => array(
'postdate' => 1344803344,
'opendiscussion' => true,
'formatter' => 'syntaxhighlighting',
),
);
private static $commentid = '5a52eebf11c4c94b';
private static $comment = array(
'data' => '{"iv":"Pd4pOKWkmDTT9uPwVwd5Ag","v":1,"iter":1000,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"ZIUhFTliVz4","ct":"6nOCU3peNDclDDpFtJEBKA"}',
'meta' => array(
'nickname' => '{"iv":"76MkAtOGC4oFogX/aSMxRA","v":1,"iter":1000,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","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 $_conf;
private $_model;
public function setUp() public function setUp()
{ {
/* Setup Routine */ /* Setup Routine */
$this->_model = zerobin_data::getInstance(array('dir' => PATH . 'data')); $this->_model = zerobin_data::getInstance(array('dir' => PATH . 'data'));
serversalt::setPath(PATH . 'data'); serversalt::setPath(PATH . 'data');
$this->_conf = PATH . 'cfg' . DIRECTORY_SEPARATOR . 'conf.ini';
$this->reset(); $this->reset();
} }
public function tearDown() public function tearDown()
{ {
/* Tear Down Routine */ /* Tear Down Routine */
helper::confRestore();
} }
public function reset() public function reset()
@@ -46,10 +22,9 @@ class zerobinTest extends PHPUnit_Framework_TestCase
$_POST = array(); $_POST = array();
$_GET = array(); $_GET = array();
$_SERVER = array(); $_SERVER = array();
if ($this->_model->exists(self::$pasteid)) if ($this->_model->exists(helper::getPasteId()))
$this->_model->delete(self::$pasteid); $this->_model->delete(helper::getPasteId());
if (is_file($this->_conf . '.bak')) helper::confRestore();
rename($this->_conf . '.bak', $this->_conf);
} }
/** /**
@@ -77,11 +52,10 @@ class zerobinTest extends PHPUnit_Framework_TestCase
public function testViewLanguageSelection() public function testViewLanguageSelection()
{ {
$this->reset(); $this->reset();
$options = parse_ini_file($this->_conf, true); $options = parse_ini_file(CONF, true);
$options['main']['languageselection'] = true; $options['main']['languageselection'] = true;
if (!is_file($this->_conf . '.bak') && is_file($this->_conf)) helper::confBackup();
rename($this->_conf, $this->_conf . '.bak'); helper::createIniFile(CONF, $options);
helper::createIniFile($this->_conf, $options);
$_COOKIE['lang'] = 'de'; $_COOKIE['lang'] = 'de';
ob_start(); ob_start();
new zerobin; new zerobin;
@@ -126,9 +100,8 @@ class zerobinTest extends PHPUnit_Framework_TestCase
public function testConf() public function testConf()
{ {
$this->reset(); $this->reset();
if (!is_file($this->_conf . '.bak') && is_file($this->_conf)) helper::confBackup();
rename($this->_conf, $this->_conf . '.bak'); file_put_contents(CONF, '');
file_put_contents($this->_conf, '');
ob_start(); ob_start();
new zerobin; new zerobin;
$content = ob_get_contents(); $content = ob_get_contents();
@@ -140,7 +113,13 @@ class zerobinTest extends PHPUnit_Framework_TestCase
public function testCreate() public function testCreate()
{ {
$this->reset(); $this->reset();
$_POST = self::$paste; $options = parse_ini_file(CONF, true);
$options['traffic']['limit'] = 0;
helper::confBackup();
helper::createIniFile(CONF, $options);
$_POST = helper::getPaste();
$_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
$_SERVER['REQUEST_METHOD'] = 'POST';
$_SERVER['REMOTE_ADDR'] = '::1'; $_SERVER['REMOTE_ADDR'] = '::1';
ob_start(); ob_start();
new zerobin; new zerobin;
@@ -161,14 +140,17 @@ class zerobinTest extends PHPUnit_Framework_TestCase
public function testCreateInvalidTimelimit() public function testCreateInvalidTimelimit()
{ {
$this->reset(); $this->reset();
$_POST = self::$paste; $_POST = helper::getPaste();
$_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
$_SERVER['REQUEST_METHOD'] = 'POST';
$_SERVER['REMOTE_ADDR'] = '::1'; $_SERVER['REMOTE_ADDR'] = '::1';
trafficlimiter::canPass();
ob_start(); ob_start();
new zerobin; new zerobin;
$content = ob_get_contents(); $content = ob_get_contents();
$response = json_decode($content, true); $response = json_decode($content, true);
$this->assertEquals(1, $response['status'], 'outputs error status'); $this->assertEquals(1, $response['status'], 'outputs error status');
$this->assertFalse($this->_model->exists(self::$pasteid), 'paste exists after posting data'); $this->assertFalse($this->_model->exists(helper::getPasteId()), 'paste exists after posting data');
} }
/** /**
@@ -177,20 +159,21 @@ class zerobinTest extends PHPUnit_Framework_TestCase
public function testCreateInvalidSize() public function testCreateInvalidSize()
{ {
$this->reset(); $this->reset();
$options = parse_ini_file($this->_conf, true); $options = parse_ini_file(CONF, true);
$options['main']['sizelimit'] = 10; $options['main']['sizelimit'] = 10;
$options['traffic']['limit'] = 0; $options['traffic']['limit'] = 0;
if (!is_file($this->_conf . '.bak') && is_file($this->_conf)) helper::confBackup();
rename($this->_conf, $this->_conf . '.bak'); helper::createIniFile(CONF, $options);
helper::createIniFile($this->_conf, $options); $_POST = helper::getPaste();
$_POST = self::$paste; $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
$_SERVER['REQUEST_METHOD'] = 'POST';
$_SERVER['REMOTE_ADDR'] = '::1'; $_SERVER['REMOTE_ADDR'] = '::1';
ob_start(); ob_start();
new zerobin; new zerobin;
$content = ob_get_contents(); $content = ob_get_contents();
$response = json_decode($content, true); $response = json_decode($content, true);
$this->assertEquals(1, $response['status'], 'outputs error status'); $this->assertEquals(1, $response['status'], 'outputs error status');
$this->assertFalse($this->_model->exists(self::$pasteid), 'paste exists after posting data'); $this->assertFalse($this->_model->exists(helper::getPasteId()), 'paste exists after posting data');
} }
/** /**
@@ -199,19 +182,21 @@ class zerobinTest extends PHPUnit_Framework_TestCase
public function testCreateProxyHeader() public function testCreateProxyHeader()
{ {
$this->reset(); $this->reset();
$options = parse_ini_file($this->_conf, true); $options = parse_ini_file(CONF, true);
$options['traffic']['header'] = 'X_FORWARDED_FOR'; $options['traffic']['header'] = 'X_FORWARDED_FOR';
if (!is_file($this->_conf . '.bak') && is_file($this->_conf)) $options['traffic']['limit'] = 100;
rename($this->_conf, $this->_conf . '.bak'); helper::confBackup();
helper::createIniFile($this->_conf, $options); helper::createIniFile(CONF, $options);
$_POST = self::$paste; $_POST = helper::getPaste();
$_SERVER['HTTP_X_FORWARDED_FOR'] = '::1'; $_SERVER['HTTP_X_FORWARDED_FOR'] = '::1';
$_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
$_SERVER['REQUEST_METHOD'] = 'POST';
ob_start(); ob_start();
new zerobin; new zerobin;
$content = ob_get_contents(); $content = ob_get_contents();
$response = json_decode($content, true); $response = json_decode($content, true);
$this->assertEquals(1, $response['status'], 'outputs error status'); $this->assertEquals(1, $response['status'], 'outputs error status');
$this->assertFalse($this->_model->exists(self::$pasteid), 'paste exists after posting data'); $this->assertFalse($this->_model->exists(helper::getPasteId()), 'paste exists after posting data');
} }
/** /**
@@ -220,20 +205,21 @@ class zerobinTest extends PHPUnit_Framework_TestCase
public function testCreateDuplicateId() public function testCreateDuplicateId()
{ {
$this->reset(); $this->reset();
$options = parse_ini_file($this->_conf, true); $options = parse_ini_file(CONF, true);
$options['traffic']['limit'] = 0; $options['traffic']['limit'] = 0;
if (!is_file($this->_conf . '.bak') && is_file($this->_conf)) helper::confBackup();
rename($this->_conf, $this->_conf . '.bak'); helper::createIniFile(CONF, $options);
helper::createIniFile($this->_conf, $options); $this->_model->create(helper::getPasteId(), helper::getPaste());
$this->_model->create(self::$pasteid, self::$paste); $_POST = helper::getPaste();
$_POST = self::$paste; $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
$_SERVER['REQUEST_METHOD'] = 'POST';
$_SERVER['REMOTE_ADDR'] = '::1'; $_SERVER['REMOTE_ADDR'] = '::1';
ob_start(); ob_start();
new zerobin; new zerobin;
$content = ob_get_contents(); $content = ob_get_contents();
$response = json_decode($content, true); $response = json_decode($content, true);
$this->assertEquals(1, $response['status'], 'outputs error status'); $this->assertEquals(1, $response['status'], 'outputs error status');
$this->assertTrue($this->_model->exists(self::$pasteid), 'paste exists after posting data'); $this->assertTrue($this->_model->exists(helper::getPasteId()), 'paste exists after posting data');
} }
/** /**
@@ -242,14 +228,15 @@ class zerobinTest extends PHPUnit_Framework_TestCase
public function testCreateValidExpire() public function testCreateValidExpire()
{ {
$this->reset(); $this->reset();
$options = parse_ini_file($this->_conf, true); $options = parse_ini_file(CONF, true);
$options['traffic']['limit'] = 0; $options['traffic']['limit'] = 0;
if (!is_file($this->_conf . '.bak') && is_file($this->_conf)) helper::confBackup();
rename($this->_conf, $this->_conf . '.bak'); helper::createIniFile(CONF, $options);
helper::createIniFile($this->_conf, $options); $_POST = helper::getPaste();
$_POST = self::$paste;
$_POST['expire'] = '5min'; $_POST['expire'] = '5min';
$_POST['formatter'] = 'foo'; $_POST['formatter'] = 'foo';
$_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
$_SERVER['REQUEST_METHOD'] = 'POST';
$_SERVER['REMOTE_ADDR'] = '::1'; $_SERVER['REMOTE_ADDR'] = '::1';
ob_start(); ob_start();
new zerobin; new zerobin;
@@ -262,6 +249,40 @@ class zerobinTest extends PHPUnit_Framework_TestCase
'outputs valid delete token' 'outputs valid delete token'
); );
$this->assertTrue($this->_model->exists($response['id']), 'paste exists after posting data'); $this->assertTrue($this->_model->exists($response['id']), 'paste exists after posting data');
$paste = $this->_model->read($response['id']);
$this->assertEquals(time() + 300, $paste->meta->expire_date, 'time is set correctly');
}
/**
* @runInSeparateProcess
*/
public function testCreateValidExpireWithDiscussion()
{
$this->reset();
$options = parse_ini_file(CONF, true);
$options['traffic']['limit'] = 0;
helper::confBackup();
helper::createIniFile(CONF, $options);
$_POST = helper::getPaste();
$_POST['expire'] = '5min';
$_POST['opendiscussion'] = '1';
$_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
$_SERVER['REQUEST_METHOD'] = 'POST';
$_SERVER['REMOTE_ADDR'] = '::1';
ob_start();
new zerobin;
$content = ob_get_contents();
$response = json_decode($content, true);
$this->assertEquals(0, $response['status'], 'outputs status');
$this->assertEquals(
hash_hmac('sha1', $response['id'], serversalt::get()),
$response['deletetoken'],
'outputs valid delete token'
);
$this->assertTrue($this->_model->exists($response['id']), 'paste exists after posting data');
$paste = $this->_model->read($response['id']);
$this->assertEquals(time() + 300, $paste->meta->expire_date, 'time is set correctly');
$this->assertEquals(1, $paste->meta->opendiscussion, 'time is set correctly');
} }
/** /**
@@ -270,13 +291,14 @@ class zerobinTest extends PHPUnit_Framework_TestCase
public function testCreateInvalidExpire() public function testCreateInvalidExpire()
{ {
$this->reset(); $this->reset();
$options = parse_ini_file($this->_conf, true); $options = parse_ini_file(CONF, true);
$options['traffic']['limit'] = 0; $options['traffic']['limit'] = 0;
if (!is_file($this->_conf . '.bak') && is_file($this->_conf)) helper::confBackup();
rename($this->_conf, $this->_conf . '.bak'); helper::createIniFile(CONF, $options);
helper::createIniFile($this->_conf, $options); $_POST = helper::getPaste();
$_POST = self::$paste;
$_POST['expire'] = 'foo'; $_POST['expire'] = 'foo';
$_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
$_SERVER['REQUEST_METHOD'] = 'POST';
$_SERVER['REMOTE_ADDR'] = '::1'; $_SERVER['REMOTE_ADDR'] = '::1';
ob_start(); ob_start();
new zerobin; new zerobin;
@@ -297,20 +319,21 @@ class zerobinTest extends PHPUnit_Framework_TestCase
public function testCreateInvalidBurn() public function testCreateInvalidBurn()
{ {
$this->reset(); $this->reset();
$options = parse_ini_file($this->_conf, true); $options = parse_ini_file(CONF, true);
$options['traffic']['limit'] = 0; $options['traffic']['limit'] = 0;
if (!is_file($this->_conf . '.bak') && is_file($this->_conf)) helper::confBackup();
rename($this->_conf, $this->_conf . '.bak'); helper::createIniFile(CONF, $options);
helper::createIniFile($this->_conf, $options); $_POST = helper::getPaste();
$_POST = self::$paste;
$_POST['burnafterreading'] = 'neither 1 nor 0'; $_POST['burnafterreading'] = 'neither 1 nor 0';
$_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
$_SERVER['REQUEST_METHOD'] = 'POST';
$_SERVER['REMOTE_ADDR'] = '::1'; $_SERVER['REMOTE_ADDR'] = '::1';
ob_start(); ob_start();
new zerobin; new zerobin;
$content = ob_get_contents(); $content = ob_get_contents();
$response = json_decode($content, true); $response = json_decode($content, true);
$this->assertEquals(1, $response['status'], 'outputs error status'); $this->assertEquals(1, $response['status'], 'outputs error status');
$this->assertFalse($this->_model->exists(self::$pasteid), 'paste exists after posting data'); $this->assertFalse($this->_model->exists(helper::getPasteId()), 'paste exists after posting data');
} }
/** /**
@@ -319,20 +342,21 @@ class zerobinTest extends PHPUnit_Framework_TestCase
public function testCreateInvalidOpenDiscussion() public function testCreateInvalidOpenDiscussion()
{ {
$this->reset(); $this->reset();
$options = parse_ini_file($this->_conf, true); $options = parse_ini_file(CONF, true);
$options['traffic']['limit'] = 0; $options['traffic']['limit'] = 0;
if (!is_file($this->_conf . '.bak') && is_file($this->_conf)) helper::confBackup();
rename($this->_conf, $this->_conf . '.bak'); helper::createIniFile(CONF, $options);
helper::createIniFile($this->_conf, $options); $_POST = helper::getPaste();
$_POST = self::$paste;
$_POST['opendiscussion'] = 'neither 1 nor 0'; $_POST['opendiscussion'] = 'neither 1 nor 0';
$_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
$_SERVER['REQUEST_METHOD'] = 'POST';
$_SERVER['REMOTE_ADDR'] = '::1'; $_SERVER['REMOTE_ADDR'] = '::1';
ob_start(); ob_start();
new zerobin; new zerobin;
$content = ob_get_contents(); $content = ob_get_contents();
$response = json_decode($content, true); $response = json_decode($content, true);
$this->assertEquals(1, $response['status'], 'outputs error status'); $this->assertEquals(1, $response['status'], 'outputs error status');
$this->assertFalse($this->_model->exists(self::$pasteid), 'paste exists after posting data'); $this->assertFalse($this->_model->exists(helper::getPasteId()), 'paste exists after posting data');
} }
/** /**
@@ -341,16 +365,16 @@ class zerobinTest extends PHPUnit_Framework_TestCase
public function testCreateAttachment() public function testCreateAttachment()
{ {
$this->reset(); $this->reset();
$options = parse_ini_file($this->_conf, true); $options = parse_ini_file(CONF, true);
$options['traffic']['limit'] = 0; $options['traffic']['limit'] = 0;
$options['main']['fileupload'] = true; $options['main']['fileupload'] = true;
if (!is_file($this->_conf . '.bak') && is_file($this->_conf)) helper::confBackup();
rename($this->_conf, $this->_conf . '.bak'); helper::createIniFile(CONF, $options);
helper::createIniFile($this->_conf, $options); $_POST = helper::getPasteWithAttachment();
$_POST = self::$paste; $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
$_POST['attachment'] = self::$comment['data']; $_SERVER['REQUEST_METHOD'] = 'POST';
$_POST['attachmentname'] = self::$comment['meta']['nickname'];
$_SERVER['REMOTE_ADDR'] = '::1'; $_SERVER['REMOTE_ADDR'] = '::1';
$this->assertFalse($this->_model->exists(helper::getPasteId()), 'paste does not exists before posting data');
ob_start(); ob_start();
new zerobin; new zerobin;
$content = ob_get_contents(); $content = ob_get_contents();
@@ -362,6 +386,11 @@ class zerobinTest extends PHPUnit_Framework_TestCase
'outputs valid delete token' 'outputs valid delete token'
); );
$this->assertTrue($this->_model->exists($response['id']), 'paste exists after posting data'); $this->assertTrue($this->_model->exists($response['id']), 'paste exists after posting data');
$original = json_decode(json_encode($_POST));
$stored = $this->_model->read($response['id']);
foreach (array('data', 'attachment', 'attachmentname') as $key) {
$this->assertEquals($original->$key, $stored->$key);
}
} }
/** /**
@@ -370,13 +399,14 @@ class zerobinTest extends PHPUnit_Framework_TestCase
public function testCreateValidNick() public function testCreateValidNick()
{ {
$this->reset(); $this->reset();
$options = parse_ini_file($this->_conf, true); $options = parse_ini_file(CONF, true);
$options['traffic']['limit'] = 0; $options['traffic']['limit'] = 0;
if (!is_file($this->_conf . '.bak') && is_file($this->_conf)) helper::confBackup();
rename($this->_conf, $this->_conf . '.bak'); helper::createIniFile(CONF, $options);
helper::createIniFile($this->_conf, $options); $_POST = helper::getPaste();
$_POST = self::$paste; $_POST['nickname'] = helper::getComment()['meta']['nickname'];
$_POST['nickname'] = self::$comment['meta']['nickname']; $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
$_SERVER['REQUEST_METHOD'] = 'POST';
$_SERVER['REMOTE_ADDR'] = '::1'; $_SERVER['REMOTE_ADDR'] = '::1';
ob_start(); ob_start();
new zerobin; new zerobin;
@@ -397,20 +427,24 @@ class zerobinTest extends PHPUnit_Framework_TestCase
public function testCreateInvalidNick() public function testCreateInvalidNick()
{ {
$this->reset(); $this->reset();
$options = parse_ini_file($this->_conf, true); $options = parse_ini_file(CONF, true);
$options['traffic']['limit'] = 0; $options['traffic']['limit'] = 0;
if (!is_file($this->_conf . '.bak') && is_file($this->_conf)) helper::confBackup();
rename($this->_conf, $this->_conf . '.bak'); helper::createIniFile(CONF, $options);
helper::createIniFile($this->_conf, $options); $_POST = helper::getCommentPost();
$_POST = self::$paste; $_POST['pasteid'] = helper::getPasteId();
$_POST['parentid'] = helper::getPasteId();
$_POST['nickname'] = 'foo'; $_POST['nickname'] = 'foo';
$_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
$_SERVER['REQUEST_METHOD'] = 'POST';
$_SERVER['REMOTE_ADDR'] = '::1'; $_SERVER['REMOTE_ADDR'] = '::1';
$this->_model->create(helper::getPasteId(), helper::getPaste());
ob_start(); ob_start();
new zerobin; new zerobin;
$content = ob_get_contents(); $content = ob_get_contents();
$response = json_decode($content, true); $response = json_decode($content, true);
$this->assertEquals(1, $response['status'], 'outputs error status'); $this->assertEquals(1, $response['status'], 'outputs error status');
$this->assertFalse($this->_model->exists(self::$pasteid), 'paste exists after posting data'); $this->assertTrue($this->_model->exists(helper::getPasteId()), 'paste exists after posting data');
} }
/** /**
@@ -419,22 +453,23 @@ class zerobinTest extends PHPUnit_Framework_TestCase
public function testCreateComment() public function testCreateComment()
{ {
$this->reset(); $this->reset();
$options = parse_ini_file($this->_conf, true); $options = parse_ini_file(CONF, true);
$options['traffic']['limit'] = 0; $options['traffic']['limit'] = 0;
if (!is_file($this->_conf . '.bak') && is_file($this->_conf)) helper::confBackup();
rename($this->_conf, $this->_conf . '.bak'); helper::createIniFile(CONF, $options);
helper::createIniFile($this->_conf, $options); $_POST = helper::getCommentPost();
$_POST = self::$comment; $_POST['pasteid'] = helper::getPasteId();
$_POST['pasteid'] = self::$pasteid; $_POST['parentid'] = helper::getPasteId();
$_POST['parentid'] = self::$pasteid; $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
$_SERVER['REQUEST_METHOD'] = 'POST';
$_SERVER['REMOTE_ADDR'] = '::1'; $_SERVER['REMOTE_ADDR'] = '::1';
$this->_model->create(self::$pasteid, self::$paste); $this->_model->create(helper::getPasteId(), helper::getPaste());
ob_start(); ob_start();
new zerobin; new zerobin;
$content = ob_get_contents(); $content = ob_get_contents();
$response = json_decode($content, true); $response = json_decode($content, true);
$this->assertEquals(0, $response['status'], 'outputs status'); $this->assertEquals(0, $response['status'], 'outputs status');
$this->assertTrue($this->_model->existsComment(self::$pasteid, self::$pasteid, $response['id']), 'paste exists after posting data'); $this->assertTrue($this->_model->existsComment(helper::getPasteId(), helper::getPasteId(), $response['id']), 'paste exists after posting data');
} }
/** /**
@@ -443,22 +478,23 @@ class zerobinTest extends PHPUnit_Framework_TestCase
public function testCreateInvalidComment() public function testCreateInvalidComment()
{ {
$this->reset(); $this->reset();
$options = parse_ini_file($this->_conf, true); $options = parse_ini_file(CONF, true);
$options['traffic']['limit'] = 0; $options['traffic']['limit'] = 0;
if (!is_file($this->_conf . '.bak') && is_file($this->_conf)) helper::confBackup();
rename($this->_conf, $this->_conf . '.bak'); helper::createIniFile(CONF, $options);
helper::createIniFile($this->_conf, $options); $_POST = helper::getCommentPost();
$_POST = self::$comment; $_POST['pasteid'] = helper::getPasteId();
$_POST['pasteid'] = self::$pasteid;
$_POST['parentid'] = 'foo'; $_POST['parentid'] = 'foo';
$_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
$_SERVER['REQUEST_METHOD'] = 'POST';
$_SERVER['REMOTE_ADDR'] = '::1'; $_SERVER['REMOTE_ADDR'] = '::1';
$this->_model->create(self::$pasteid, self::$paste); $this->_model->create(helper::getPasteId(), helper::getPaste());
ob_start(); ob_start();
new zerobin; new zerobin;
$content = ob_get_contents(); $content = ob_get_contents();
$response = json_decode($content, true); $response = json_decode($content, true);
$this->assertEquals(1, $response['status'], 'outputs error status'); $this->assertEquals(1, $response['status'], 'outputs error status');
$this->assertFalse($this->_model->existsComment(self::$pasteid, self::$pasteid, self::$commentid), 'paste exists after posting data'); $this->assertFalse($this->_model->existsComment(helper::getPasteId(), helper::getPasteId(), helper::getCommentId()), 'paste exists after posting data');
} }
/** /**
@@ -467,24 +503,24 @@ class zerobinTest extends PHPUnit_Framework_TestCase
public function testCreateCommentDiscussionDisabled() public function testCreateCommentDiscussionDisabled()
{ {
$this->reset(); $this->reset();
$options = parse_ini_file($this->_conf, true); $options = parse_ini_file(CONF, true);
$options['traffic']['limit'] = 0; $options['traffic']['limit'] = 0;
if (!is_file($this->_conf . '.bak') && is_file($this->_conf)) helper::confBackup();
rename($this->_conf, $this->_conf . '.bak'); helper::createIniFile(CONF, $options);
helper::createIniFile($this->_conf, $options); $_POST = helper::getCommentPost();
$_POST = self::$comment; $_POST['pasteid'] = helper::getPasteId();
$_POST['pasteid'] = self::$pasteid; $_POST['parentid'] = helper::getPasteId();
$_POST['parentid'] = self::$pasteid; $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
$_SERVER['REQUEST_METHOD'] = 'POST';
$_SERVER['REMOTE_ADDR'] = '::1'; $_SERVER['REMOTE_ADDR'] = '::1';
$paste = self::$paste; $paste = helper::getPaste(array('opendiscussion' => false));
$paste['meta']['opendiscussion'] = false; $this->_model->create(helper::getPasteId(), $paste);
$this->_model->create(self::$pasteid, $paste);
ob_start(); ob_start();
new zerobin; new zerobin;
$content = ob_get_contents(); $content = ob_get_contents();
$response = json_decode($content, true); $response = json_decode($content, true);
$this->assertEquals(1, $response['status'], 'outputs error status'); $this->assertEquals(1, $response['status'], 'outputs error status');
$this->assertFalse($this->_model->existsComment(self::$pasteid, self::$pasteid, self::$commentid), 'paste exists after posting data'); $this->assertFalse($this->_model->existsComment(helper::getPasteId(), helper::getPasteId(), helper::getCommentId()), 'paste exists after posting data');
} }
/** /**
@@ -493,21 +529,22 @@ class zerobinTest extends PHPUnit_Framework_TestCase
public function testCreateCommentInvalidPaste() public function testCreateCommentInvalidPaste()
{ {
$this->reset(); $this->reset();
$options = parse_ini_file($this->_conf, true); $options = parse_ini_file(CONF, true);
$options['traffic']['limit'] = 0; $options['traffic']['limit'] = 0;
if (!is_file($this->_conf . '.bak') && is_file($this->_conf)) helper::confBackup();
rename($this->_conf, $this->_conf . '.bak'); helper::createIniFile(CONF, $options);
helper::createIniFile($this->_conf, $options); $_POST = helper::getCommentPost();
$_POST = self::$comment; $_POST['pasteid'] = helper::getPasteId();
$_POST['pasteid'] = self::$pasteid; $_POST['parentid'] = helper::getPasteId();
$_POST['parentid'] = self::$pasteid; $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
$_SERVER['REQUEST_METHOD'] = 'POST';
$_SERVER['REMOTE_ADDR'] = '::1'; $_SERVER['REMOTE_ADDR'] = '::1';
ob_start(); ob_start();
new zerobin; new zerobin;
$content = ob_get_contents(); $content = ob_get_contents();
$response = json_decode($content, true); $response = json_decode($content, true);
$this->assertEquals(1, $response['status'], 'outputs error status'); $this->assertEquals(1, $response['status'], 'outputs error status');
$this->assertFalse($this->_model->existsComment(self::$pasteid, self::$pasteid, self::$commentid), 'paste exists after posting data'); $this->assertFalse($this->_model->existsComment(helper::getPasteId(), helper::getPasteId(), helper::getCommentId()), 'paste exists after posting data');
} }
/** /**
@@ -516,24 +553,25 @@ class zerobinTest extends PHPUnit_Framework_TestCase
public function testCreateDuplicateComment() public function testCreateDuplicateComment()
{ {
$this->reset(); $this->reset();
$options = parse_ini_file($this->_conf, true); $options = parse_ini_file(CONF, true);
$options['traffic']['limit'] = 0; $options['traffic']['limit'] = 0;
if (!is_file($this->_conf . '.bak') && is_file($this->_conf)) helper::confBackup();
rename($this->_conf, $this->_conf . '.bak'); helper::createIniFile(CONF, $options);
helper::createIniFile($this->_conf, $options); $this->_model->create(helper::getPasteId(), helper::getPaste());
$this->_model->create(self::$pasteid, self::$paste); $this->_model->createComment(helper::getPasteId(), helper::getPasteId(), helper::getCommentId(), helper::getComment());
$this->_model->createComment(self::$pasteid, self::$pasteid, self::$commentid, self::$comment); $this->assertTrue($this->_model->existsComment(helper::getPasteId(), helper::getPasteId(), helper::getCommentId()), 'comment exists before posting data');
$this->assertTrue($this->_model->existsComment(self::$pasteid, self::$pasteid, self::$commentid), 'comment exists before posting data'); $_POST = helper::getCommentPost();
$_POST = self::$comment; $_POST['pasteid'] = helper::getPasteId();
$_POST['pasteid'] = self::$pasteid; $_POST['parentid'] = helper::getPasteId();
$_POST['parentid'] = self::$pasteid; $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
$_SERVER['REQUEST_METHOD'] = 'POST';
$_SERVER['REMOTE_ADDR'] = '::1'; $_SERVER['REMOTE_ADDR'] = '::1';
ob_start(); ob_start();
new zerobin; new zerobin;
$content = ob_get_contents(); $content = ob_get_contents();
$response = json_decode($content, true); $response = json_decode($content, true);
$this->assertEquals(1, $response['status'], 'outputs error status'); $this->assertEquals(1, $response['status'], 'outputs error status');
$this->assertTrue($this->_model->existsComment(self::$pasteid, self::$pasteid, self::$commentid), 'paste exists after posting data'); $this->assertTrue($this->_model->existsComment(helper::getPasteId(), helper::getPasteId(), helper::getCommentId()), 'paste exists after posting data');
} }
/** /**
@@ -542,15 +580,15 @@ class zerobinTest extends PHPUnit_Framework_TestCase
public function testRead() public function testRead()
{ {
$this->reset(); $this->reset();
$this->_model->create(self::$pasteid, self::$paste); $this->_model->create(helper::getPasteId(), helper::getPaste());
$_SERVER['QUERY_STRING'] = self::$pasteid; $_SERVER['QUERY_STRING'] = helper::getPasteId();
ob_start(); ob_start();
new zerobin; new zerobin;
$content = ob_get_contents(); $content = ob_get_contents();
$this->assertTag( $this->assertTag(
array( array(
'id' => 'cipherdata', 'id' => 'cipherdata',
'content' => htmlspecialchars(json_encode(self::$paste), ENT_NOQUOTES) 'content' => htmlspecialchars(helper::getPasteAsJson(), ENT_NOQUOTES)
), ),
$content, $content,
'outputs data correctly' 'outputs data correctly'
@@ -583,7 +621,7 @@ class zerobinTest extends PHPUnit_Framework_TestCase
public function testReadNonexisting() public function testReadNonexisting()
{ {
$this->reset(); $this->reset();
$_SERVER['QUERY_STRING'] = self::$pasteid; $_SERVER['QUERY_STRING'] = helper::getPasteId();
ob_start(); ob_start();
new zerobin; new zerobin;
$content = ob_get_contents(); $content = ob_get_contents();
@@ -603,10 +641,9 @@ class zerobinTest extends PHPUnit_Framework_TestCase
public function testReadExpired() public function testReadExpired()
{ {
$this->reset(); $this->reset();
$expiredPaste = self::$paste; $expiredPaste = helper::getPaste(array('expire_date' => 1344803344));
$expiredPaste['meta']['expire_date'] = $expiredPaste['meta']['postdate']; $this->_model->create(helper::getPasteId(), $expiredPaste);
$this->_model->create(self::$pasteid, $expiredPaste); $_SERVER['QUERY_STRING'] = helper::getPasteId();
$_SERVER['QUERY_STRING'] = self::$pasteid;
ob_start(); ob_start();
new zerobin; new zerobin;
$content = ob_get_contents(); $content = ob_get_contents();
@@ -626,17 +663,16 @@ class zerobinTest extends PHPUnit_Framework_TestCase
public function testReadBurn() public function testReadBurn()
{ {
$this->reset(); $this->reset();
$burnPaste = self::$paste; $burnPaste = helper::getPaste(array('burnafterreading' => true));
$burnPaste['meta']['burnafterreading'] = true; $this->_model->create(helper::getPasteId(), $burnPaste);
$this->_model->create(self::$pasteid, $burnPaste); $_SERVER['QUERY_STRING'] = helper::getPasteId();
$_SERVER['QUERY_STRING'] = self::$pasteid;
ob_start(); ob_start();
new zerobin; new zerobin;
$content = ob_get_contents(); $content = ob_get_contents();
$this->assertTag( $this->assertTag(
array( array(
'id' => 'cipherdata', 'id' => 'cipherdata',
'content' => htmlspecialchars(json_encode($burnPaste), ENT_NOQUOTES) 'content' => htmlspecialchars(helper::getPasteAsJson($burnPaste['meta']), ENT_NOQUOTES)
), ),
$content, $content,
'outputs data correctly' 'outputs data correctly'
@@ -649,14 +685,23 @@ class zerobinTest extends PHPUnit_Framework_TestCase
public function testReadJson() public function testReadJson()
{ {
$this->reset(); $this->reset();
$this->_model->create(self::$pasteid, self::$paste); $paste = helper::getPaste();
$_SERVER['QUERY_STRING'] = self::$pasteid . '&json'; $this->_model->create(helper::getPasteId(), $paste);
$_SERVER['QUERY_STRING'] = helper::getPasteId();
$_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
ob_start(); ob_start();
new zerobin; new zerobin;
$content = ob_get_contents(); $content = ob_get_contents();
$response = json_decode($content, true); $response = json_decode($content, true);
$this->assertEquals(0, $response['status'], 'outputs success status'); $this->assertEquals(0, $response['status'], 'outputs success status');
$this->assertEquals(array(self::$paste), $response['messages'], 'outputs data correctly'); $this->assertEquals(helper::getPasteId(), $response['id'], 'outputs data correctly');
$this->assertStringEndsWith('?' . $response['id'], $response['url'], 'returned URL points to new paste');
$this->assertEquals($paste['data'], $response['data'], 'outputs data correctly');
$this->assertEquals($paste['meta']['formatter'], $response['meta']['formatter'], 'outputs format correctly');
$this->assertEquals($paste['meta']['postdate'], $response['meta']['postdate'], 'outputs postdate correctly');
$this->assertEquals($paste['meta']['opendiscussion'], $response['meta']['opendiscussion'], 'outputs opendiscussion correctly');
$this->assertEquals(0, $response['comment_count'], 'outputs comment_count correctly');
$this->assertEquals(0, $response['comment_offset'], 'outputs comment_offset correctly');
} }
/** /**
@@ -665,7 +710,8 @@ class zerobinTest extends PHPUnit_Framework_TestCase
public function testReadInvalidJson() public function testReadInvalidJson()
{ {
$this->reset(); $this->reset();
$_SERVER['QUERY_STRING'] = self::$pasteid . '&json'; $_SERVER['QUERY_STRING'] = helper::getPasteId();
$_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
ob_start(); ob_start();
new zerobin; new zerobin;
$content = ob_get_contents(); $content = ob_get_contents();
@@ -679,19 +725,23 @@ class zerobinTest extends PHPUnit_Framework_TestCase
public function testReadOldSyntax() public function testReadOldSyntax()
{ {
$this->reset(); $this->reset();
$oldPaste = self::$paste; $oldPaste = helper::getPaste();
$oldPaste['meta']['syntaxcoloring'] = true; $meta = array(
unset($oldPaste['meta']['formatter']); 'syntaxcoloring' => true,
$this->_model->create(self::$pasteid, $oldPaste); 'postdate' => $oldPaste['meta']['postdate'],
$_SERVER['QUERY_STRING'] = self::$pasteid; 'opendiscussion' => $oldPaste['meta']['opendiscussion'],
);
$oldPaste['meta'] = $meta;
$this->_model->create(helper::getPasteId(), $oldPaste);
$_SERVER['QUERY_STRING'] = helper::getPasteId();
ob_start(); ob_start();
new zerobin; new zerobin;
$content = ob_get_contents(); $content = ob_get_contents();
$oldPaste['meta']['formatter'] = 'syntaxhighlighting'; $meta['formatter'] = 'syntaxhighlighting';
$this->assertTag( $this->assertTag(
array( array(
'id' => 'cipherdata', 'id' => 'cipherdata',
'content' => htmlspecialchars(json_encode($oldPaste), ENT_NOQUOTES) 'content' => htmlspecialchars(helper::getPasteAsJson($meta), ENT_NOQUOTES)
), ),
$content, $content,
'outputs data correctly' 'outputs data correctly'
@@ -704,10 +754,10 @@ class zerobinTest extends PHPUnit_Framework_TestCase
public function testReadOldFormat() public function testReadOldFormat()
{ {
$this->reset(); $this->reset();
$oldPaste = self::$paste; $oldPaste = helper::getPaste();
unset($oldPaste['meta']['formatter']); unset($oldPaste['meta']['formatter']);
$this->_model->create(self::$pasteid, $oldPaste); $this->_model->create(helper::getPasteId(), $oldPaste);
$_SERVER['QUERY_STRING'] = self::$pasteid; $_SERVER['QUERY_STRING'] = helper::getPasteId();
ob_start(); ob_start();
new zerobin; new zerobin;
$content = ob_get_contents(); $content = ob_get_contents();
@@ -715,7 +765,7 @@ class zerobinTest extends PHPUnit_Framework_TestCase
$this->assertTag( $this->assertTag(
array( array(
'id' => 'cipherdata', 'id' => 'cipherdata',
'content' => htmlspecialchars(json_encode($oldPaste), ENT_NOQUOTES) 'content' => htmlspecialchars(helper::getPasteAsJson($oldPaste['meta']), ENT_NOQUOTES)
), ),
$content, $content,
'outputs data correctly' 'outputs data correctly'
@@ -728,10 +778,10 @@ class zerobinTest extends PHPUnit_Framework_TestCase
public function testDelete() public function testDelete()
{ {
$this->reset(); $this->reset();
$this->_model->create(self::$pasteid, self::$paste); $this->_model->create(helper::getPasteId(), helper::getPaste());
$this->assertTrue($this->_model->exists(self::$pasteid), 'paste exists before deleting data'); $this->assertTrue($this->_model->exists(helper::getPasteId()), 'paste exists before deleting data');
$_GET['pasteid'] = self::$pasteid; $_GET['pasteid'] = helper::getPasteId();
$_GET['deletetoken'] = hash_hmac('sha1', self::$pasteid, serversalt::get()); $_GET['deletetoken'] = hash_hmac('sha1', helper::getPasteId(), serversalt::get());
ob_start(); ob_start();
new zerobin; new zerobin;
$content = ob_get_contents(); $content = ob_get_contents();
@@ -743,7 +793,7 @@ class zerobinTest extends PHPUnit_Framework_TestCase
$content, $content,
'outputs deleted status correctly' 'outputs deleted status correctly'
); );
$this->assertFalse($this->_model->exists(self::$pasteid), 'paste successfully deleted'); $this->assertFalse($this->_model->exists(helper::getPasteId()), 'paste successfully deleted');
} }
/** /**
@@ -752,7 +802,7 @@ class zerobinTest extends PHPUnit_Framework_TestCase
public function testDeleteInvalidId() public function testDeleteInvalidId()
{ {
$this->reset(); $this->reset();
$this->_model->create(self::$pasteid, self::$paste); $this->_model->create(helper::getPasteId(), helper::getPaste());
$_GET['pasteid'] = 'foo'; $_GET['pasteid'] = 'foo';
$_GET['deletetoken'] = 'bar'; $_GET['deletetoken'] = 'bar';
ob_start(); ob_start();
@@ -766,7 +816,7 @@ class zerobinTest extends PHPUnit_Framework_TestCase
$content, $content,
'outputs delete error correctly' 'outputs delete error correctly'
); );
$this->assertTrue($this->_model->exists(self::$pasteid), 'paste exists after failing to delete data'); $this->assertTrue($this->_model->exists(helper::getPasteId()), 'paste exists after failing to delete data');
} }
/** /**
@@ -775,7 +825,7 @@ class zerobinTest extends PHPUnit_Framework_TestCase
public function testDeleteInexistantId() public function testDeleteInexistantId()
{ {
$this->reset(); $this->reset();
$_GET['pasteid'] = self::$pasteid; $_GET['pasteid'] = helper::getPasteId();
$_GET['deletetoken'] = 'bar'; $_GET['deletetoken'] = 'bar';
ob_start(); ob_start();
new zerobin; new zerobin;
@@ -796,8 +846,8 @@ class zerobinTest extends PHPUnit_Framework_TestCase
public function testDeleteInvalidToken() public function testDeleteInvalidToken()
{ {
$this->reset(); $this->reset();
$this->_model->create(self::$pasteid, self::$paste); $this->_model->create(helper::getPasteId(), helper::getPaste());
$_GET['pasteid'] = self::$pasteid; $_GET['pasteid'] = helper::getPasteId();
$_GET['deletetoken'] = 'bar'; $_GET['deletetoken'] = 'bar';
ob_start(); ob_start();
new zerobin; new zerobin;
@@ -810,7 +860,7 @@ class zerobinTest extends PHPUnit_Framework_TestCase
$content, $content,
'outputs delete error correctly' 'outputs delete error correctly'
); );
$this->assertTrue($this->_model->exists(self::$pasteid), 'paste exists after failing to delete data'); $this->assertTrue($this->_model->exists(helper::getPasteId()), 'paste exists after failing to delete data');
} }
/** /**
@@ -819,18 +869,19 @@ class zerobinTest extends PHPUnit_Framework_TestCase
public function testDeleteBurnAfterReading() public function testDeleteBurnAfterReading()
{ {
$this->reset(); $this->reset();
$burnPaste = self::$paste; $burnPaste = helper::getPaste(array('burnafterreading' => true));
$burnPaste['meta']['burnafterreading'] = true; $this->_model->create(helper::getPasteId(), $burnPaste);
$this->_model->create(self::$pasteid, $burnPaste); $this->assertTrue($this->_model->exists(helper::getPasteId()), 'paste exists before deleting data');
$this->assertTrue($this->_model->exists(self::$pasteid), 'paste exists before deleting data'); $_POST['deletetoken'] = 'burnafterreading';
$_GET['pasteid'] = self::$pasteid; $_SERVER['QUERY_STRING'] = helper::getPasteId();
$_GET['deletetoken'] = 'burnafterreading'; $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
$_SERVER['REQUEST_METHOD'] = 'POST';
ob_start(); ob_start();
new zerobin; new zerobin;
$content = ob_get_contents(); $content = ob_get_contents();
$response = json_decode($content, true); $response = json_decode($content, true);
$this->assertEquals(0, $response['status'], 'outputs status'); $this->assertEquals(0, $response['status'], 'outputs status');
$this->assertFalse($this->_model->exists(self::$pasteid), 'paste successfully deleted'); $this->assertFalse($this->_model->exists(helper::getPasteId()), 'paste successfully deleted');
} }
/** /**
@@ -839,16 +890,18 @@ class zerobinTest extends PHPUnit_Framework_TestCase
public function testDeleteInvalidBurnAfterReading() public function testDeleteInvalidBurnAfterReading()
{ {
$this->reset(); $this->reset();
$this->_model->create(self::$pasteid, self::$paste); $this->_model->create(helper::getPasteId(), helper::getPaste());
$this->assertTrue($this->_model->exists(self::$pasteid), 'paste exists before deleting data'); $this->assertTrue($this->_model->exists(helper::getPasteId()), 'paste exists before deleting data');
$_GET['pasteid'] = self::$pasteid; $_POST['deletetoken'] = 'burnafterreading';
$_GET['deletetoken'] = 'burnafterreading'; $_SERVER['QUERY_STRING'] = helper::getPasteId();
$_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
$_SERVER['REQUEST_METHOD'] = 'POST';
ob_start(); ob_start();
new zerobin; new zerobin;
$content = ob_get_contents(); $content = ob_get_contents();
$response = json_decode($content, true); $response = json_decode($content, true);
$this->assertEquals(1, $response['status'], 'outputs status'); $this->assertEquals(1, $response['status'], 'outputs status');
$this->assertTrue($this->_model->exists(self::$pasteid), 'paste successfully deleted'); $this->assertTrue($this->_model->exists(helper::getPasteId()), 'paste successfully deleted');
} }
/** /**
@@ -857,12 +910,11 @@ class zerobinTest extends PHPUnit_Framework_TestCase
public function testDeleteExpired() public function testDeleteExpired()
{ {
$this->reset(); $this->reset();
$expiredPaste = self::$paste; $expiredPaste = helper::getPaste(array('expire_date' => 1000));
$expiredPaste['meta']['expire_date'] = 1000; $this->assertFalse($this->_model->exists(helper::getPasteId()), 'paste does not exist before being created');
$this->assertFalse($this->_model->exists(self::$pasteid), 'paste does not exist before being created'); $this->_model->create(helper::getPasteId(), $expiredPaste);
$this->_model->create(self::$pasteid, $expiredPaste); $this->assertTrue($this->_model->exists(helper::getPasteId()), 'paste exists before deleting data');
$this->assertTrue($this->_model->exists(self::$pasteid), 'paste exists before deleting data'); $_GET['pasteid'] = helper::getPasteId();
$_GET['pasteid'] = self::$pasteid;
$_GET['deletetoken'] = 'does not matter in this context, but has to be set'; $_GET['deletetoken'] = 'does not matter in this context, but has to be set';
ob_start(); ob_start();
new zerobin; new zerobin;
@@ -875,6 +927,6 @@ class zerobinTest extends PHPUnit_Framework_TestCase
$content, $content,
'outputs error correctly' 'outputs error correctly'
); );
$this->assertFalse($this->_model->exists(self::$pasteid), 'paste successfully deleted'); $this->assertFalse($this->_model->exists(helper::getPasteId()), 'paste successfully deleted');
} }
} }

View File

@@ -1,28 +1,6 @@
<?php <?php
class zerobin_dataTest extends PHPUnit_Framework_TestCase class zerobin_dataTest extends PHPUnit_Framework_TestCase
{ {
private static $pasteid = '5e9bc25c89fb3bf9';
private static $paste = array(
'data' => '{"iv":"EN39/wd5Nk8HAiSG2K5AsQ","v":1,"iter":1000,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"QKN1DBXe5PI","ct":"8hA83xDdXjD7K2qfmw5NdA"}',
'meta' => array(
'postdate' => 1344803344,
'expire_date' => 1344803644,
'opendiscussion' => true,
),
);
private static $commentid = '5a52eebf11c4c94b';
private static $comment = array(
'data' => '{"iv":"Pd4pOKWkmDTT9uPwVwd5Ag","v":1,"iter":1000,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"ZIUhFTliVz4","ct":"6nOCU3peNDclDDpFtJEBKA"}',
'meta' => array(
'nickname' => '{"iv":"76MkAtOGC4oFogX/aSMxRA","v":1,"iter":1000,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","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 $_model;
private $_path; private $_path;
@@ -42,29 +20,47 @@ class zerobin_dataTest extends PHPUnit_Framework_TestCase
public function testFileBasedDataStoreWorks() public function testFileBasedDataStoreWorks()
{ {
$this->_model->delete(helper::getPasteId());
// storing pastes // storing pastes
$this->assertFalse($this->_model->exists(self::$pasteid), 'paste does not yet exist'); $paste = helper::getPaste(array('expire_date' => 1344803344));
$this->assertTrue($this->_model->create(self::$pasteid, self::$paste), 'store new paste'); $this->assertFalse($this->_model->exists(helper::getPasteId()), 'paste does not yet exist');
$this->assertTrue($this->_model->exists(self::$pasteid), 'paste exists after storing it'); $this->assertTrue($this->_model->create(helper::getPasteId(), $paste), 'store new paste');
$this->assertFalse($this->_model->create(self::$pasteid, self::$paste), 'unable to store the same paste twice'); $this->assertTrue($this->_model->exists(helper::getPasteId()), 'paste exists after storing it');
$this->assertEquals(json_decode(json_encode(self::$paste)), $this->_model->read(self::$pasteid)); $this->assertFalse($this->_model->create(helper::getPasteId(), $paste), 'unable to store the same paste twice');
$this->assertEquals(json_decode(json_encode($paste)), $this->_model->read(helper::getPasteId()));
// storing comments // storing comments
$this->assertFalse($this->_model->existsComment(self::$pasteid, self::$pasteid, self::$commentid), 'comment does not yet exist'); $this->assertFalse($this->_model->existsComment(helper::getPasteId(), helper::getPasteId(), helper::getCommentId()), '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->createComment(helper::getPasteId(), helper::getPasteId(), helper::getCommentId(), helper::getComment()) !== false, 'store comment');
$this->assertTrue($this->_model->existsComment(self::$pasteid, self::$pasteid, self::$commentid), 'comment exists after storing it'); $this->assertTrue($this->_model->existsComment(helper::getPasteId(), helper::getPasteId(), helper::getCommentId()), 'comment exists after storing it');
$comment = json_decode(json_encode(self::$comment)); $comment = json_decode(json_encode(helper::getComment()));
$comment->meta->commentid = self::$commentid; $comment->id = helper::getCommentId();
$comment->meta->parentid = self::$pasteid; $comment->parentid = helper::getPasteId();
$this->assertEquals( $this->assertEquals(
array($comment->meta->postdate => $comment), array($comment->meta->postdate => $comment),
$this->_model->readComments(self::$pasteid) $this->_model->readComments(helper::getPasteId())
); );
// deleting pastes // deleting pastes
$this->_model->delete(self::$pasteid); $this->_model->delete(helper::getPasteId());
$this->assertFalse($this->_model->exists(self::$pasteid), 'paste successfully deleted'); $this->assertFalse($this->_model->exists(helper::getPasteId()), 'paste successfully deleted');
$this->assertFalse($this->_model->existsComment(self::$pasteid, self::$pasteid, self::$commentid), 'comment was deleted with paste'); $this->assertFalse($this->_model->existsComment(helper::getPasteId(), helper::getPasteId(), helper::getCommentId()), 'comment was deleted with paste');
$this->assertFalse($this->_model->read(self::$pasteid), 'paste can no longer be found'); $this->assertFalse($this->_model->read(helper::getPasteId()), 'paste can no longer be found');
} }
public function testFileBasedAttachmentStoreWorks()
{
$this->_model->delete(helper::getPasteId());
$original = $paste = helper::getPasteWithAttachment(array('expire_date' => 1344803344));
$paste['meta']['attachment'] = $paste['attachment'];
$paste['meta']['attachmentname'] = $paste['attachmentname'];
unset($paste['attachment'], $paste['attachmentname']);
$this->assertFalse($this->_model->exists(helper::getPasteId()), 'paste does not yet exist');
$this->assertTrue($this->_model->create(helper::getPasteId(), $paste), 'store new paste');
$this->assertTrue($this->_model->exists(helper::getPasteId()), 'paste exists after storing it');
$this->assertFalse($this->_model->create(helper::getPasteId(), $paste), 'unable to store the same paste twice');
$this->assertEquals(json_decode(json_encode($original)), $this->_model->read(helper::getPasteId()));
}
} }

View File

@@ -1,69 +1,65 @@
<?php <?php
class zerobin_dbTest extends PHPUnit_Framework_TestCase class zerobin_dbTest extends PHPUnit_Framework_TestCase
{ {
private static $pasteid = '5e9bc25c89fb3bf9';
private static $paste = array(
'data' => '{"iv":"EN39/wd5Nk8HAiSG2K5AsQ","v":1,"iter":1000,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"QKN1DBXe5PI","ct":"8hA83xDdXjD7K2qfmw5NdA"}',
'meta' => array(
'postdate' => 1344803344,
'expire_date' => 1344803644,
'opendiscussion' => true,
),
);
private static $commentid = '5a52eebf11c4c94b';
private static $comment = array(
'data' => '{"iv":"Pd4pOKWkmDTT9uPwVwd5Ag","v":1,"iter":1000,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"ZIUhFTliVz4","ct":"6nOCU3peNDclDDpFtJEBKA"}',
'meta' => array(
'nickname' => '{"iv":"76MkAtOGC4oFogX/aSMxRA","v":1,"iter":1000,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","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 $_model;
private $_options = array(
'dsn' => 'sqlite::memory:',
'usr' => null,
'pwd' => null,
'opt' => array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION),
);
public function setUp() public function setUp()
{ {
/* Setup Routine */ /* Setup Routine */
$this->_model = zerobin_db::getInstance( $this->_model = zerobin_db::getInstance($this->_options);
array(
'dsn' => 'sqlite::memory:',
'usr' => null,
'pwd' => null,
'opt' => array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION),
)
);
} }
public function testDatabaseBasedDataStoreWorks() public function testDatabaseBasedDataStoreWorks()
{ {
$this->_model->delete(helper::getPasteId());
// storing pastes // storing pastes
$this->assertFalse($this->_model->exists(self::$pasteid), 'paste does not yet exist'); $paste = helper::getPaste(array('expire_date' => 1344803344));
$this->assertTrue($this->_model->create(self::$pasteid, self::$paste), 'store new paste'); $this->assertFalse($this->_model->exists(helper::getPasteId()), 'paste does not yet exist');
$this->assertTrue($this->_model->exists(self::$pasteid), 'paste exists after storing it'); $this->assertTrue($this->_model->create(helper::getPasteId(), $paste), 'store new paste');
$this->assertFalse($this->_model->create(self::$pasteid, self::$paste), 'unable to store the same paste twice'); $this->assertTrue($this->_model->exists(helper::getPasteId()), 'paste exists after storing it');
$this->assertEquals(json_decode(json_encode(self::$paste)), $this->_model->read(self::$pasteid)); $this->assertFalse($this->_model->create(helper::getPasteId(), $paste), 'unable to store the same paste twice');
$this->assertEquals(json_decode(json_encode($paste)), $this->_model->read(helper::getPasteId()));
// storing comments // storing comments
$this->assertFalse($this->_model->existsComment(self::$pasteid, self::$pasteid, self::$commentid), 'comment does not yet exist'); $this->assertFalse($this->_model->existsComment(helper::getPasteId(), helper::getPasteId(), helper::getCommentId()), '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->createComment(helper::getPasteId(), helper::getPasteId(), helper::getCommentId(), helper::getComment()) !== false, 'store comment');
$this->assertTrue($this->_model->existsComment(self::$pasteid, self::$pasteid, self::$commentid), 'comment exists after storing it'); $this->assertTrue($this->_model->existsComment(helper::getPasteId(), helper::getPasteId(), helper::getCommentId()), 'comment exists after storing it');
$comment = json_decode(json_encode(self::$comment)); $comment = json_decode(json_encode(helper::getComment()));
$comment->meta->commentid = self::$commentid; $comment->id = helper::getCommentId();
$comment->meta->parentid = self::$pasteid; $comment->parentid = helper::getPasteId();
$this->assertEquals( $this->assertEquals(
array($comment->meta->postdate => $comment), array($comment->meta->postdate => $comment),
$this->_model->readComments(self::$pasteid) $this->_model->readComments(helper::getPasteId())
); );
// deleting pastes // deleting pastes
$this->_model->delete(self::$pasteid); $this->_model->delete(helper::getPasteId());
$this->assertFalse($this->_model->exists(self::$pasteid), 'paste successfully deleted'); $this->assertFalse($this->_model->exists(helper::getPasteId()), 'paste successfully deleted');
$this->assertFalse($this->_model->existsComment(self::$pasteid, self::$pasteid, self::$commentid), 'comment was deleted with paste'); $this->assertFalse($this->_model->existsComment(helper::getPasteId(), helper::getPasteId(), helper::getCommentId()), 'comment was deleted with paste');
$this->assertFalse($this->_model->read(self::$pasteid), 'paste can no longer be found'); $this->assertFalse($this->_model->read(helper::getPasteId()), 'paste can no longer be found');
}
public function testDatabaseBasedAttachmentStoreWorks()
{
$this->_model->delete(helper::getPasteId());
$original = $paste = helper::getPasteWithAttachment(array('expire_date' => 1344803344));
$paste['meta']['burnafterreading'] = $original['meta']['burnafterreading'] = true;
$paste['meta']['attachment'] = $paste['attachment'];
$paste['meta']['attachmentname'] = $paste['attachmentname'];
unset($paste['attachment'], $paste['attachmentname']);
$this->assertFalse($this->_model->exists(helper::getPasteId()), 'paste does not yet exist');
$this->assertTrue($this->_model->create(helper::getPasteId(), $paste), 'store new paste');
$this->assertTrue($this->_model->exists(helper::getPasteId()), 'paste exists after storing it');
$this->assertFalse($this->_model->create(helper::getPasteId(), $paste), 'unable to store the same paste twice');
$this->assertEquals(json_decode(json_encode($original)), $this->_model->read(helper::getPasteId()));
} }
/** /**
@@ -142,4 +138,29 @@ class zerobin_dbTest extends PHPUnit_Framework_TestCase
'dsn' => 'foo:', 'usr' => null, 'pwd' => null, 'opt' => null 'dsn' => 'foo:', 'usr' => null, 'pwd' => null, 'opt' => null
)); ));
} }
public function testTableUpgrade()
{
$path = PATH . 'data/db-test.sq3';
@unlink($path);
$this->_options['dsn'] = 'sqlite:' . $path;
$this->_options['tbl'] = 'foo_';
$db = new PDO(
$this->_options['dsn'],
$this->_options['usr'],
$this->_options['pwd'],
$this->_options['opt']
);
$db->exec(
'CREATE TABLE foo_paste ( ' .
'dataid CHAR(16), ' .
'data TEXT, ' .
'postdate INT, ' .
'expiredate INT, ' .
'opendiscussion INT, ' .
'burnafterreading INT );'
);
zerobin_db::getInstance($this->_options);
@unlink($path);
}
} }

43
tst/zerobinWithDb.php Normal file
View File

@@ -0,0 +1,43 @@
<?php
require_once 'zerobin.php';
class zerobinWithDbTest extends zerobinTest
{
private $_options = array(
'dsn' => 'sqlite:../data/tst.sq3',
'usr' => null,
'pwd' => null,
'opt' => array(
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_PERSISTENT => true
),
);
public function setUp()
{
/* Setup Routine */
$this->_model = zerobin_db::getInstance($this->_options);
serversalt::setPath(PATH . 'data');
$this->reset();
}
public function tearDown()
{
/* Tear Down Routine */
parent::tearDown();
@unlink('../data/tst.sq3');
}
public function reset()
{
parent::reset();
// but then inject a db config
$options = parse_ini_file(CONF, true);
$options['model'] = array(
'class' => 'zerobin_db',
);
$options['model_options'] = $this->_options;
helper::confBackup();
helper::createIniFile(CONF, $options);
}
}