From 8c0ad21283a674e23d4852a66e021f374e6aa61f Mon Sep 17 00:00:00 2001 From: rugk Date: Wed, 3 Feb 2016 00:33:50 +0100 Subject: [PATCH 01/19] Fix typo in Readme It's HPKP :smile: --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ea5581e..11eaed4 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ without loosing any data. 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 + [HPKP](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 From b90260a0e152359f654781c504f2e2e86f5b31f4 Mon Sep 17 00:00:00 2001 From: squarefractal Date: Tue, 16 Feb 2016 17:36:28 +0530 Subject: [PATCH 02/19] Add a (disabled by default) .htaccess file to block out robots and other link scanning agents. --- .htaccess.disabled | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .htaccess.disabled diff --git a/.htaccess.disabled b/.htaccess.disabled new file mode 100644 index 0000000..1f5af9f --- /dev/null +++ b/.htaccess.disabled @@ -0,0 +1,3 @@ +RewriteEngine on +RewriteCond %{HTTP_USER_AGENT} ^.*(bot|spider|crawl|https?://|WhatsApp|SkypeUriPreview) [NC] +RewriteRule .* - [R=403,L] From 3a92c940a99fbeb95de22cce73efee4a9bce8a17 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Fri, 8 Apr 2016 23:29:44 +0200 Subject: [PATCH 03/19] implementing media type negotiation (based on language negotiation logic) in cases both JSON and (X)HTML are being requested, resolving #68 --- lib/i18n.php | 6 ++- lib/request.php | 107 ++++++++++++++++++++++++++++++++++++++++++++---- lib/zerobin.php | 2 +- tst/request.php | 48 ++++++++++++++++++++++ 4 files changed, 151 insertions(+), 12 deletions(-) diff --git a/lib/i18n.php b/lib/i18n.php index 0900ef3..30d7d11 100644 --- a/lib/i18n.php +++ b/lib/i18n.php @@ -198,7 +198,8 @@ class i18n if (array_key_exists('HTTP_ACCEPT_LANGUAGE', $_SERVER)) { $languageRanges = explode(',', trim($_SERVER['HTTP_ACCEPT_LANGUAGE'])); - foreach ($languageRanges as $languageRange) { + foreach ($languageRanges as $languageRange) + { if (preg_match( '/(\*|[a-zA-Z0-9]{1,8}(?:-[a-zA-Z0-9]{1,8})*)(?:\s*;\s*q\s*=\s*(0(?:\.\d{0,3})|1(?:\.0{0,3})))?/', trim($languageRange), $match @@ -325,7 +326,8 @@ class i18n protected static function _getMatchingLanguage($acceptedLanguages, $availableLanguages) { $matches = array(); $any = false; - foreach ($acceptedLanguages as $acceptedQuality => $acceptedValues) { + foreach ($acceptedLanguages as $acceptedQuality => $acceptedValues) + { $acceptedQuality = floatval($acceptedQuality); if ($acceptedQuality === 0.0) continue; foreach ($availableLanguages as $availableValue) diff --git a/lib/request.php b/lib/request.php index baa6706..3a7fe30 100644 --- a/lib/request.php +++ b/lib/request.php @@ -17,6 +17,27 @@ */ class request { + /** + * MIME type for JSON + * + * @const string + */ + const MIME_JSON = 'application/json'; + + /** + * MIME type for HTML + * + * @const string + */ + const MIME_HTML = 'text/html'; + + /** + * MIME type for XHTML + * + * @const string + */ + const MIME_XHTML = 'application/xhtml+xml'; + /** * Input stream to use for PUT parameter parsing. * @@ -66,15 +87,7 @@ class request } // 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; - } + $this->_isJsonApi = $this->_detectJsonRequest(); // parse parameters, depending on request type switch (array_key_exists('REQUEST_METHOD', $_SERVER) ? $_SERVER['REQUEST_METHOD'] : 'GET') @@ -168,4 +181,80 @@ class request { self::$_inputStream = $input; } + + /** + * detect the clients supported media type and decide if its a JSON API call or not + * + * Adapted from: http://stackoverflow.com/questions/3770513/detect-browser-language-in-php#3771447 + * + * @access private + * @return bool + */ + private function _detectJsonRequest() + { + $hasAcceptHeader = array_key_exists('HTTP_ACCEPT', $_SERVER); + $acceptHeader = $hasAcceptHeader ? $_SERVER['HTTP_ACCEPT'] : ''; + + // simple cases + if ( + (array_key_exists('HTTP_X_REQUESTED_WITH', $_SERVER) && + $_SERVER['HTTP_X_REQUESTED_WITH'] == 'JSONHttpRequest') || + ($hasAcceptHeader && + strpos($acceptHeader, self::MIME_JSON) !== false && + strpos($acceptHeader, self::MIME_HTML) === false && + strpos($acceptHeader, self::MIME_XHTML) === false) + ) + { + return true; + } + + // advanced case: media type negotiation + $mediaTypes = array(); + if ($hasAcceptHeader) + { + $mediaTypeRanges = explode(',', trim($acceptHeader)); + foreach ($mediaTypeRanges as $mediaTypeRange) + { + if (preg_match( + '#(\*/\*|[a-z\-]+/[a-z\-+*]+(?:\s*;\s*[^q]\S*)*)(?:\s*;\s*q\s*=\s*(0(?:\.\d{0,3})|1(?:\.0{0,3})))?#', + trim($mediaTypeRange), $match + )) + { + if (!isset($match[2])) + { + $match[2] = '1.0'; + } + else + { + $match[2] = (string) floatval($match[2]); + } + if (!isset($mediaTypes[$match[2]])) + { + $mediaTypes[$match[2]] = array(); + } + $mediaTypes[$match[2]][] = strtolower($match[1]); + } + } + krsort($mediaTypes); + foreach ($mediaTypes as $acceptedQuality => $acceptedValues) + { + if ($acceptedQuality === 0.0) continue; + foreach ($acceptedValues as $acceptedValue) + { + if ( + strpos($acceptedValue, self::MIME_HTML) === 0 || + strpos($acceptedValue, self::MIME_XHTML) === 0 + ) + { + return false; + } + elseif (strpos($acceptedValue, self::MIME_JSON) === 0) + { + return true; + } + } + } + } + return false; + } } \ No newline at end of file diff --git a/lib/zerobin.php b/lib/zerobin.php index 9e02fcb..b279dd2 100644 --- a/lib/zerobin.php +++ b/lib/zerobin.php @@ -151,7 +151,7 @@ class zerobin // output JSON or HTML if ($this->_request->isJsonApiCall()) { - header('Content-type: application/json'); + header('Content-type: ' . request::MIME_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'); diff --git a/tst/request.php b/tst/request.php index 0df2192..cf4a4eb 100644 --- a/tst/request.php +++ b/tst/request.php @@ -104,4 +104,52 @@ class requestTest extends PHPUnit_Framework_TestCase $this->assertEquals('foo', $request->getParam('pasteid')); $this->assertEquals('bar', $request->getParam('deletetoken')); } + + public function testReadWithNegotiation() + { + $this->reset(); + $_SERVER['REQUEST_METHOD'] = 'GET'; + $_SERVER['HTTP_ACCEPT'] = 'text/html,text/html; charset=UTF-8,application/xhtml+xml, application/xml;q=0.9,*/*;q=0.8, text/csv,application/json'; + $_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 testReadWithXhtmlNegotiation() + { + $this->reset(); + $_SERVER['REQUEST_METHOD'] = 'GET'; + $_SERVER['HTTP_ACCEPT'] = 'application/xhtml+xml,text/html,text/html; charset=UTF-8, application/xml;q=0.9,*/*;q=0.8, text/csv,application/json'; + $_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 testApiReadWithNegotiation() + { + $this->reset(); + $_SERVER['REQUEST_METHOD'] = 'GET'; + $_SERVER['HTTP_ACCEPT'] = 'text/plain,text/csv, application/xml;q=0.9, application/json, text/html,text/html; charset=UTF-8,application/xhtml+xml, */*;q=0.8'; + $_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 testReadWithFailedNegotiation() + { + $this->reset(); + $_SERVER['REQUEST_METHOD'] = 'GET'; + $_SERVER['HTTP_ACCEPT'] = 'text/plain,text/csv, application/xml;q=0.9, */*;q=0.8'; + $_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()); + } } \ No newline at end of file From 4565b72a7dca427b77d86001f4cff927825ebdeb Mon Sep 17 00:00:00 2001 From: Jiawei Zhou Date: Tue, 26 Apr 2016 13:08:35 -0500 Subject: [PATCH 04/19] Adding Chinese Translation (#73) --- i18n/zh.json | 140 ++++++++++++++++++++++++++++++++++++++++++++++++++ js/zerobin.js | 2 +- 2 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 i18n/zh.json diff --git a/i18n/zh.json b/i18n/zh.json new file mode 100644 index 0000000..d736eb4 --- /dev/null +++ b/i18n/zh.json @@ -0,0 +1,140 @@ +{ + "en": "zh", + "Paste does not exist, has expired or has been deleted.": + "粘贴不存在,已过期或者已被删除。", + "ZeroBin requires php 5.2.6 or above to work. Sorry.": + "ZeroBin需要工作于PHP 5.2.6及以上版本,抱歉。", + "ZeroBin requires configuration section [%s] to be present in configuration file.": + "ZeroBin需要设置配置文件中 [%s] 的部分。", + "Please wait %d seconds between each post.": + "每 %d 秒只能粘贴一次。", + "Paste is limited to %s of encrypted data.": + "粘贴受限于 %s 加密数据。", + "Invalid data.": + "无效的数据。", + "You are unlucky. Try again.": + "请再试一次。", + "Error saving comment. Sorry.": + "存储评论时出现错误,抱歉。", + "Error saving paste. Sorry.": + "存储粘贴时出现错误,抱歉。", + "Invalid paste ID.": + "无效的ID。", + "Paste is not of burn-after-reading type.": + "粘贴不是阅后即焚类型。", + "Wrong deletion token. Paste was not deleted.": + "错误的删除token,粘贴没有被删除。", + "Paste was properly deleted.": + "粘贴已被正确删除。", + "ZeroBin": "ZeroBin", + "ZeroBin is a minimalist, opensource online pastebin where the server has zero knowledge of pasted data. Data is encrypted/decrypted in the browser using 256 bits AES. More information on the project page.": + "ZeroBin是一个极简,开源,对粘贴内容毫不知情的在线粘贴板,数据在浏览器内进行AES-256加密。更多信息请查看项目主页。", + "Because ignorance is bliss": + "因为无知是福", + "Javascript is required for ZeroBin to work.
Sorry for the inconvenience.": + "ZeroBin需要JavaScript来进行加解密。
带来的不便敬请谅解。", + "ZeroBin requires a modern browser to work.": + "ZeroBin需要工作于现代化的浏览器。", + "Still using Internet Explorer? Do yourself a favor, switch to a modern browser:": + "还在使用Internet Explorer?帮自己个忙,换上一个现代化的浏览器:", + "New": + "新建", + "Send": + "送出", + "Clone": + "克隆", + "Raw text": + "纯文本", + "Expires": + "有效期", + "Burn after reading": + "阅后即焚", + "Open discussion": + "开放讨论", + "Password (recommended)": + "密码 (推荐)", + "Discussion": + "讨论", + "Toggle navigation": + "切换导航栏", + "%d seconds": ["%d 秒", "%d 秒"], + "%d minutes": ["%d 分钟", "%d 分钟"], + "%d hours": ["%d 小时", "%d 小时"], + "%d days": ["%d 天", "%d 天"], + "%d weeks": ["%d 周", "%d 周"], + "%d months": ["%d 个月", "%d 个月"], + "%d years": ["%d 年", "%d 年"], + "Never": + "永不过期", + "Note: This is a test service: Data may be deleted anytime. Kittens will die if you abuse this service.": + "注意:这是一个测试服务,数据随时可能被删除。如果你滥用这个服务的话,小猫咪会死的。", + "This document will expire in %d seconds.": + ["这份文档将在一秒后过期。", "这份文档将在 %d 秒后过期"], + "This document will expire in %d minutes.": + ["这份文档将在一分钟后过期。", "这份文档将在 %d 分钟后过期。"], + "This document will expire in %d hours.": + ["这份文档将在一小时后过期。", "这份文档将在 %d 小时后过期。"], + "This document will expire in %d days.": + ["这份文档将在一天后过期。", "这份文档将在 %d 天后过期。"], + "This document will expire in %d months.": + ["这份文档将在一个月后过期。", "这份文档将在 %d 个月后过期。"], + "Please enter the password for this paste:": + "请输入这份粘贴的密码:", + "Could not decrypt data (Wrong key?)": + "无法解密数据 (密钥错误?)", + "Could not delete the paste, it was not stored in burn after reading mode.": + "无法删除此粘贴,它没有以阅后即焚模式存储。", + "FOR YOUR EYES ONLY. Don't close this window, this message can't be displayed again.": + "看!仔!细!了! 不要关闭窗口,否则你再也见不到这条消息了。", + "Could not decrypt comment; Wrong key?": + "无法解密评论; 密钥错误?", + "Reply": + "回复", + "Anonymous": + "匿名", + "Anonymous avatar (Vizhash of the IP address)": + "匿名头像 (由IP地址生成Vizhash)", + "Add comment": + "添加评论", + "Optional nickname...": + "可选昵称...", + "Post comment": + "评论", + "Sending comment...": + "评论发送中...", + "Comment posted.": + "评论已发送。", + "Could not refresh display: %s": + "无法刷新显示: %s", + "unknown status": + "未知状态", + "server error or not responding": + "服务器错误或无回应", + "Could not post comment: %s": + "无法发送评论: %s", + "Sending paste (Please move your mouse for more entropy)...": + "粘贴提交中 (请移动鼠标以产生更多熵)...", + "Sending paste...": + "粘贴提交中...", + "Your paste is %s (Hit [Ctrl]+[c] to copy)": + "您的粘贴的链接是%s (按下 [Ctrl]+[c] 以复制)", + "Delete data": + "删除数据", + "Could not create paste: %s": + "无法创建粘贴: %s", + "Cannot decrypt paste: Decryption key missing in URL (Did you use a redirector or an URL shortener which strips part of the URL?)": + "无法解密粘贴: URL中缺失解密密钥 (是否使用了重定向或者短链接导致密钥丢失?)", + "Format": "格式", + "Plain Text": "纯文本", + "Source Code": "源代码", + "Markdown": "Markdown", + "Download attachment": "下载附件", + "Cloned file attached.": "已附加克隆的文件", + "Attach a file": "添加一个附件", + "Remove attachment": "移除附件", + "Your browser does not support uploading encrypted files. Please use a newer browser.": + "您的浏览器不支持上传加密的文件,请使用更新的浏览器。", + "Invalid attachment.": "无效的附件", + "Options": "选项", + "Shorten URL": "缩短链接" +} diff --git a/js/zerobin.js b/js/zerobin.js index 530f860..7b4c881 100644 --- a/js/zerobin.js +++ b/js/zerobin.js @@ -294,7 +294,7 @@ $(function() { /** * supported languages, minus the built in 'en' */ - supportedLanguages: ['de', 'fr', 'pl', 'sl'], + supportedLanguages: ['de', 'fr', 'pl', 'sl', 'zh'], /** * translate a string, alias for translate() From 4918bef4dc8a6779c6deb9084e92d24c18de0475 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Tue, 26 Apr 2016 20:21:30 +0200 Subject: [PATCH 05/19] Although there usually are no plurals in chinese, there's an exception for words related to persons, when not preceeded by a numeric word. Sources: - http://localization-guide.readthedocs.org/en/latest/l10n/pluralforms.html#f3 - https://answers.yahoo.com/question/index?qid=20110606153553AAAW5zX --- js/zerobin.js | 1 + lib/i18n.php | 1 + 2 files changed, 2 insertions(+) diff --git a/js/zerobin.js b/js/zerobin.js index 7b4c881..52979e9 100644 --- a/js/zerobin.js +++ b/js/zerobin.js @@ -363,6 +363,7 @@ $(function() { switch (this.language) { case 'fr': + case 'zh': return (n > 1 ? 1 : 0); case 'pl': return (n == 1 ? 0 : n%10 >= 2 && n %10 <=4 && (n%100 < 10 || n%100 >= 20) ? 1 : 2); diff --git a/lib/i18n.php b/lib/i18n.php index 30d7d11..14e5bc5 100644 --- a/lib/i18n.php +++ b/lib/i18n.php @@ -303,6 +303,7 @@ class i18n { switch (self::$_language) { case 'fr': + case 'zh': return ($n > 1 ? 1 : 0); case 'pl': return ($n == 1 ? 0 : $n%10 >= 2 && $n %10 <=4 && ($n%100 < 10 || $n%100 >= 20) ? 1 : 2); From ff3154316c4dcc97325c9e250f41a16e419b8a57 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Tue, 26 Apr 2016 20:32:48 +0200 Subject: [PATCH 06/19] Sometimes simple solutions are the cleanest. Resolves #51, resolves #72. --- tpl/bootstrap-compact.html | 2 +- tpl/bootstrap-dark-page.html | 2 +- tpl/bootstrap-dark.html | 2 +- tpl/bootstrap-page.html | 2 +- tpl/bootstrap.html | 2 +- tpl/page.html | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tpl/bootstrap-compact.html b/tpl/bootstrap-compact.html index 7d44543..97baca9 100644 --- a/tpl/bootstrap-compact.html +++ b/tpl/bootstrap-compact.html @@ -158,7 +158,7 @@ {$STATUS|htmlspecialchars} {/if} - + {/if} - + {/if} - + {/if} - + {/if} - +