diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d7b0879 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,30 @@ +Changelog +========= + +Version 0.1.2 - Alpha +--------------------- +- Bugfix: You can't comment, after you already comment (Error + Auto-Logout). +- Bugfix: Translated strings didn't returned correctly (English is returned instead), + Thansk to [#10](https://github.com/pytesNET/snicker/issues/10). +- Bugfix: The "Terms of Use" checkbox couldn't be disabled. + Thanks to [#16](https://github.com/pytesNET/snicker/issues/16). + +Version 0.1.1 - Alpha +--------------------- +- Add: The Persian Translation, many thanks to [abdulhalim](https://github.com/abdulhalim). +- Add: The GD-less PureCaptcha library, written by Abbas Naderi, which doesn't use PHP GD. + Thanks to [#8](https://github.com/pytesNET/snicker/issues/8) +- Add: Reload function to the Captcha Image (Click on the image). + Thanks to [#5](https://github.com/pytesNET/snicker/issues/5). +- Add: A JS-Snippet which Enables/Disables the Gravatar Select Field. + Thanks to [#9](https://github.com/pytesNET/snicker/issues/9). +- Add: The Captcha and Terms note aren't shown for logged in users. + Thanks to [#7](https://github.com/pytesNET/snicker/issues/7). +- Bugfix: The `comments_per_page` option couldn't be set on 0 to disable the limit! + Thanks to [#9](https://github.com/pytesNET/snicker/issues/9). +- Bugfix: Reload the Captcha, if it was wrong. + Thansk to [#5](https://github.com/pytesNET/snicker/issues/5). + +Version 0.1.0 - Alpha +--------------------- +- Initial Release owo/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..d7199b1 --- /dev/null +++ b/README.md @@ -0,0 +1,43 @@ +Snicker +======= + +Snicker is the first native FlatFile comment system for the Content Management System +[Bludit](https://github.com/bludit/bludit). It allows to write and publish comments using basic +HTML Syntax or Markdown. The Plugin also offers an extensive environment, many settings and +possibilities and is also completely compliant with the GDPR! + +Features +-------- +- Level-Based, AJAX-enabled Commenting for Guests and Users +- Many Configurations and adaptable Strings and Themes +- Guest Management for Not-Logged-In Comment Authors +- Moderatable Comments (Pending, Approved, Rejected, Spam) +- Extensive Backend with many possibilities +- Compliant with the European GDPR + +Requirements +------------ +- PHP v5.6.0+ +- Bludit v3.5.0+ + +Dependencies +------------ +- Snicker use the awesome [Captcha PHP Library](https://github.com/Gregwar/Captcha) made by Grégoire Passault +- Snicker uses also the [PureCaptcha PHP Library](https://github.com/OWASP/PureCaptcha) as fallback by Abbas Naderi +- The Avatars are served per default by [Gravatar](https://de.gravatar.com/), made by Automattic / WordPress +- **But** you can also directly use [Identicons](http://identicon.net) instead... +- ... where we use the [Identicon PHP Library](https://github.com/yzalis/Identicon) from Benjamin Laugueux +- ... and the [Identicon JavaScript Library](https://github.com/stewartlord/identicon.js) from Stewart Lord +- ... which itself depends on the [PNG JavaScript Library](https://www.xarg.org/2010/03/generate-client-side-png-files-using-javascript/) by Robert Eisele + +Thanks for this awesome packages and projects! + +Installation +------------ +- Download the [Snicker Plugin](https://github.com/pytesNET/snicker/zipball/master) +- Upload it to your `bl-plugins` folder of your Bludit Website +- Visit the Bludit Administration and enable the "Snicker" Plugin through "Settings" > "Plugins" + +Copyright & License +------------------- +Published under the MIT-License; Copyright © 2019 SamBrishes, pytesNET diff --git a/admin/add.php b/admin/add.php new file mode 100644 index 0000000..44ce605 --- /dev/null +++ b/admin/add.php @@ -0,0 +1,16 @@ + + | @version 0.1.2 [0.1.0] - Alpha + | + | @website https://github.com/pytesNET/snicker + | @license X11 / MIT License + | @copyright Copyright © 2019 SamBrishes, pytesNET + */ + if(!defined("BLUDIT")){ die("Go directly to Jail. Do not pass Go. Do not collect 200 Cookies!"); } + + // Add Formular 4 Admins + +?> diff --git a/admin/css/admin.snicker.css b/admin/css/admin.snicker.css new file mode 100644 index 0000000..d6cff98 --- /dev/null +++ b/admin/css/admin.snicker.css @@ -0,0 +1,106 @@ +@charset "UTF-8"; +/* + | Snicker The first native FlatFile Comment Plugin 4 Bludit + | @file ./admin/css/admin.snicker.css + | @author SamBrishes + | @version 0.1.2 [0.1.0] - Alpha + | + | @website https://github.com/pytesNET/snicker + | @license X11 / MIT License + | @copyright Copyright © 2019 SamBrishes, pytesNET + */ + +/* @start GENERAL */ +.shadow-sm-both{ + box-shadow: 0 0 0.25rem rgba(0,0,0,.075) !important; +} +.table-hover-light tr:hover{ + background-color: #f0f3f6; +} +/* @end GENERAL */ + +/* @start Admin Navigation */ +.nav.nav-pills .nav-link{ + color: #343a40; + font-weight: 400; + font-weight: 600; + background-color: #e9ecef; + transition: color 142ms linear, background 142ms linear; +} +.nav.nav-pills .nav-link:hover{ + background-color: #d9dcdf; +} +.nav.nav-pills .nav-link.active{ + color: #ffffff; + text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.75); + background-color: #343a40; +} +.nav.nav-pills .nav-link .badge{ + margin-top: 3px; + text-shadow: none; + vertical-align: top; +} +.nav.nav-pills .nav-link.active .badge{ + text-shadow: none; + background-color: #fff; +} + +.nav.nav-pills .nav-link.nav-pending{ + border-top-right-radius: 0 !important; + border-bottom-right-radius: 0 !important; +} +.nav.nav-pills .nav-link.nav-pending.active{ + text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.25); + background-color: #007bff; +} +.nav.nav-pills .nav-link.nav-pending.active .badge{ + color: #007bff; +} +.nav.nav-pills .nav-link.nav-approved{ + border-radius: 0 !important; +} +.nav.nav-pills .nav-link.nav-approved.active{ + text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.25); + background-color: #28A745; +} +.nav.nav-pills .nav-link.nav-rejected{ + border-radius: 0 !important; +} +.nav.nav-pills .nav-link.nav-rejected.active{ + text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.25); + background-color: #FFC107; +} +.nav.nav-pills .nav-link.nav-spam{ + border-top-left-radius: 0 !important; + border-bottom-left-radius: 0 !important; +} +.nav.nav-pills .nav-link.nav-spam.active{ + text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.25); + background-color: #DC3545; +} +.nav.nav-pills .nav-link.nav-spam.active .badge{ + color: #DC3545; +} +.nav.nav-pills .nav-link.nav-config{ + font-weight: 400; +} +.nav.nav-pills .nav-link.nav-config .oi{ + margin-right: 3px; + line-height: 1.5; + vertical-align: top; +} +/* @end Admin Navigation */ + +/* @start Admin Pagination */ +.btn-group.btn-group-pagination .btn{ + width: 34px; + padding-top: 5px; + padding-bottom: 7px; + font-size: 20px; + line-height: 20px; + text-align: center; +} +.btn-group.btn-group-pagination .btn.disabled{ + cursor: not-allowed; +} +/* @end Admin Pagination */ diff --git a/admin/edit.php b/admin/edit.php new file mode 100644 index 0000000..e0b8f4c --- /dev/null +++ b/admin/edit.php @@ -0,0 +1,93 @@ + + | @version 0.1.2 [0.1.0] - Alpha + | + | @website https://github.com/pytesNET/snicker + | @license X11 / MIT License + | @copyright Copyright © 2019 SamBrishes, pytesNET + */ + if(!defined("BLUDIT")){ die("Go directly to Jail. Do not pass Go. Do not collect 200 Cookies!"); } + + global $login, $pages, $security, $SnickerIndex; + + $data = $SnickerIndex->getComment($_GET["uid"]); + $comment = new Comment($_GET["uid"], $data["page_uuid"]); + $page = new Page($pages->getByUUID($data["page_uuid"])); + +?>

+ Snicker / +

+
+
+
+
+
+ + + + + + +
+ +
+ +
+
+
+
+ +
+
+ " /> +
+
+ +
+
+ +
+
+
+
+
+ + getValue("author"), "bludit") === 0){ ?> +

+ +

+

+ " class="form-control" disabled /> +

+ +

+ " /> +

+

+ " /> +

+ +

+ +

+
+
+ +

+ +

+
+
+
diff --git a/admin/index-comments.php b/admin/index-comments.php new file mode 100644 index 0000000..8945044 --- /dev/null +++ b/admin/index-comments.php @@ -0,0 +1,188 @@ + + | @version 0.1.2 [0.1.0] - Alpha + | + | @website https://github.com/pytesNET/snicker + | @license X11 / MIT License + | @copyright Copyright © 2019 SamBrishes, pytesNET + */ + if(!defined("BLUDIT")){ die("Go directly to Jail. Do not pass Go. Do not collect 200 Cookies!"); } + + global $pages, $security, $Snicker, $SnickerIndex, $SnickerPlugin, $SnickerUsers; + + // Get Data + $limit = $SnickerPlugin->getValue("frontend_per_page"); + if($limit === 0){ + $limit = 15; + } + $current = isset($_GET["tab"])? $_GET["tab"]: "pending"; + + // Get View + $view = "index"; + if(isset($_GET["view"]) && in_array($_GET["view"], array("search", "single", "uuid", "user"))){ + $view = $current = $_GET["view"]; + $tabs = array($view); + } else { + $tabs = array("pending", "approved", "rejected", "spam"); + } + + // Render Comemnts Tab + foreach($tabs AS $status){ + if(isset($_GET["tab"]) && $_GET["tab"] === $status){ + $page = max((isset($_GET["page"])? (int) $_GET["page"]: 1), 1); + } else { + $page = 1; + } + + // Get Comments + if($view === "index"){ + $comments = $SnickerIndex->getList($status, $page, $limit); + $total = $SnickerIndex->count($status); + } else if($view === "search"){ + $comments = $SnickerIndex->searchComments(isset($_GET["search"])? $_GET["search"]: ""); + $total = count($comments); + } else if($view === "single"){ + $comments = $SnickerIndex->getListByParent(isset($_GET["single"])? $_GET["single"]: ""); + $total = count($comments); + } else if($view === "uuid"){ + $comments = $SnickerIndex->getListByUUID(isset($_GET["uuid"])? $_GET["uuid"]: ""); + $total = count($comments); + } else if($view === "user"){ + $comments = $SnickerIndex->getListByUser(isset($_GET["user"])? $_GET["user"]: ""); + $total = count($comments); + } + + // Render Tab Content + $link = DOMAIN_ADMIN . "snicker?page=%d&tab={$status}#{$status}"; + ?> +
"> +
+
+
+
+
+
+ + " /> +
+
+ +
+
+
+ +
+ $limit){ ?> +
+ + « + + + « + + + + + » + + + » + +
+ +
+
+
+
+ + + +
+
+
+
+
+
+
+
+ + + + + getTokenCSRF(); ?> + + + <> + + + + + + > + + + + getComment($uid, $status); + if(!(isset($data["page_uuid"]) && is_string($data["page_uuid"]))){ + continue; + } + $user = $SnickerUsers->getByString($data["author"]); + ?> + + + + + + + +
+ getValue("comment_title") !== "disabled" && !empty($data["title"])){ + echo '' . $data["title"] . ''; + } + echo '

' . (isset($data["excerpt"])? $data["excerpt"]: "") . '

'; + if(!empty($data["parent_uid"]) && $SnickerIndex->exists($data["parent_uid"]) && $view !== "single"){ + $reply = DOMAIN_ADMIN . "snicker?view=single&single={$uid}"; + $reply = '' . $SnickerIndex->getComment($data["parent_uid"])["title"] . ''; + echo "
" . sn__("Reply To") . ": " . $reply . "
"; + } + ?> +
+ + + +
+ + +
+ + getByUUID($data["page_uuid"])); ?> + +
+ + + | @version 0.1.2 [0.1.0] - Alpha + | + | @website https://github.com/pytesNET/snicker + | @license X11 / MIT License + | @copyright Copyright © 2019 SamBrishes, pytesNET + */ + if(!defined("BLUDIT")){ die("Go directly to Jail. Do not pass Go. Do not collect 200 Cookies!"); } + + global $L, $login, $pages, $security, $Snicker, $SnickerPlugin; + + // Get Static Pages + $static = $pages->getStaticDB(false); + +?> +
+
+
+
+
+
+ + + + +
+ +
+ +
+
+
+
+ +
+
+
+
+
+
+ +
+ + + +
+ /> + +
+
+ + +
+
+ /> + +
+
+
+ +
+ +
+
+ /> + +
+
+ /> + +
+
+ /> + +
+
+
+ +
+ +
+ +
+
+ +
+ +
+ " + class="form-control" min="0" placeholder="" /> +
+
+ +
+ +
+ " + class="form-control" min="0" placeholder="" /> + +
+
+ +
+ +
+
+ /> + +
+
+ /> + +
+
+
+ +
+ +
+ + + () +
+

+ Cookie Storage is located on the Computer of the user. So you don't have the full control AND you require the appropriate permissions from the user."); ?> +

+

+ Session Storage is just stored temporary on the server, it gets cleaned up when the user closes the browser. Therefore you don't need any permissions from the user."); ?> +

+

+ Database Storage generates and stores an anonymized but assignable value of the user, which also requires the appropriate permissions from the user."); ?> +

+

+ Please Note: You are responsible for obtaining the appropriate permissions, Snicker just handles the permissions for data send (and stored) via the comment form!"); ?> +

+
+ +
+ /> + +
+
+ /> + +
+
+
+
+
+
+ +
+
+
+
+
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ " + class="form-control" min="0" step="1" placheolder="" /> + +
+
+ +
+ +
+ + +
+
+ +
+ +
+ + +
+
+ +
+ +
+ +
+ + +
+ /> + +
+
+
+ +
+ +
+ + +
+
+
+
+
+ +
+
+
+
+
:(
+
+ +
+ +
+
+ +
+ +
+ " + class="form-control" placeholder="" disabled="disabled" /> +
+
+ +
+ +
+ " + class="form-control" placeholder="" disabled="disabled" /> +
+
+ +
+ +
+ +
+
+ +
+ +
+ + '.sn__("here").'')); ?> +
+
+
+
+
+ +
+
+
+
+
+ +
+ " + class="form-control" placeholder="dbFields["string_success_1"]; ?>" /> +
+
+ +
+ +
+ " + class="form-control" placeholder="dbFields["string_success_2"]; ?>" /> +
+
+ +
+ +
+ " + class="form-control" placeholder="dbFields["string_success_3"]; ?>" /> +
+
+ +
+ +
+ " + class="form-control" placeholder="dbFields["string_error_1"]; ?>" /> +
+
+ +
+ +
+ " + class="form-control" placeholder="dbFields["string_error_2"]; ?>" /> +
+
+ +
+ +
+ " + class="form-control" placeholder="dbFields["string_error_3"]; ?>" /> +
+
+ +
+ +
+ " + class="form-control" placeholder="dbFields["string_error_4"]; ?>" /> +
+
+ +
+ +
+ " + class="form-control" placeholder="dbFields["string_error_5"]; ?>" /> +
+
+ +
+ +
+ " + class="form-control" placeholder="dbFields["string_error_6"]; ?>" /> +
+
+ +
+ +
+ " + class="form-control" placeholder="dbFields["string_error_7"]; ?>" /> +
+
+ +
+ +
+ " + class="form-control" placeholder="dbFields["string_error_8"]; ?>" /> +
+
+ +
+ +
+ " + class="form-control" placeholder="dbFields["string_terms_of_use"]; ?>" /> +
+
+
+
+
+
+ +
+
+ +
+
+
+
diff --git a/admin/index-users.php b/admin/index-users.php new file mode 100644 index 0000000..6c58477 --- /dev/null +++ b/admin/index-users.php @@ -0,0 +1,133 @@ + + | @version 0.1.2 [0.1.0] - Alpha + | + | @website https://github.com/pytesNET/snicker + | @license X11 / MIT License + | @copyright Copyright © 2019 SamBrishes, pytesNET + */ + if(!defined("BLUDIT")){ die("Go directly to Jail. Do not pass Go. Do not collect 200 Cookies!"); } + + global $SnickerUsers; + + // Get Data + $page = max((isset($_GET["page"])? (int) $_GET["page"]: 1), 1); + $limit = sn_config("frontend_per_page"); + $total = count($SnickerUsers->db); + + // Get Users + $search = null; + if(isset($_GET["view"]) && $_GET["view"] === "users"){ + $page = 1; + $limit = -1; + $search = isset($_GET["search"])? $_GET["search"]: null; + } + $users = $SnickerUsers->getList($search, $page, $limit); + + // Link + $link = DOMAIN_ADMIN . "snicker?page=%d&tab=users#users"; + +?> +
+
+
+
+
+
+
+ " /> +
+
+ +
+
+
+ +
+ $limit){ ?> +
+ + « + + + « + + + + + » + + + » + +
+ +
+
+
+
+ + +
+
+
+
+
+
+
+ + getTokenCSRF(); ?> + + + <> + + + + + + + > + + + + $user){ ?> + + + + + + + + +
+ + + + + + + + + +
+ + +
+
+ +
diff --git a/admin/index.php b/admin/index.php new file mode 100644 index 0000000..52eb425 --- /dev/null +++ b/admin/index.php @@ -0,0 +1,85 @@ + + | @version 0.1.2 [0.1.0] - Alpha + | + | @website https://github.com/pytesNET/snicker + | @license X11 / MIT License + | @copyright Copyright © 2019 SamBrishes, pytesNET + */ + if(!defined("BLUDIT")){ die("Go directly to Jail. Do not pass Go. Do not collect 200 Cookies!"); } + + global $L, $Snicker; + + // Pending Counter + $count = count($Snicker->getIndex("pending")); + $count = ($count > 99)? "99+": $count; + $spam = count($Snicker->getIndex("spam")); + + // Tab Strings + $strings = array( + "pending" => sn__("Pending"), + "approved" => sn__("Approved"), + "rejected" => sn__("Rejected"), + "spam" => sn__("Spam"), + "search" => sn__("Search Comments"), + "single" => sn__("Single Comment"), + "uuid" => sn__("Page Comments"), + "user" => sn__("User Comments") + ); + + // Current Tab + $view = "index"; + if(isset($_GET["view"]) && in_array($_GET["view"], array("search", "single", "uuid", "user"))){ + $view = $current = $_GET["view"]; + $tabs = array($view); + } else { + $current = isset($_GET["tab"])? $_GET["tab"]: "pending"; + $tabs = array("pending", "approved", "rejected", "spam"); + } +?> +

+ Snicker +

+ + + +
+ +
diff --git a/admin/js/admin.snicker.js b/admin/js/admin.snicker.js new file mode 100644 index 0000000..c7eed91 --- /dev/null +++ b/admin/js/admin.snicker.js @@ -0,0 +1,125 @@ +/* + | Snicker The first native FlatFile Comment Plugin 4 Bludit + | @file ./admin/js/admin.snicker.js + | @author SamBrishes + | @version 0.1.2 [0.1.0] - Alpha + | + | @website https://github.com/pytesNET/snicker + | @license X11 / MIT License + | @copyright Copyright © 2019 SamBrishes, pytesNET + */ +;(function(root){ + "use strict"; + var w = root, d = root.document; + + /* + | HELPER :: LOOP + | @since 0.1.0 + */ + var each = function(elements, callback){ + if(elements instanceof HTMLElement){ + callback.call(elements, elements); + } else if(elements.length && elements.length > 0){ + for(var l = elements.length, i = 0; i < l; i++){ + callback.call(elements[i], elements[i], i); + } + } + }; + + // Ready? + d.addEventListener("DOMContentLoaded", function(){ + "use strict"; + + /* + | BOOTSTRAP POPOVER + | @since 0.1.0 + */ + jQuery('[data-toggle="popover"]').popover({ + content: function(){ + var data = d.querySelector(this.getAttribute("data-target")); + return data.innerHTML; + }, + html: true + }).click(function(event){ + event.preventDefault(); + }).on("inserted.bs.popover", function(event){ + d.querySelector("#" + this.getAttribute("aria-describedby")).style.width = "410px"; + d.querySelector("#" + this.getAttribute("aria-describedby")).style.maxWidth = "410px"; + }) + + /* + | MAIN MENU LINK HANDLER + | @since 0.1.0 + */ + var mainMenu = d.querySelector("[data-handle='tabs']"); + if(mainMenu){ + var menuLink = function(link){ + if(typeof(link) === "undefined"){ + if(w.location.hash.length == 0){ + var link = mainMenu.querySelector("li a"); + } else { + var link = mainMenu.querySelector("[href='#snicker-" + w.location.hash.substr(1) + "']"); + } + } + if(!(link instanceof Element)){ + return false; + } + + // Handle + if(link && !link.classList.contains("active")){ + link.click(); + } + if(link){ + w.location.hash = link.getAttribute("href").replace("snicker-", ""); + } + }; + + // Current Hash Handler + if(w.location.hash.length > 0){ + menuLink(); + } + + // Local Hash Handler + each(mainMenu.querySelectorAll("li > a"), function(){ + this.addEventListener("click", function(event){ + menuLink(this); + }); + }); + + // History Hash Handler + w.onhashchange = function(event){ + if(w.location.hash.length == 0){ + var link = mainMenu.querySelector("li a"); + } else { + var link = mainMenu.querySelector("[href='#snicker-" + w.location.hash.substr(1) + "']"); + } + menuLink(link); + }; + } + + /* + | MAIN MENU LINK HANDLER + | @since 0.1.1 + */ + var avatar = document.getElementById("sn-avatar"), + gravatar = document.getElementById("sn-gravatar"); + if(avatar && gravatar){ + var GravatarOption = function(){ + console.log(avatar.value) + + if(avatar.value == "gravatar"){ + gravatar.disabled = false; + document.querySelector("label[for='sn-gravatar']").classList.remove("text-muted"); + } else { + gravatar.disabled = true; + document.querySelector("label[for='sn-gravatar']").classList.add("text-muted"); + } + }; + + avatar.addEventListener("change", function(){ + GravatarOption(); + }); + GravatarOption(); + } + }); +})(window); diff --git a/includes/Gregwar/Captcha/CaptchaBuilder.php b/includes/Gregwar/Captcha/CaptchaBuilder.php new file mode 100644 index 0000000..16c5658 --- /dev/null +++ b/includes/Gregwar/Captcha/CaptchaBuilder.php @@ -0,0 +1,717 @@ + + * @author Jeremy Livingston + */ +class CaptchaBuilder implements CaptchaBuilderInterface +{ + /** + * @var array + */ + protected $fingerprint = array(); + + /** + * @var bool + */ + protected $useFingerprint = false; + + /** + * @var array + */ + protected $textColor = array(); + + /** + * @var array + */ + protected $backgroundColor = null; + + /** + * @var array + */ + protected $backgroundImages = array(); + + /** + * @var resource + */ + protected $contents = null; + + /** + * @var string + */ + protected $phrase = null; + + /** + * @var PhraseBuilderInterface + */ + protected $builder; + + /** + * @var bool + */ + protected $distortion = true; + + /** + * The maximum number of lines to draw in front of + * the image. null - use default algorithm + */ + protected $maxFrontLines = null; + + /** + * The maximum number of lines to draw behind + * the image. null - use default algorithm + */ + protected $maxBehindLines = null; + + /** + * The maximum angle of char + */ + protected $maxAngle = 8; + + /** + * The maximum offset of char + */ + protected $maxOffset = 5; + + /** + * Is the interpolation enabled ? + * + * @var bool + */ + protected $interpolation = true; + + /** + * Ignore all effects + * + * @var bool + */ + protected $ignoreAllEffects = false; + + /** + * Allowed image types for the background images + * + * @var array + */ + protected $allowedBackgroundImageTypes = array('image/png', 'image/jpeg', 'image/gif'); + + /** + * The image contents + */ + public function getContents() + { + return $this->contents; + } + + /** + * Enable/Disables the interpolation + * + * @param $interpolate bool True to enable, false to disable + * + * @return CaptchaBuilder + */ + public function setInterpolation($interpolate = true) + { + $this->interpolation = $interpolate; + + return $this; + } + + /** + * Temporary dir, for OCR check + */ + public $tempDir = 'temp/'; + + public function __construct($phrase = null, PhraseBuilderInterface $builder = null) + { + if ($builder === null) { + $this->builder = new PhraseBuilder; + } else { + $this->builder = $builder; + } + + $this->phrase = is_string($phrase) ? $phrase : $this->builder->build($phrase); + } + + /** + * Setting the phrase + */ + public function setPhrase($phrase) + { + $this->phrase = (string) $phrase; + } + + /** + * Enables/disable distortion + */ + public function setDistortion($distortion) + { + $this->distortion = (bool) $distortion; + + return $this; + } + + public function setMaxBehindLines($maxBehindLines) + { + $this->maxBehindLines = $maxBehindLines; + + return $this; + } + + public function setMaxFrontLines($maxFrontLines) + { + $this->maxFrontLines = $maxFrontLines; + + return $this; + } + + public function setMaxAngle($maxAngle) + { + $this->maxAngle = $maxAngle; + + return $this; + } + + public function setMaxOffset($maxOffset) + { + $this->maxOffset = $maxOffset; + + return $this; + } + + /** + * Gets the captcha phrase + */ + public function getPhrase() + { + return $this->phrase; + } + + /** + * Returns true if the given phrase is good + */ + public function testPhrase($phrase) + { + return ($this->builder->niceize($phrase) == $this->builder->niceize($this->getPhrase())); + } + + /** + * Instantiation + */ + public static function create($phrase = null) + { + return new self($phrase); + } + + /** + * Sets the text color to use + */ + public function setTextColor($r, $g, $b) + { + $this->textColor = array($r, $g, $b); + + return $this; + } + + /** + * Sets the background color to use + */ + public function setBackgroundColor($r, $g, $b) + { + $this->backgroundColor = array($r, $g, $b); + + return $this; + } + + /** + * Sets the ignoreAllEffects value + * + * @param bool $ignoreAllEffects + * @return CaptchaBuilder + */ + public function setIgnoreAllEffects($ignoreAllEffects) + { + $this->ignoreAllEffects = $ignoreAllEffects; + + return $this; + } + + /** + * Sets the list of background images to use (one image is randomly selected) + */ + public function setBackgroundImages(array $backgroundImages) + { + $this->backgroundImages = $backgroundImages; + + return $this; + } + + /** + * Draw lines over the image + */ + protected function drawLine($image, $width, $height, $tcol = null) + { + if ($tcol === null) { + $tcol = imagecolorallocate($image, $this->rand(100, 255), $this->rand(100, 255), $this->rand(100, 255)); + } + + if ($this->rand(0, 1)) { // Horizontal + $Xa = $this->rand(0, $width/2); + $Ya = $this->rand(0, $height); + $Xb = $this->rand($width/2, $width); + $Yb = $this->rand(0, $height); + } else { // Vertical + $Xa = $this->rand(0, $width); + $Ya = $this->rand(0, $height/2); + $Xb = $this->rand(0, $width); + $Yb = $this->rand($height/2, $height); + } + imagesetthickness($image, $this->rand(1, 3)); + imageline($image, $Xa, $Ya, $Xb, $Yb, $tcol); + } + + /** + * Apply some post effects + */ + protected function postEffect($image) + { + if (!function_exists('imagefilter')) { + return; + } + + if ($this->backgroundColor != null || $this->textColor != null) { + return; + } + + // Negate ? + if ($this->rand(0, 1) == 0) { + imagefilter($image, IMG_FILTER_NEGATE); + } + + // Edge ? + if ($this->rand(0, 10) == 0) { + imagefilter($image, IMG_FILTER_EDGEDETECT); + } + + // Contrast + imagefilter($image, IMG_FILTER_CONTRAST, $this->rand(-50, 10)); + + // Colorize + if ($this->rand(0, 5) == 0) { + imagefilter($image, IMG_FILTER_COLORIZE, $this->rand(-80, 50), $this->rand(-80, 50), $this->rand(-80, 50)); + } + } + + /** + * Writes the phrase on the image + */ + protected function writePhrase($image, $phrase, $font, $width, $height) + { + $length = mb_strlen($phrase); + if ($length === 0) { + return \imagecolorallocate($image, 0, 0, 0); + } + + // Gets the text size and start position + $size = $width / $length - $this->rand(0, 3) - 1; + $box = \imagettfbbox($size, 0, $font, $phrase); + $textWidth = $box[2] - $box[0]; + $textHeight = $box[1] - $box[7]; + $x = ($width - $textWidth) / 2; + $y = ($height - $textHeight) / 2 + $size; + + if (!$this->textColor) { + $textColor = array($this->rand(0, 150), $this->rand(0, 150), $this->rand(0, 150)); + } else { + $textColor = $this->textColor; + } + $col = \imagecolorallocate($image, $textColor[0], $textColor[1], $textColor[2]); + + // Write the letters one by one, with random angle + for ($i=0; $i<$length; $i++) { + $symbol = mb_substr($phrase, $i, 1); + $box = \imagettfbbox($size, 0, $font, $symbol); + $w = $box[2] - $box[0]; + $angle = $this->rand(-$this->maxAngle, $this->maxAngle); + $offset = $this->rand(-$this->maxOffset, $this->maxOffset); + \imagettftext($image, $size, $angle, $x, $y + $offset, $col, $font, $symbol); + $x += $w; + } + + return $col; + } + + /** + * Try to read the code against an OCR + */ + public function isOCRReadable() + { + if (!is_dir($this->tempDir)) { + @mkdir($this->tempDir, 0755, true); + } + + $tempj = $this->tempDir . uniqid('captcha', true) . '.jpg'; + $tempp = $this->tempDir . uniqid('captcha', true) . '.pgm'; + + $this->save($tempj); + shell_exec("convert $tempj $tempp"); + $value = trim(strtolower(shell_exec("ocrad $tempp"))); + + @unlink($tempj); + @unlink($tempp); + + return $this->testPhrase($value); + } + + /** + * Builds while the code is readable against an OCR + */ + public function buildAgainstOCR($width = 150, $height = 40, $font = null, $fingerprint = null) + { + do { + $this->build($width, $height, $font, $fingerprint); + } while ($this->isOCRReadable()); + } + + /** + * Generate the image + */ + public function build($width = 150, $height = 40, $font = null, $fingerprint = null) + { + if (null !== $fingerprint) { + $this->fingerprint = $fingerprint; + $this->useFingerprint = true; + } else { + $this->fingerprint = array(); + $this->useFingerprint = false; + } + + if ($font === null) { + $font = __DIR__ . '/Font/captcha'.$this->rand(0, 5).'.ttf'; + } + + if (empty($this->backgroundImages)) { + // if background images list is not set, use a color fill as a background + $image = imagecreatetruecolor($width, $height); + if ($this->backgroundColor == null) { + $bg = imagecolorallocate($image, $this->rand(200, 255), $this->rand(200, 255), $this->rand(200, 255)); + } else { + $color = $this->backgroundColor; + $bg = imagecolorallocate($image, $color[0], $color[1], $color[2]); + } + $this->background = $bg; + imagefill($image, 0, 0, $bg); + } else { + // use a random background image + $randomBackgroundImage = $this->backgroundImages[rand(0, count($this->backgroundImages)-1)]; + + $imageType = $this->validateBackgroundImage($randomBackgroundImage); + + $image = $this->createBackgroundImageFromType($randomBackgroundImage, $imageType); + } + + // Apply effects + if (!$this->ignoreAllEffects) { + $square = $width * $height; + $effects = $this->rand($square/3000, $square/2000); + + // set the maximum number of lines to draw in front of the text + if ($this->maxBehindLines != null && $this->maxBehindLines > 0) { + $effects = min($this->maxBehindLines, $effects); + } + + if ($this->maxBehindLines !== 0) { + for ($e = 0; $e < $effects; $e++) { + $this->drawLine($image, $width, $height); + } + } + } + + // Write CAPTCHA text + $color = $this->writePhrase($image, $this->phrase, $font, $width, $height); + + // Apply effects + if (!$this->ignoreAllEffects) { + $square = $width * $height; + $effects = $this->rand($square/3000, $square/2000); + + // set the maximum number of lines to draw in front of the text + if ($this->maxFrontLines != null && $this->maxFrontLines > 0) { + $effects = min($this->maxFrontLines, $effects); + } + + if ($this->maxFrontLines !== 0) { + for ($e = 0; $e < $effects; $e++) { + $this->drawLine($image, $width, $height, $color); + } + } + } + + // Distort the image + if ($this->distortion && !$this->ignoreAllEffects) { + $image = $this->distort($image, $width, $height, $bg); + } + + // Post effects + if (!$this->ignoreAllEffects) { + $this->postEffect($image); + } + + $this->contents = $image; + + return $this; + } + + /** + * Distorts the image + */ + public function distort($image, $width, $height, $bg) + { + $contents = imagecreatetruecolor($width, $height); + $X = $this->rand(0, $width); + $Y = $this->rand(0, $height); + $phase = $this->rand(0, 10); + $scale = 1.1 + $this->rand(0, 10000) / 30000; + for ($x = 0; $x < $width; $x++) { + for ($y = 0; $y < $height; $y++) { + $Vx = $x - $X; + $Vy = $y - $Y; + $Vn = sqrt($Vx * $Vx + $Vy * $Vy); + + if ($Vn != 0) { + $Vn2 = $Vn + 4 * sin($Vn / 30); + $nX = $X + ($Vx * $Vn2 / $Vn); + $nY = $Y + ($Vy * $Vn2 / $Vn); + } else { + $nX = $X; + $nY = $Y; + } + $nY = $nY + $scale * sin($phase + $nX * 0.2); + + if ($this->interpolation) { + $p = $this->interpolate( + $nX - floor($nX), + $nY - floor($nY), + $this->getCol($image, floor($nX), floor($nY), $bg), + $this->getCol($image, ceil($nX), floor($nY), $bg), + $this->getCol($image, floor($nX), ceil($nY), $bg), + $this->getCol($image, ceil($nX), ceil($nY), $bg) + ); + } else { + $p = $this->getCol($image, round($nX), round($nY), $bg); + } + + if ($p == 0) { + $p = $bg; + } + + imagesetpixel($contents, $x, $y, $p); + } + } + + return $contents; + } + + /** + * Saves the Captcha to a jpeg file + */ + public function save($filename, $quality = 90) + { + imagejpeg($this->contents, $filename, $quality); + } + + /** + * Gets the image GD + */ + public function getGd() + { + return $this->contents; + } + + /** + * Gets the image contents + */ + public function get($quality = 90) + { + ob_start(); + $this->output($quality); + + return ob_get_clean(); + } + + /** + * Gets the HTML inline base64 + */ + public function inline($quality = 90) + { + return 'data:image/jpeg;base64,' . base64_encode($this->get($quality)); + } + + /** + * Outputs the image + */ + public function output($quality = 90) + { + imagejpeg($this->contents, null, $quality); + } + + /** + * @return array + */ + public function getFingerprint() + { + return $this->fingerprint; + } + + /** + * Returns a random number or the next number in the + * fingerprint + */ + protected function rand($min, $max) + { + if (!is_array($this->fingerprint)) { + $this->fingerprint = array(); + } + + if ($this->useFingerprint) { + $value = current($this->fingerprint); + next($this->fingerprint); + } else { + $value = mt_rand($min, $max); + $this->fingerprint[] = $value; + } + + return $value; + } + + /** + * @param $x + * @param $y + * @param $nw + * @param $ne + * @param $sw + * @param $se + * + * @return int + */ + protected function interpolate($x, $y, $nw, $ne, $sw, $se) + { + list($r0, $g0, $b0) = $this->getRGB($nw); + list($r1, $g1, $b1) = $this->getRGB($ne); + list($r2, $g2, $b2) = $this->getRGB($sw); + list($r3, $g3, $b3) = $this->getRGB($se); + + $cx = 1.0 - $x; + $cy = 1.0 - $y; + + $m0 = $cx * $r0 + $x * $r1; + $m1 = $cx * $r2 + $x * $r3; + $r = (int) ($cy * $m0 + $y * $m1); + + $m0 = $cx * $g0 + $x * $g1; + $m1 = $cx * $g2 + $x * $g3; + $g = (int) ($cy * $m0 + $y * $m1); + + $m0 = $cx * $b0 + $x * $b1; + $m1 = $cx * $b2 + $x * $b3; + $b = (int) ($cy * $m0 + $y * $m1); + + return ($r << 16) | ($g << 8) | $b; + } + + /** + * @param $image + * @param $x + * @param $y + * + * @return int + */ + protected function getCol($image, $x, $y, $background) + { + $L = imagesx($image); + $H = imagesy($image); + if ($x < 0 || $x >= $L || $y < 0 || $y >= $H) { + return $background; + } + + return imagecolorat($image, $x, $y); + } + + /** + * @param $col + * + * @return array + */ + protected function getRGB($col) + { + return array( + (int) ($col >> 16) & 0xff, + (int) ($col >> 8) & 0xff, + (int) ($col) & 0xff, + ); + } + + /** + * Validate the background image path. Return the image type if valid + * + * @param string $backgroundImage + * @return string + * @throws Exception + */ + protected function validateBackgroundImage($backgroundImage) + { + // check if file exists + if (!file_exists($backgroundImage)) { + $backgroundImageExploded = explode('/', $backgroundImage); + $imageFileName = count($backgroundImageExploded) > 1? $backgroundImageExploded[count($backgroundImageExploded)-1] : $backgroundImage; + + throw new Exception('Invalid background image: ' . $imageFileName); + } + + // check image type + $finfo = finfo_open(FILEINFO_MIME_TYPE); // return mime type ala mimetype extension + $imageType = finfo_file($finfo, $backgroundImage); + finfo_close($finfo); + + if (!in_array($imageType, $this->allowedBackgroundImageTypes)) { + throw new Exception('Invalid background image type! Allowed types are: ' . join(', ', $this->allowedBackgroundImageTypes)); + } + + return $imageType; + } + + /** + * Create background image from type + * + * @param string $backgroundImage + * @param string $imageType + * @return resource + * @throws Exception + */ + protected function createBackgroundImageFromType($backgroundImage, $imageType) + { + switch ($imageType) { + case 'image/jpeg': + $image = imagecreatefromjpeg($backgroundImage); + break; + case 'image/png': + $image = imagecreatefrompng($backgroundImage); + break; + case 'image/gif': + $image = imagecreatefromgif($backgroundImage); + break; + + default: + throw new Exception('Not supported file type for background image!'); + break; + } + + return $image; + } +} diff --git a/includes/Gregwar/Captcha/CaptchaBuilderInterface.php b/includes/Gregwar/Captcha/CaptchaBuilderInterface.php new file mode 100644 index 0000000..bebd070 --- /dev/null +++ b/includes/Gregwar/Captcha/CaptchaBuilderInterface.php @@ -0,0 +1,29 @@ + + * @author Jeremy Livingston + */ +class ImageFileHandler +{ + /** + * Name of folder for captcha images + * @var string + */ + protected $imageFolder; + + /** + * Absolute path to public web folder + * @var string + */ + protected $webPath; + + /** + * Frequency of garbage collection in fractions of 1 + * @var int + */ + protected $gcFreq; + + /** + * Maximum age of images in minutes + * @var int + */ + protected $expiration; + + /** + * @param $imageFolder + * @param $webPath + * @param $gcFreq + * @param $expiration + */ + public function __construct($imageFolder, $webPath, $gcFreq, $expiration) + { + $this->imageFolder = $imageFolder; + $this->webPath = $webPath; + $this->gcFreq = $gcFreq; + $this->expiration = $expiration; + } + + /** + * Saves the provided image content as a file + * + * @param string $contents + * + * @return string + */ + public function saveAsFile($contents) + { + $this->createFolderIfMissing(); + + $filename = md5(uniqid()) . '.jpg'; + $filePath = $this->webPath . '/' . $this->imageFolder . '/' . $filename; + imagejpeg($contents, $filePath, 15); + + return '/' . $this->imageFolder . '/' . $filename; + } + + /** + * Randomly runs garbage collection on the image directory + * + * @return bool + */ + public function collectGarbage() + { + if (!mt_rand(1, $this->gcFreq) == 1) { + return false; + } + + $this->createFolderIfMissing(); + + $finder = new Finder(); + $criteria = sprintf('<= now - %s minutes', $this->expiration); + $finder->in($this->webPath . '/' . $this->imageFolder) + ->date($criteria); + + foreach ($finder->files() as $file) { + unlink($file->getPathname()); + } + + return true; + } + + /** + * Creates the folder if it doesn't exist + */ + protected function createFolderIfMissing() + { + if (!file_exists($this->webPath . '/' . $this->imageFolder)) { + mkdir($this->webPath . '/' . $this->imageFolder, 0755); + } + } +} diff --git a/includes/Gregwar/Captcha/PhraseBuilder.php b/includes/Gregwar/Captcha/PhraseBuilder.php new file mode 100644 index 0000000..5cc938b --- /dev/null +++ b/includes/Gregwar/Captcha/PhraseBuilder.php @@ -0,0 +1,59 @@ + + */ +class PhraseBuilder implements PhraseBuilderInterface +{ + /** + * @var int + */ + public $length; + + /** + * @var string + */ + public $charset; + /** + * Constructs a PhraseBuilder with given parameters + */ + public function __construct($length = 5, $charset = 'abcdefghijklmnpqrstuvwxyz123456789') + { + $this->length = $length; + $this->charset = $charset; + } + + /** + * Generates random phrase of given length with given charset + */ + public function build($length = null, $charset = null) + { + if ($length !== null) { + $this->length = $length; + } + if ($charset !== null) { + $this->charset = $charset; + } + + $phrase = ''; + $chars = str_split($this->charset); + + for ($i = 0; $i < $this->length; $i++) { + $phrase .= $chars[array_rand($chars)]; + } + + return $phrase; + } + + /** + * "Niceize" a code + */ + public function niceize($str) + { + return strtr(strtolower($str), '01', 'ol'); + } +} diff --git a/includes/Gregwar/Captcha/PhraseBuilderInterface.php b/includes/Gregwar/Captcha/PhraseBuilderInterface.php new file mode 100644 index 0000000..62ec4b0 --- /dev/null +++ b/includes/Gregwar/Captcha/PhraseBuilderInterface.php @@ -0,0 +1,21 @@ + + */ +interface PhraseBuilderInterface +{ + /** + * Generates random phrase of given length with given charset + */ + public function build(); + + /** + * "Niceize" a code + */ + public function niceize($str); +} diff --git a/includes/Identicon/Generator/BaseGenerator.php b/includes/Identicon/Generator/BaseGenerator.php new file mode 100644 index 0000000..fa280eb --- /dev/null +++ b/includes/Identicon/Generator/BaseGenerator.php @@ -0,0 +1,250 @@ + + */ +class BaseGenerator +{ + /** + * @var mixed + */ + protected $generatedImage; + + /** + * @var array + */ + protected $color; + + /** + * @var array + */ + protected $backgroundColor; + + /** + * @var int + */ + protected $size; + + /** + * @var int + */ + protected $pixelRatio; + + /** + * @var string + */ + private $hash; + + /** + * @var array + */ + private $arrayOfSquare = []; + + /** + * Set the image color. + * + * @param string|array $color The color in hexa (3 or 6 chars) or rgb array + * + * @return $this + */ + public function setColor($color) + { + if (null === $color) { + return $this; + } + + $this->color = $this->convertColor($color); + + return $this; + } + + /** + * Set the image background color. + * + * @param string|array $backgroundColor The color in hexa (3 or 6 chars) or rgb array + * + * @return $this + */ + public function setBackgroundColor($backgroundColor) + { + if (null === $backgroundColor) { + return $this; + } + + $this->backgroundColor = $this->convertColor($backgroundColor); + + return $this; + } + + /** + * @param array|string $color + * + * @return array + */ + private function convertColor($color) + { + if (is_array($color)) { + return $color; + } + + if (preg_match('/^#?([a-z\d])([a-z\d])([a-z\d])$/i', $color, $matches)) { + $color = $matches[1].$matches[1]; + $color .= $matches[2].$matches[2]; + $color .= $matches[3].$matches[3]; + } + + preg_match('/#?([a-z\d]{2})([a-z\d]{2})([a-z\d]{2})$/i', $color, $matches); + + return array_map(function ($value) { + return hexdec($value); + }, array_slice($matches, 1, 3)); + } + + /** + * Get the color. + * + * @return array + */ + public function getColor() + { + return $this->color; + } + + /** + * Get the background color. + * + * @return array + */ + public function getBackgroundColor() + { + return $this->backgroundColor; + } + + /** + * Convert the hash into an multidimensional array of boolean. + * + * @return $this + */ + private function convertHashToArrayOfBoolean() + { + preg_match_all('/(\w)(\w)/', $this->hash, $chars); + + foreach ($chars[1] as $i => $char) { + $index = (int) ($i / 3); + $data = $this->convertHexaToBoolean($char); + + $items = [ + 0 => [0, 4], + 1 => [1, 3], + 2 => [2], + ]; + + foreach ($items[$i % 3] as $item) { + $this->arrayOfSquare[$index][$item] = $data; + } + + ksort($this->arrayOfSquare[$index]); + } + + $this->color = array_map(function ($data) { + return hexdec($data) * 16; + }, array_reverse($chars[1])); + + return $this; + } + + /** + * Convert an hexadecimal number into a boolean. + * + * @param string $hexa + * + * @return bool + */ + private function convertHexaToBoolean($hexa) + { + return (bool) round(hexdec($hexa) / 10); + } + + /** + * @return array + */ + public function getArrayOfSquare() + { + return $this->arrayOfSquare; + } + + /** + * Get the identicon string hash. + * + * @return string + */ + public function getHash() + { + return $this->hash; + } + + /** + * Generate a hash from the original string. + * + * @param string $string + * + * @throws \Exception + * + * @return $this + */ + public function setString($string) + { + if (null === $string) { + throw new Exception('The string cannot be null.'); + } + + $this->hash = md5($string); + + $this->convertHashToArrayOfBoolean(); + + return $this; + } + + /** + * Set the image size. + * + * @param int $size + * + * @return $this + */ + public function setSize($size) + { + if (null === $size) { + return $this; + } + + $this->size = $size; + $this->pixelRatio = (int) round($size / 5); + + return $this; + } + + /** + * Get the image size. + * + * @return int + */ + public function getSize() + { + return $this->size; + } + + /** + * Get the pixel ratio. + * + * @return int + */ + public function getPixelRatio() + { + return $this->pixelRatio; + } +} diff --git a/includes/Identicon/Generator/GdGenerator.php b/includes/Identicon/Generator/GdGenerator.php new file mode 100644 index 0000000..21ea922 --- /dev/null +++ b/includes/Identicon/Generator/GdGenerator.php @@ -0,0 +1,90 @@ + + */ +class GdGenerator extends BaseGenerator implements GeneratorInterface +{ + /** + * GdGenerator constructor. + */ + public function __construct() + { + if (!extension_loaded('gd') && !extension_loaded('ext-gd')) { + throw new Exception('GD does not appear to be available in your PHP installation. Please try another generator'); + } + } + + /** + * @return string + */ + public function getMimeType() + { + return 'image/png'; + } + + /** + * @return $this + */ + private function generateImage() + { + // prepare image + $this->generatedImage = imagecreatetruecolor($this->getPixelRatio() * 5, $this->getPixelRatio() * 5); + + $rgbBackgroundColor = $this->getBackgroundColor(); + if (null === $rgbBackgroundColor) { + $background = imagecolorallocate($this->generatedImage, 0, 0, 0); + imagecolortransparent($this->generatedImage, $background); + } else { + $background = imagecolorallocate($this->generatedImage, $rgbBackgroundColor[0], $rgbBackgroundColor[1], $rgbBackgroundColor[2]); + imagefill($this->generatedImage, 0, 0, $background); + } + + // prepare color + $rgbColor = $this->getColor(); + $gdColor = imagecolorallocate($this->generatedImage, $rgbColor[0], $rgbColor[1], $rgbColor[2]); + + // draw content + foreach ($this->getArrayOfSquare() as $lineKey => $lineValue) { + foreach ($lineValue as $colKey => $colValue) { + if (true === $colValue) { + imagefilledrectangle($this->generatedImage, $colKey * $this->getPixelRatio(), $lineKey * $this->getPixelRatio(), ($colKey + 1) * $this->getPixelRatio(), ($lineKey + 1) * $this->getPixelRatio(), $gdColor); + } + } + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getImageBinaryData($string, $size = null, $color = null, $backgroundColor = null) + { + ob_start(); + imagepng($this->getImageResource($string, $size, $color, $backgroundColor)); + $imageData = ob_get_contents(); + ob_end_clean(); + + return $imageData; + } + + /** + * {@inheritdoc} + */ + public function getImageResource($string, $size = null, $color = null, $backgroundColor = null) + { + $this + ->setString($string) + ->setSize($size) + ->setColor($color) + ->setBackgroundColor($backgroundColor) + ->generateImage(); + + return $this->generatedImage; + } +} diff --git a/includes/Identicon/Generator/GeneratorInterface.php b/includes/Identicon/Generator/GeneratorInterface.php new file mode 100644 index 0000000..c6e8e0e --- /dev/null +++ b/includes/Identicon/Generator/GeneratorInterface.php @@ -0,0 +1,43 @@ + + */ +interface GeneratorInterface +{ + /** + * @param string $string + * @param int $size + * @param array|string $color + * @param array|string $backgroundColor + * + * @return mixed + */ + public function getImageBinaryData($string, $size = null, $color = null, $backgroundColor = null); + + /** + * @param string $string + * @param int $size + * @param array|string $color + * @param array|string $backgroundColor + * + * @return string + */ + public function getImageResource($string, $size = null, $color = null, $backgroundColor = null); + + /** + * Return the mime-type of this identicon. + * + * @return string + */ + public function getMimeType(); + + /** + * Return the color of the created identicon. + * + * @return array + */ + public function getColor(); +} diff --git a/includes/Identicon/Generator/ImageMagickGenerator.php b/includes/Identicon/Generator/ImageMagickGenerator.php new file mode 100644 index 0000000..0bcd349 --- /dev/null +++ b/includes/Identicon/Generator/ImageMagickGenerator.php @@ -0,0 +1,98 @@ + + */ +class ImageMagickGenerator extends BaseGenerator implements GeneratorInterface +{ + /** + * ImageMagickGenerator constructor. + * + * @throws \Exception + */ + public function __construct() + { + if (!extension_loaded('imagick')) { + throw new Exception('ImageMagick does not appear to be avaliable in your PHP installation. Please try another generator'); + } + } + + /** + * @return string + */ + public function getMimeType() + { + return 'image/png'; + } + + /** + * @return $this + */ + private function generateImage() + { + $this->generatedImage = new \Imagick(); + $rgbBackgroundColor = $this->getBackgroundColor(); + + if (null === $rgbBackgroundColor) { + $background = 'none'; + } else { + $background = new ImagickPixel("rgb($rgbBackgroundColor[0],$rgbBackgroundColor[1],$rgbBackgroundColor[2])"); + } + + $this->generatedImage->newImage($this->pixelRatio * 5, $this->pixelRatio * 5, $background, 'png'); + + // prepare color + $rgbColor = $this->getColor(); + $color = new ImagickPixel("rgb($rgbColor[0],$rgbColor[1],$rgbColor[2])"); + + $draw = new ImagickDraw(); + $draw->setFillColor($color); + + // draw the content + foreach ($this->getArrayOfSquare() as $lineKey => $lineValue) { + foreach ($lineValue as $colKey => $colValue) { + if (true === $colValue) { + $draw->rectangle($colKey * $this->pixelRatio, $lineKey * $this->pixelRatio, ($colKey + 1) * $this->pixelRatio, ($lineKey + 1) * $this->pixelRatio); + } + } + } + + $this->generatedImage->drawImage($draw); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getImageBinaryData($string, $size = null, $color = null, $backgroundColor = null) + { + ob_start(); + echo $this->getImageResource($string, $size, $color, $backgroundColor); + $imageData = ob_get_contents(); + ob_end_clean(); + + return $imageData; + } + + /** + * {@inheritdoc} + */ + public function getImageResource($string, $size = null, $color = null, $backgroundColor = null) + { + $this + ->setString($string) + ->setSize($size) + ->setColor($color) + ->setBackgroundColor($backgroundColor) + ->generateImage(); + + return $this->generatedImage; + } +} diff --git a/includes/Identicon/Generator/SvgGenerator.php b/includes/Identicon/Generator/SvgGenerator.php new file mode 100644 index 0000000..d654a95 --- /dev/null +++ b/includes/Identicon/Generator/SvgGenerator.php @@ -0,0 +1,88 @@ + + */ +class SvgGenerator extends BaseGenerator implements GeneratorInterface +{ + /** + * {@inheritdoc} + */ + public function getMimeType() + { + return 'image/svg+xml'; + } + + /** + * {@inheritdoc} + */ + public function getImageBinaryData($string, $size = null, $color = null, $backgroundColor = null) + { + return $this->getImageResource($string, $size, $color, $backgroundColor); + } + + /** + * {@inheritdoc} + */ + public function getImageResource($string, $size = null, $color = null, $backgroundColor = null) + { + $this + ->setString($string) + ->setSize($size) + ->setColor($color) + ->setBackgroundColor($backgroundColor) + ->_generateImage(); + + return $this->generatedImage; + } + + /** + * @return $this + */ + protected function _generateImage() + { + // prepare image + $w = $this->getPixelRatio() * 5; + $h = $this->getPixelRatio() * 5; + $svg = ''; + + $backgroundColor = '#FFFFFF'; + $rgbBackgroundColor = $this->getBackgroundColor(); + if (!is_null($rgbBackgroundColor)) { + $backgroundColor = $this->_toUnderstandableColor($rgbBackgroundColor); + } + $svg .= ''; + + $rgbColor = $this->_toUnderstandableColor($this->getColor()); + // draw content + foreach ($this->getArrayOfSquare() as $lineKey => $lineValue) { + foreach ($lineValue as $colKey => $colValue) { + if (true === $colValue) { + $svg .= ''; + } + } + } + + $svg .= ''; + + $this->generatedImage = $svg; + + return $this; + } + + /** + * @param array|string $color + * + * @return string + */ + protected function _toUnderstandableColor($color) + { + if (is_array($color)) { + return 'rgb('.implode(', ', $color).')'; + } + + return $color; + } +} diff --git a/includes/Identicon/Identicon.php b/includes/Identicon/Identicon.php new file mode 100644 index 0000000..08d32f5 --- /dev/null +++ b/includes/Identicon/Identicon.php @@ -0,0 +1,123 @@ + + */ +class Identicon +{ + /** + * @var \Identicon\Generator\GeneratorInterface + */ + private $generator; + + /** + * Identicon constructor. + * + * @param \Identicon\Generator\GeneratorInterface|null $generator + */ + public function __construct($generator = null) + { + if (null === $generator) { + $this->generator = new GdGenerator(); + } else { + $this->generator = $generator; + } + } + + /** + * Set the image generator. + * + * @param \Identicon\Generator\GeneratorInterface $generator + * + * @return $this + */ + public function setGenerator(GeneratorInterface $generator) + { + $this->generator = $generator; + + return $this; + } + + /** + * Display an Identicon image. + * + * @param string $string + * @param int $size + * @param string $color + * @param string $backgroundColor + */ + public function displayImage($string, $size = 64, $color = null, $backgroundColor = null) + { + header('Content-Type: '.$this->generator->getMimeType()); + echo $this->getImageData($string, $size, $color, $backgroundColor); + } + + /** + * Get an Identicon PNG image data. + * + * @param string $string + * @param int $size + * @param string $color + * @param string $backgroundColor + * + * @return string + */ + public function getImageData($string, $size = 64, $color = null, $backgroundColor = null) + { + return $this->generator->getImageBinaryData($string, $size, $color, $backgroundColor); + } + + /** + * Get an Identicon PNG image resource. + * + * @param string $string + * @param int $size + * @param string $color + * @param string $backgroundColor + * + * @return string + */ + public function getImageResource($string, $size = 64, $color = null, $backgroundColor = null) + { + return $this->generator->getImageResource($string, $size, $color, $backgroundColor); + } + + /** + * Get an Identicon PNG image data as base 64 encoded. + * + * @param string $string + * @param int $size + * @param string $color + * @param string $backgroundColor + * + * @return string + */ + public function getImageDataUri($string, $size = 64, $color = null, $backgroundColor = null) + { + return sprintf('data:%s;base64,%s', $this->generator->getMimeType(), base64_encode($this->getImageData($string, $size, $color, $backgroundColor))); + } + + /** + * Get the color of the Identicon + * + * Returns an array with RGB values of the Identicon's color. Colors may be NULL if no image has been generated + * so far (e.g., when calling the method on a new Identicon()). + * + * @return array + */ + public function getColor() + { + $colors = $this->generator->getColor(); + + return [ + "r" => $colors[0], + "g" => $colors[1], + "b" => $colors[2] + ]; + } +} diff --git a/includes/OWASP/PureCaptcha.php b/includes/OWASP/PureCaptcha.php new file mode 100644 index 0000000..984d8b3 --- /dev/null +++ b/includes/OWASP/PureCaptcha.php @@ -0,0 +1,244 @@ +ascii=unserialize(gzuncompress(base64_decode( + preg_replace('/\s+/', '', $this->ascii)))); + $this->text=$this->randomText(); + + } + + /** + * Contains the captcha text. + * @var string + */ + public $text; + + /** + * Generates random text for use in captcha + * @param integer $length + * @return string + */ + protected function randomText($length=5) + { + $res=""; + for ($i=0;$i<$length;++$i) + $res.=$this->chars[mt_rand(0,strlen($this->chars)-1)]; + return $res; + } + /** + * returns the index of a char in $chars array + */ + protected function asciiEntry($char) + { + for ($i=0;$ichars);++$i) + if ($this->chars[$i]==$char) return $i; + return -1; + + } + /** + * converts a text to a bitmap + * which is a 2D array of ones and zeroes denoting the text + */ + protected function textBitmap($text,$spacing=2) + { + $width=$this->charWidth; + $height=$this->charHeight; + $result=array(); + $baseY=$baseX=0; + + for ($index=0;$indexascii[$this->asciiEntry($text[$index])][$j][$i]; + for ($i=0;$i<$spacing;++$i) + $result[$baseY+$j][$baseX+$width+$i]=0; + } + $baseX+=$width+$spacing; + } + return $result; + } + /** + * displays a bitmap string on the browser screen + */ + protected function displayBitmap($bitmap) + { + header("Content-Type: image/bmp"); + echo $this->bitmap2bmp($bitmap); + } + + protected function inlineBitmap($bitmap) + { + return base64_encode($this->bitmap2bmp($bitmap)); + } + + /** + * generates a monochrome BMP file + * a bitmap needs to be sent to this function + * i.e a 2D array with every element being either 1 or 0 + * @param integer $width + * @param integer $height + * @param array $bitmap + * @return string + */ + protected function bitmap2bmp($bitmap) + { + $width=count($bitmap[0]); + $height=count($bitmap); + $bytemap=$this->bitmap2bytemap($bitmap); + + $rowSize=floor(($width+31)/32)*4; + $size=$rowSize*$height + 62; //62 metadata size + #bitmap header + $data= "BM"; //header + $data.= (pack('V',$size)); //bitmap size ,4 bytes unsigned little endian + $data.= "RRRR"; + $data.= (pack('V',14+40+8)); //bitmap data start offset , + //4 bytes unsigned little endian, 14 forced, 40 header, 8 colors + + #info header + $data.= pack('V',40); //bitmap header size (min 40), + //4 bytes unsigned little-endian + $data.= (pack('V',$width)); //bitmap width , 4 bytes signed integer + $data.= (pack('V',$height)); //bitmap height , 4 bytes signed integer + $data.= (pack('v',1)); //number of colored plains , 2 bytes + $data.= (pack('v',1)); //color depth , 2 bytes + $data.= (pack('V',0)); //compression algorithm , 4 bytes (0=none, RGB) + $data.= (pack('V',0)); //size of raw data, 0 is fine for no compression + $data.= (pack('V',11808)); //horizontal resolution (dpi), 4 bytes + $data.= (pack('V',11808)); //vertical resolution (dpi), 4 bytes + $data.= (pack('V',0)); //number of colors in pallette (0 = all), 4 bytes + $data.= (pack('V',0)); //number of important colors (0 = all), 4 bytes + + #color palette + $data.= (pack('V',0x00FFFFFF)); //next color, white + $data.= (pack('V',0)); //first color, black + + for ($j=$height-1;$j>=0;--$j) + for ($i=0;$i<$rowSize/4;++$i) + for ($k=0;$k<4;++$k) + if (isset($bytemap[$j][$i*4+$k])) + $data.= pack('C',$bytemap[$j][$i*4+$k]); + else + $data.= pack('C',0); + return $data; + } + /** + * Converts a bitmap to a bytemap, which is necessary for outputting it + * + */ + protected function bitmap2bytemap($bitmap) + { + $width=count($bitmap[0]); + $height=count($bitmap); + $bytemap=array(); + for ($j=0;$j<$height;++$j) + { + for ($i=0;$i<$width/8;++$i) + { + $bitstring=""; + for ($k=0;$k<8;++$k) + if (isset($bitmap[$j][$i*8+$k])) + $bitstring.=$bitmap[$j][$i*8+$k]; + else + $bitstring.="0"; + $bytemap[$j][]=bindec($bitstring); + } + } + return $bytemap; + } + /** + * rotates a bitmap, returning new dimensions with the bitmap + * return bitmap + */ + protected function rotateBitmap($bitmap, $degree) + { + $c=cos(deg2rad($degree)); + $s=sin(deg2rad($degree)); + + $width=count($bitmap[0]); + $height=count($bitmap); + $newHeight=round(abs($width*$s)+abs($height*$c)); + $newWidth=round(abs($width*$c) + abs($height*$s))+1; + $x0 = $width/2 - $c*$newWidth/2 - $s*$newHeight/2; + $y0 = $height/2 - $c*$newHeight/2 + $s*$newWidth/2; + $result=array_fill(0, $newHeight, array_fill(0, $newWidth, 0)); + for ($j=0;$j<$newHeight;++$j) + for ($i=1;$i<$newWidth;++$i) + { + $y=(int)(-$s*$i+$c*$j+$y0); + $x=(int)($c*$i+$s*$j+$x0); + if (isset($bitmap[$y][$x])) + $result[$j][$i]=$bitmap[$y][$x]; + } + return $result; + } + /** + * scales a bitmap to be bigger + */ + protected function scaleBitmap($bitmap,$scaleX,$scaleY) + { + $width=count($bitmap[0]); + $height=count($bitmap); + $newHeight=$height*$scaleY; + $newWidth=$width*$scaleX; + $result=array_fill(0, $newHeight, array_fill(0, $newWidth, 0)); + for ($j=0;$j<$newHeight;++$j) + for ($i=0;$i<$newWidth;++$i) + $result[$j][$i]=$bitmap[(int)($j/$scaleY)] + [(int)($i/$scaleX)]; + return $result; + } + /** + * adds random noise to the captcha + */ + protected function distort($bitmap,$noisePercent=5) + { + for ($j=0;$jtextBitmap($this->text); + $degree=mt_rand(2,4); + if (mt_rand()%100<50) + $degree=-$degree; + $bitmap=$this->rotateBitmap($bitmap,$degree); + $bitmap=$this->scaleBitmap($bitmap,$scale,$scale); + if ($distort) $bitmap=$this->distort($bitmap); + return $this->inlineBitmap($bitmap); + } + +} diff --git a/includes/PIT/Zip.php b/includes/PIT/Zip.php new file mode 100644 index 0000000..5a13562 --- /dev/null +++ b/includes/PIT/Zip.php @@ -0,0 +1,516 @@ + + | @version 0.2.1 [0.2.1] - Beta + | + | @license X11 / MIT License + | @copyright Copyright © 2015 - 2019 SamBrishes, pytesNET + */ +/* + | The following websites contains all required informations, which were unavoidable for the + | creation of this class: + | + | - https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT + | - https://users.cs.jmu.edu/buchhofp/forensics/formats/pkzip.html + | - https://php.net/manual/class.ziparchive.php + */ + + namespace PIT; + + class Zip{ + const FLAGS = "\x00\x00"; + const VERSION = "\x14\x00"; + const SIGNATURE = "\x50\x4b"; + const COMPRESSION = "\x08\x00"; + + /* + | SETTINGs + */ + public $zipArchive = false; + public $compression = 6; + + /* + | ZIP ARCHIVE + */ + private $zipFilename; + private $zipInstance; + + /* + | FALLBACK + */ + private $offset = 0; + private $headers = array(); + private $central = array(); + private $counter = 0; + + /* + | CONSTRUCTOR + | @since 0.2.0 + | + | @param bool TRUE to check and use ZipArchive if available, + | FALSE to use the PKZIP PHP compression per default. + | @return int The compression level between -1 and 9. + */ + public function __construct($ziparchive = false, $compression = 6){ + if($ziparchive){ + $this->zipArchive = class_exists("ZipArchive", false); + $this->zipInstance = ($this->zipArchive)? new ZipArchive(): false; + } + if($this->zipArchive){ + $this->zipFilename = tempnam(sys_get_temp_dir(), "tzp") . ".zip"; + $this->zipInstance->open($this->zipFilename, ZipArchive::CREATE); + } + $this->compression = ($compression >= -1 && $compression <= 9)? $compression: 6; + } + + /* + | DESTRUCTOR + | @since 0.2.0 + */ + public function __destruct(){ + $this->clear(false); + } + + /* + | HELPER :: CONVERT UNIX TO DOS TIME + | @since 0.2.1 + | + | @param int The respective timestamp as INTEGER. + */ + protected function msDOSTime($time){ + $array = getdate((is_int($time) && $time > 0)? $time: time()); + if($array["year"] < 1980 || $array["year"] > 2107){ + $array = getdate(time()); + } + + // Return as DEC + return ( + (($array["year"]-1980 << 25)) | + (($array["mon"] << 21)) | + (($array["mday"] << 16)) | + (($array["hours"] << 11)) | + (($array["minutes"] << 5)) | + (($array["seconds"] >> 1)) + ); + } + + /* + | ADD A FILE + | @since 0.1.0 + | @update 0.2.1 + | + | @param string The relative or absolute filepath or the respective file content. + | @param string The local path within the archive file. + | @param int The timestamp to use. + | @param string The optional file comment or just an empty string. + | + | @return bool TRUE on success, FALSE on failure. + */ + public function addFile($data, $path, $time = 0, $comment = ""){ + if((!is_string($data) && !is_numeric($data)) || !is_string($path)){ + return false; + } + + // Sanitize Data + if(is_string($data) && file_exists($data) && is_file($data)){ + $data = file_get_contents($data); + } + $path = trim(str_replace("\\", "/", $path), "/"); + $time = $this->msDOSTime($time); + + // Zip Archive + if($this->zipArchive){ + return $this->zipInstance->addFromString($path, $data); + } + + // Fallback + $crcval = crc32($data); + $length = strlen($data); + if(version_compare(PHP_VERSION, "5.4.0", ">=")){ + $gzcval = gzcompress($data, $this->compression, ZLIB_ENCODING_DEFLATE); + } else { + $gzcval = gzcompress($data, $this->compression); + } + $gzcval = substr($gzcval, 2, strlen($gzcval) - 6); // Fix CRC-32 Bug + $gzclen = strlen($gzcval); + + /* + | LOCAL FILE HEADER + | 01 SIGNATURE + | 02 Version needed to extract this archive. + | 03 General purpose bit flag. + | 04 Compression method. + | 05 Last modification DOS datetime. + | 06 CRC32 value. + | 07 Compressed Filesize. + | 08 Uncompressed Filesize. + | 09 Length of the filename inside the archive. + | 10 Length of the extra fields. + | 11 The relative path / filename inside the archive. + | 12 The main file data value. + */ + $this->headers[] = + self::SIGNATURE . "\x03\x04" . + self::VERSION . + self::FLAGS . + self::COMPRESSION . + pack("V", $time) . + pack("V", $crcval) . + pack("V", $gzclen) . + pack("V", $length) . + pack("v", strlen($path)) . + pack("v", 0) . + $path . + $gzcval; + + /* + | CENTRAL DIRECTORY RECORD + | 01 SIGNATURE + | 02 MadeBy Version numbers. + | 03 Version needed to extract this archive. + | 04 General purpose bit flag. + | 05 Compression method. + | 06 Last modification DOS datetime. + | 07 CRC32 value. + | 08 Compressed Filesize. + | 09 Uncompressed Filesize. + | 10 Length of the filename inside the archive. + | 11 Length of the extra fields. + | 12 Length of the file comment. + | 13 The disk number where the file exists. + | 14 Internal file attributes. + | 15 External file attributes. + | 16 Offset of the local file header. + | 17 The relative path / filename inside the archive. + | 18 The file comment. + */ + $this->central[] = + self::SIGNATURE . "\x01\x02" . + "\x00\x00" . + self::VERSION . + self::FLAGS . + self::COMPRESSION . + pack("V", $time) . + pack("V", $crcval) . + pack("V", $gzclen) . + pack("V", $length) . + pack("v", strlen($path)) . + pack("v", 0) . + pack("v", strlen($comment)) . + pack("v", 0) . + pack("v", 0) . + pack("V", 32) . + pack("V", $this->offset) . + $path . + $comment; + + // Count Offset and Return + $this->offset += strlen($this->headers[count($this->headers)-1]); + return true; + } + + /* + | ADD MULTIPLE FILES + | @since 0.2.0 + | @update 0.2.1 + | + | @param array Multiple 'local/file/path' => "filepath/or/filecontent" ARRAY pairs. + | @param int The timestamp to use for all files. + | @param string The optional comment or just an empty string for alle respective files. + | + | @return int The number of successfully added elements / files. + */ + public function addFiles($array, $time = 0, $comment = ""){ + if(!is_array($array)){ + return false; + } + foreach($array AS $path => &$data){ + $data = $this->addFile($data, $path); + } + return array_filter(array_values($array)); + } + + /* + | ADD FOLDER + | @since 0.2.0 + | + | @param string The path to the folder, which should be zipped. + | @param string The local path within the zip file. + | @param bool TRUE to zip recursive and include all sub directories, + | FALSE to just zip all files within the $path folder. + | @param bool TRUE to include empty folders on recursive zips. + | FALSE to skip empty folders. + | + | @return multi The number as INT of successfully added elements / files, + | FALSE on failure. + */ + public function addFolder($path, $local = "/", $recursive = false, $empty = false){ + if(!file_exists($path) || !is_dir($path)){ + return false; + } + + // Chech Path + $path = str_replace(array("/", "\\"), DIRECTORY_SEPARATOR, realpath($path)); + if(strpos($path, DIRECTORY_SEPARATOR) !== strlen($path)-1){ + $path .= DIRECTORY_SEPARATOR; + } + + // Check Local + if(!is_string($local)){ + $local = ""; + } + $local = trim(str_replace("\\", "/", $local), "/") . "/"; + + // Start Flow + $this->counter = 0; + $this->addFolderFlow($path, "", $local, !!$recursive, !!$empty); + return $this->counter; + } + + /* + | HELPER :: ADD FOLDER LOOP + | @since 0.2.0 + | + | @param string The base path to the folder, which should be zipped. + | @param string The further path, within the base path, on recursive calls. + | @param string The local path within the zip file. + | @param bool TRUE to zip recursive and include all sub directories, + | FALSE to just zip all files within the $path folder. + | @param bool TRUE to include empty folders on recursive zips. + | FALSE to skip empty folders. + | + | @return int The number of successfully added elements / files. + */ + private function addFolderFlow($base, $path = "", $local = "", $recursive = false, $empty = false){ + $path = str_replace(array("/", "\\"), DIRECTORY_SEPARATOR, $path); + $path = trim($path, DIRECTORY_SEPARATOR); + if(!empty($path)){ + $path .= DIRECTORY_SEPARATOR; + } + + $count = 0; + $handle = opendir($base . $path); + while(($file = readdir($handle)) !== false){ + if(in_array($file, array(".", ".."))){ + continue; + } + if(is_dir($base . $path . $file)){ + if($recursive){ + $count = $this->addFolderFlow($base, $path . $file, $local, $recursive, $empty); + if($count == 0 && $empty){ + $this->addEmptyFolder($local . $path . $file); + } + } + continue; + } + if(is_file($base . $path . $file)){ + if($this->addFile($base . $path . $file, $local . $path . $file)){ + $count++; + $this->counter++; + } + } + } + closedir($handle); + return $count; + } + + /* + | ADD EMPTY FOLDER + | @since 0.2.0 + | @update 0.2.1 + | + | @param string The local path structure within the zip file. + | @param int The timestamp to use. + | @param string The optional file comment or just an empty string. + | + | @return bool TRUE on success, FALSE on failure. + */ + public function addEmptyFolder($path, $time = 0, $comment = ""){ + $path = trim(str_replace("\\", "/", $path), "/") . "/"; + $time = $this->msDOSTime($time); + + // ZipArchive + if($this->zipArchive){ + return $this->zipInstance->addEmptyDir($path); + } + + // Add Header + $this->headers[] = + self::SIGNATURE . "\x03\x04" . + self::VERSION . + self::FLAGS . + "\x00\x00" . + pack("V", $time) . + pack("V", 0) . + pack("V", 0) . + pack("V", 0) . + pack("v", strlen($path)) . + pack("v", 0) . + $path . + ""; + + // Add Central + $this->central[] = + self::SIGNATURE . "\x01\x02" . + "\x14\x03" . + self::VERSION . + self::FLAGS . + "\x00\x00" . + pack("V", $time) . + pack("V", 0) . + pack("V", 0) . + pack("V", 0) . + pack("v", strlen($path)) . + pack("v", 0) . + pack("v", strlen($comment)) . + pack("v", 0) . + pack("v", 0) . + "\x00\x00\xFF\x41" . + pack("V", $this->offset) . + $path . + $comment; + + // Count Offset and Return + $this->offset += strlen($this->headers[count($this->headers)-1]); + return true; + } + + /* + | CLEAR DATA STRINGs + | @since 0.1.0 + | @update 0.2.0 + */ + public function clear($new = true){ + if($this->zipArchive){ + if(is_a($this->zipInstance, "ZipArchive")){ + $this->zipInstance->close(); + } + if(strpos($this->zipFilename, sys_get_temp_dir()) === 0 && file_exists($this->zipFilename)){ + @unlink($this->zipFilename); + } + if($new){ + $this->zipFilename = "./temp-".time().".zip"; + $this->zipInstance = new ZipArchive(); + $this->zipInstance->open($this->zipFilename, ZipArchive::CREATE); + } + } + $this->offset = 0; + $this->headers = array(); + $this->central = array(); + return true; + } + + /* + | DUMBS OUT THE FILE + | @since 0.1.0 + | @update 0.2.1 + */ + public function file(){ + $comment = "PKZipped with https://github.com/SamBrishes/FoxCMS/tree/helpers/zip"; + + // ZipArchive + if($this->zipArchive){ + $this->zipInstance->setArchiveComment($comment); + $this->zipInstance->close(); + + $content = file_get_contents($this->zipFilename); + + $this->zipInstance = new ZipArchive(); + $this->zipInstance->open($this->zipFilename); + return $content; + } + + // Fallback + $headers = implode("", $this->headers); + $central = implode("", $this->central); + /* + | RETURN + | 01 The file header items. + | 02 The central directory items. + | 03 The signature for the end of the central directory record. + | 04 The number of this disk / part. + | 05 The number of the disk / part where the central directory starts. + | 06 The number of central directoy entries on this disk. + | 07 Total number of entries on this disk / part. + | 08 Total number of entries in general. + | 09 Length of the central directory. + | 10 Offset where the central directory starts. + | 11 The length of the following comment field. + | 12 The archive comment. + */ + return $headers . $central . + self::SIGNATURE . "\x05\x06" . + "\x00" . + "\x00" . + "\x00" . + "\x00" . + pack("v", count($this->central)) . + pack("v", count($this->central)) . + pack("V", strlen($central)) . + pack("V", strlen($headers)) . + pack("v", strlen($comment)) . + $comment; + } + + /* + | STORE THE ZIP FILE + | @since 0.1.0 + | @update 0.2.0 + | + | @param string The filename with the respective path to store the archive. + | @param bool TRUE to overwrite existing archives, FALSE to do it not. + | + | @return bool TRUE on success, FALSE on failure. + */ + public function save($filename = "archive.zip", $overwrite = false){ + if(file_exists($filename) && !$overwrite){ + return false; + } + + // Zip Archive + if($this->zipArchive){ + if(is_a($this->zipInstance, "ZipArchive")){ + $this->zipInstance->close(); + } + if(@file_put_contents($filename, file_get_contents($this->zipFilename))){ + @unlink($this->zipFilename); + + $this->zipFilename = $filename; + $this->zipInstance = new ZipArchive(); + return $this->zipInstance->open($this->zipFilename); + } + return false; + } + + // Fallback + return @file_put_contents($filename, $this->file()) !== false; + } + + /* + | DOWNLOAD THE FILE + | @since 0.1.0 + | @update 0.2.0 + | + | @param string The filename for the archiv. + | @param bool TRUE to exit after the execution, FALSE to do it not. + | + | @return void + */ + public function download($filename = "archive.zip", $exit = false){ + $file = $this->file(); + header("Pragma: public"); + header("Expires: 0"); + header("Cache-Control: must-revalidate, post-check=0, pre-check=0"); + header("Cache-Control: private", false); + header("Content-Type: application/zip"); + header("Content-Disposition: attachment; filename={$filename};" ); + header("Content-Transfer-Encoding: binary"); + header("Content-Length: " . strlen($file)); + print ($file); + if($exit){ + die(); + } + } + } diff --git a/includes/autoload.php b/includes/autoload.php new file mode 100644 index 0000000..6139617 --- /dev/null +++ b/includes/autoload.php @@ -0,0 +1,23 @@ + + | @version 0.1.2 [0.1.0] - Alpha + | + | @website https://github.com/pytesNET/snicker + | @license X11 / MIT License + | @copyright Copyright © 2019 SamBrishes, pytesNET + */ + + spl_autoload_register(function($class){ + foreach(array("Gregwar", "Identicon", "PIT", "OWASP") AS $allowed){ + if(strpos($class, $allowed) !== 0){ + continue; + } + $path = dirname(__FILE__) . DIRECTORY_SEPARATOR; + $class = str_replace("\\", DIRECTORY_SEPARATOR, $class); + require_once $class . ".php"; + } + return false; + }); diff --git a/includes/img/default-avatar.jpg b/includes/img/default-avatar.jpg new file mode 100644 index 0000000..40e9aaa Binary files /dev/null and b/includes/img/default-avatar.jpg differ diff --git a/includes/js/identicon.js b/includes/js/identicon.js new file mode 100644 index 0000000..cd351cc --- /dev/null +++ b/includes/js/identicon.js @@ -0,0 +1,205 @@ +/** + * Identicon.js 2.3.3 + * http://github.com/stewartlord/identicon.js + * + * PNGLib required for PNG output + * http://www.xarg.org/download/pnglib.js + * + * Copyright 2018, Stewart Lord + * Released under the BSD license + * http://www.opensource.org/licenses/bsd-license.php + */ + +(function() { + var PNGlib; + if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { + PNGlib = require('./pnglib'); + } else { + PNGlib = window.PNGlib; + } + + var Identicon = function(hash, options){ + if (typeof(hash) !== 'string' || hash.length < 15) { + throw 'A hash of at least 15 characters is required.'; + } + + this.defaults = { + background: [240, 240, 240, 255], + margin: 0.08, + size: 64, + saturation: 0.7, + brightness: 0.5, + format: 'png' + }; + + this.options = typeof(options) === 'object' ? options : this.defaults; + + // backward compatibility with old constructor (hash, size, margin) + if (typeof(arguments[1]) === 'number') { this.options.size = arguments[1]; } + if (arguments[2]) { this.options.margin = arguments[2]; } + + this.hash = hash + this.background = this.options.background || this.defaults.background; + this.size = this.options.size || this.defaults.size; + this.format = this.options.format || this.defaults.format; + this.margin = this.options.margin !== undefined ? this.options.margin : this.defaults.margin; + + // foreground defaults to last 7 chars as hue at 70% saturation, 50% brightness + var hue = parseInt(this.hash.substr(-7), 16) / 0xfffffff; + var saturation = this.options.saturation || this.defaults.saturation; + var brightness = this.options.brightness || this.defaults.brightness; + this.foreground = this.options.foreground || this.hsl2rgb(hue, saturation, brightness); + }; + + Identicon.prototype = { + background: null, + foreground: null, + hash: null, + margin: null, + size: null, + format: null, + + image: function(){ + return this.isSvg() + ? new Svg(this.size, this.foreground, this.background) + : new PNGlib(this.size, this.size, 256); + }, + + render: function(){ + var image = this.image(), + size = this.size, + baseMargin = Math.floor(size * this.margin), + cell = Math.floor((size - (baseMargin * 2)) / 5), + margin = Math.floor((size - cell * 5) / 2), + bg = image.color.apply(image, this.background), + fg = image.color.apply(image, this.foreground); + + // the first 15 characters of the hash control the pixels (even/odd) + // they are drawn down the middle first, then mirrored outwards + var i, color; + for (i = 0; i < 15; i++) { + color = parseInt(this.hash.charAt(i), 16) % 2 ? bg : fg; + if (i < 5) { + this.rectangle(2 * cell + margin, i * cell + margin, cell, cell, color, image); + } else if (i < 10) { + this.rectangle(1 * cell + margin, (i - 5) * cell + margin, cell, cell, color, image); + this.rectangle(3 * cell + margin, (i - 5) * cell + margin, cell, cell, color, image); + } else if (i < 15) { + this.rectangle(0 * cell + margin, (i - 10) * cell + margin, cell, cell, color, image); + this.rectangle(4 * cell + margin, (i - 10) * cell + margin, cell, cell, color, image); + } + } + + return image; + }, + + rectangle: function(x, y, w, h, color, image){ + if (this.isSvg()) { + image.rectangles.push({x: x, y: y, w: w, h: h, color: color}); + } else { + var i, j; + for (i = x; i < x + w; i++) { + for (j = y; j < y + h; j++) { + image.buffer[image.index(i, j)] = color; + } + } + } + }, + + // adapted from: https://gist.github.com/aemkei/1325937 + hsl2rgb: function(h, s, b){ + h *= 6; + s = [ + b += s *= b < .5 ? b : 1 - b, + b - h % 1 * s * 2, + b -= s *= 2, + b, + b + h % 1 * s, + b + s + ]; + + return[ + s[ ~~h % 6 ] * 255, // red + s[ (h|16) % 6 ] * 255, // green + s[ (h|8) % 6 ] * 255 // blue + ]; + }, + + toString: function(raw){ + // backward compatibility with old toString, default to base64 + if (raw) { + return this.render().getDump(); + } else { + return this.render().getBase64(); + } + }, + + isSvg: function(){ + return this.format.match(/svg/i) + } + }; + + var Svg = function(size, foreground, background){ + this.size = size; + this.foreground = this.color.apply(this, foreground); + this.background = this.color.apply(this, background); + this.rectangles = []; + }; + + Svg.prototype = { + size: null, + foreground: null, + background: null, + rectangles: null, + + color: function(r, g, b, a){ + var values = [r, g, b].map(Math.round); + values.push((a >= 0) && (a <= 255) ? a/255 : 1); + return 'rgba(' + values.join(',') + ')'; + }, + + getDump: function(){ + var i, + xml, + rect, + fg = this.foreground, + bg = this.background, + stroke = this.size * 0.005; + + xml = "" + + ""; + + for (i = 0; i < this.rectangles.length; i++) { + rect = this.rectangles[i]; + if (rect.color == bg) continue; + xml += ""; + } + xml += "" + + return xml; + }, + + getBase64: function(){ + if ('function' === typeof btoa) { + return btoa(this.getDump()); + } else if (Buffer) { + return new Buffer(this.getDump(), 'binary').toString('base64'); + } else { + throw 'Cannot generate base64 output'; + } + } + }; + + if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { + module.exports = Identicon; + } else { + window.Identicon = Identicon; + } +})(); diff --git a/includes/js/pnglib.js b/includes/js/pnglib.js new file mode 100644 index 0000000..17c03b6 --- /dev/null +++ b/includes/js/pnglib.js @@ -0,0 +1,214 @@ +/** +* A handy class to calculate color values. +* +* @version 1.0 +* @author Robert Eisele +* @copyright Copyright (c) 2010, Robert Eisele +* @link http://www.xarg.org/2010/03/generate-client-side-png-files-using-javascript/ +* @license http://www.opensource.org/licenses/bsd-license.php BSD License +* +*/ + +(function() { + + // helper functions for that ctx + function write(buffer, offs) { + for (var i = 2; i < arguments.length; i++) { + for (var j = 0; j < arguments[i].length; j++) { + buffer[offs++] = arguments[i].charAt(j); + } + } + } + + function byte2(w) { + return String.fromCharCode((w >> 8) & 255, w & 255); + } + + function byte4(w) { + return String.fromCharCode((w >> 24) & 255, (w >> 16) & 255, (w >> 8) & 255, w & 255); + } + + function byte2lsb(w) { + return String.fromCharCode(w & 255, (w >> 8) & 255); + } + + // modified from original source to support NPM + var PNGlib = function(width,height,depth) { + + this.width = width; + this.height = height; + this.depth = depth; + + // pixel data and row filter identifier size + this.pix_size = height * (width + 1); + + // deflate header, pix_size, block headers, adler32 checksum + this.data_size = 2 + this.pix_size + 5 * Math.floor((0xfffe + this.pix_size) / 0xffff) + 4; + + // offsets and sizes of Png chunks + this.ihdr_offs = 0; // IHDR offset and size + this.ihdr_size = 4 + 4 + 13 + 4; + this.plte_offs = this.ihdr_offs + this.ihdr_size; // PLTE offset and size + this.plte_size = 4 + 4 + 3 * depth + 4; + this.trns_offs = this.plte_offs + this.plte_size; // tRNS offset and size + this.trns_size = 4 + 4 + depth + 4; + this.idat_offs = this.trns_offs + this.trns_size; // IDAT offset and size + this.idat_size = 4 + 4 + this.data_size + 4; + this.iend_offs = this.idat_offs + this.idat_size; // IEND offset and size + this.iend_size = 4 + 4 + 4; + this.buffer_size = this.iend_offs + this.iend_size; // total PNG size + + this.buffer = new Array(); + this.palette = new Object(); + this.pindex = 0; + + var _crc32 = new Array(); + + // initialize buffer with zero bytes + for (var i = 0; i < this.buffer_size; i++) { + this.buffer[i] = "\x00"; + } + + // initialize non-zero elements + write(this.buffer, this.ihdr_offs, byte4(this.ihdr_size - 12), 'IHDR', byte4(width), byte4(height), "\x08\x03"); + write(this.buffer, this.plte_offs, byte4(this.plte_size - 12), 'PLTE'); + write(this.buffer, this.trns_offs, byte4(this.trns_size - 12), 'tRNS'); + write(this.buffer, this.idat_offs, byte4(this.idat_size - 12), 'IDAT'); + write(this.buffer, this.iend_offs, byte4(this.iend_size - 12), 'IEND'); + + // initialize deflate header + var header = ((8 + (7 << 4)) << 8) | (3 << 6); + header+= 31 - (header % 31); + + write(this.buffer, this.idat_offs + 8, byte2(header)); + + // initialize deflate block headers + for (var i = 0; (i << 16) - 1 < this.pix_size; i++) { + var size, bits; + if (i + 0xffff < this.pix_size) { + size = 0xffff; + bits = "\x00"; + } else { + size = this.pix_size - (i << 16) - i; + bits = "\x01"; + } + write(this.buffer, this.idat_offs + 8 + 2 + (i << 16) + (i << 2), bits, byte2lsb(size), byte2lsb(~size)); + } + + /* Create crc32 lookup table */ + for (var i = 0; i < 256; i++) { + var c = i; + for (var j = 0; j < 8; j++) { + if (c & 1) { + c = -306674912 ^ ((c >> 1) & 0x7fffffff); + } else { + c = (c >> 1) & 0x7fffffff; + } + } + _crc32[i] = c; + } + + // compute the index into a png for a given pixel + this.index = function(x,y) { + var i = y * (this.width + 1) + x + 1; + var j = this.idat_offs + 8 + 2 + 5 * Math.floor((i / 0xffff) + 1) + i; + return j; + } + + // convert a color and build up the palette + this.color = function(red, green, blue, alpha) { + + alpha = alpha >= 0 ? alpha : 255; + var color = (((((alpha << 8) | red) << 8) | green) << 8) | blue; + + if (typeof this.palette[color] == "undefined") { + if (this.pindex == this.depth) return "\x00"; + + var ndx = this.plte_offs + 8 + 3 * this.pindex; + + this.buffer[ndx + 0] = String.fromCharCode(red); + this.buffer[ndx + 1] = String.fromCharCode(green); + this.buffer[ndx + 2] = String.fromCharCode(blue); + this.buffer[this.trns_offs+8+this.pindex] = String.fromCharCode(alpha); + + this.palette[color] = String.fromCharCode(this.pindex++); + } + return this.palette[color]; + } + + // output a PNG string, Base64 encoded + this.getBase64 = function() { + + var s = this.getDump(); + + var ch = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; + var c1, c2, c3, e1, e2, e3, e4; + var l = s.length; + var i = 0; + var r = ""; + + do { + c1 = s.charCodeAt(i); + e1 = c1 >> 2; + c2 = s.charCodeAt(i+1); + e2 = ((c1 & 3) << 4) | (c2 >> 4); + c3 = s.charCodeAt(i+2); + if (l < i+2) { e3 = 64; } else { e3 = ((c2 & 0xf) << 2) | (c3 >> 6); } + if (l < i+3) { e4 = 64; } else { e4 = c3 & 0x3f; } + r+= ch.charAt(e1) + ch.charAt(e2) + ch.charAt(e3) + ch.charAt(e4); + } while ((i+= 3) < l); + return r; + } + + // output a PNG string + this.getDump = function() { + + // compute adler32 of output pixels + row filter bytes + var BASE = 65521; /* largest prime smaller than 65536 */ + var NMAX = 5552; /* NMAX is the largest n such that 255n(n+1)/2 + (n+1)(BASE-1) <= 2^32-1 */ + var s1 = 1; + var s2 = 0; + var n = NMAX; + + for (var y = 0; y < this.height; y++) { + for (var x = -1; x < this.width; x++) { + s1+= this.buffer[this.index(x, y)].charCodeAt(0); + s2+= s1; + if ((n-= 1) == 0) { + s1%= BASE; + s2%= BASE; + n = NMAX; + } + } + } + s1%= BASE; + s2%= BASE; + write(this.buffer, this.idat_offs + this.idat_size - 8, byte4((s2 << 16) | s1)); + + // compute crc32 of the PNG chunks + function crc32(png, offs, size) { + var crc = -1; + for (var i = 4; i < size-4; i += 1) { + crc = _crc32[(crc ^ png[offs+i].charCodeAt(0)) & 0xff] ^ ((crc >> 8) & 0x00ffffff); + } + write(png, offs+size-4, byte4(crc ^ -1)); + } + + crc32(this.buffer, this.ihdr_offs, this.ihdr_size); + crc32(this.buffer, this.plte_offs, this.plte_size); + crc32(this.buffer, this.trns_offs, this.trns_size); + crc32(this.buffer, this.idat_offs, this.idat_size); + crc32(this.buffer, this.iend_offs, this.iend_size); + + // convert PNG to string + return "\x89PNG\r\n\x1a\n"+this.buffer.join(''); + } + } + + // modified from original source to support NPM + if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { + module.exports = PNGlib; + } else { + window.PNGlib = PNGlib; + } +})(); diff --git a/languages/de.json b/languages/de.json new file mode 100644 index 0000000..69f50f7 --- /dev/null +++ b/languages/de.json @@ -0,0 +1,6 @@ +{ + "plugin-data": { + "name": "Snicker", + "description": "Ein natives, AJAX-unterstütztes FlatFile Kommentiersystem für Bludit, mit einer Abonnement-Funktion und im Einklang mit der DSGVO!" + } +} diff --git a/languages/de_DE.json b/languages/de_DE.json new file mode 100644 index 0000000..edbc1e5 --- /dev/null +++ b/languages/de_DE.json @@ -0,0 +1,204 @@ +{ + "plugin-data": { + "name": "Snicker", + "description": "Ein natives, AJAX-unterstütztes FlatFile Kommentiersystem für Bludit, mit einer Abonnement-Funktion und im Einklang mit der DSGVO!" + }, + "s18n-a5d491060952aa8ad5fdee071be752de": "Kommentare", + "s18n-de95b43bceeb4b998aed4aed5cef1ae7": "Bearbeiten", + "s18n-f984023ed3d6df2326c9d59838c29792": "Kommentar bearbeiten", + "s18n-86448a506dd93303a72140b9124ee321": "Kommentar entfernen", + "s18n-db10a8eb963bc0e5f4483ac9b5dc554c": "Kommentar-Titel", + "s18n-335630425567dbe91768a3beffdec752": "Kommentar-Inhalt", + "s18n-bd7e63f881c7f787a9a0dce20b7f9e5b": "Meta Einstellungen", + "s18n-1acfe725df7bd12195751f0737c4d375": "Registrierter Nutzer", + "s18n-e988189db402fab453f72052629c02cd": "Kommentar-Benutzername", + "s18n-db84c6236ca6a01b9189504d78c012a5": "Kommentar-eMail", + "s18n-7c6c2e5d48ab37a007cbf70d3ea25fa4": "Ausstehend", + "s18n-787d5f05953ec39b108869dfdd7733e6": "Genehmigt", + "s18n-c7537d6d48ecf261749c09a9f284bd45": "Abgelehnt", + "s18n-e09f6a7593f8ae3994ea57e1117f67ec": "Spam", + "s18n-7a5115c2c1eb662308decbec83593494": "Seite aufrufen", + "s18n-0572a05cd6d6360391993a611099542f": "Kommentar Titel oder Auszug", + "s18n-428f14500191b5d53675da4a96bc8bba": "Kommentare suchen", + "s18n-e0be71bccdeceb713fba3c222f79a3c5": "Keine Kommentare verfügbar", + "s18n-06d4cd63bde972fc66a0aed41d2f5c51": "Kommentar", + "s18n-02bd92faa38aaa6cc0ea75e59937a1ef": "Autor", + "s18n-ebb67a4271abe715344471b0f16321f6": "Aktionen", + "s18n-58566b9a9b2733b0ceacb2186672b5d1": "Zeige alle Antworten", + "s18n-5f44c0081bd862a77ba8b24e923cadf1": "Antwort zu", + "s18n-eb399bcaca686f8609137153307eecf1": "Ändern", + "s18n-2736f4347985da50dc023444c193bfea": "Kommentar bBearbeiten", + "s18n-a107bf4b12e36a07161a26d95b03bc81": "Kommentar genehmigen", + "s18n-da937abd19cd9e1430470b8a471a41d4": "Kommentar ablehnen", + "s18n-32cb4199893d9948cc0853eef244f1fc": "Als Spam markieren", + "s18n-a6494adfb72d12d3a4da66855c284ec6": "Zurück zu ausstehend", + "s18n-1bda80f2be4d3658e0baa43fbe7ae8c1": "Anzeigen", + "s18n-c9ae5a4214e87ad6fbed44a267471eee": "Einstellungen speichern", + "s18n-e124d357c3c832434a8676a5e18db842": "Allgemeine Einstellungen", + "s18n-b62a9dcd666f3ff44197cf21ac66507d": "Kommentar Moderation", + "s18n-7f0217cdcdd58ba86aae84d9d3d79f81": "Moderiere", + "s18n-1a1dc91c907325c69271ddf0c944bc72": "Genehmige", + "s18n-df6963dc912cde9baeef10343167ba01": "jeden Kommentar", + "s18n-39ba4181e212acf183ac965c9b37da89": "Außer der Nutzer ist angemeldet", + "s18n-0dac4426a017f0a0370db32776bc30bb": "Außer der Nutzer ist Admin oder der Autor", + "s18n-c70aa8b8fd6f2652eda2d5366faf1de5": "Außer der Nutzer hat bereits einen genehmigen Kommentar", + "s18n-8b26cab9d444760b4bcc65dc4d8634f8": "Kommentare erlauben", + "s18n-67614909bf9de326de71946036de39f1": "... auf Öffentliche Seiten", + "s18n-a7c384c1b60785c40bc3a4e4dfc5a108": "... auf Fixierte Seiten", + "s18n-9907b78f8745810599dbf6fd29a01364": "... auf Statische Seiten", + "s18n-c89cbecce48d04cd76c0b95c8128ad97": "Aktivieren (Optional)", + "s18n-e9d51286fdd0ff058650392fc8b6ae30": "Aktivieren (Erforderlich)", + "s18n-0aaa87422396fdd678498793b6d5250e": "Deaktivieren", + "s18n-8da2c8185edfeb1765526f8e2e4f388d": "Kommentar Limit", + "s18n-77d6d185c93549dab24f29ff2e3b25a8": "Nutze '0' um das Limit zu deaktivieren!", + "s18n-1ef52691308c8add87723a4103a561c2": "Kommentar-Tiefe", + "s18n-d102731a5fedff24f30e24e883ef4636": "Kommentar-Markups", + "s18n-6640979a191e66655c26c59d404bf955": "Erlaube generelles HTML", + "s18n-915009e874f8bed1845060012f826fcd": "Erlaube Markdown", + "s18n-a9a36cb3d8f4f7297ebca99a322d6342": "Kommentar-Bewertungen", + "s18n-2123546d1ff8b0cb035df0c0b0d06825": "Speicher Gäste-Bewertungen im", + "s18n-dead693ab29895d302fca0e6baad6182": "Cookie Speicher", + "s18n-02b68043bdcae159e83199d64a5abd7d": "Session Speicher", + "s18n-40bd8791e523e91219886c35622163fc": "Datenbank Speicher", + "s18n-8717cfca734e8987971f63b20eeb8024": "Was?", + "s18n-5a8cbcf57f5b59f0d4b8ded97d018399": "Der Cookie Speicher<\/b> befindet sich auf dem Computer des Nutzers, So hast du nicht den vollen Zugriff darauf UND benötigst zudem die Erlaubnis des Nutzers!", + "s18n-a09dbdf907873e66d9d644cca71970d5": "Der Session Speicher<\/b> legt die Daten temporär auf den Server ab, bis der Nutzer den Browser schließt. Dafür benötigst du allerdings auch keine Berechtigung vom Nutzer.", + "s18n-71892ebe01ba92d1f163dc37d818b5ff": "Der Datenbank Speicher<\/b> generiert und speichert einen anonymisierten aber zuweisbaren Wert des Nutzers, welche eine entsprechende Einwilligung benötigt.", + "s18n-7c35a0bcb0b0678f0829036eead5ddca": "Bitte beachte:<\/b> Du bist für die Einholung der Einwilligung des Nutzers verantwortlich, Snicker übernimmt dies lediglich für die Speicherung der Daten über das Kommentar-Formular selbst!", + "s18n-82e5228061f185ee185bd9f3ecba4ee7": "Erlaube die Bewertung: %s", + "s18n-61b58693e0eceeb27ce0cc3b25b3bf31": "Webseiten-Einstellungen", + "s18n-b3c1c2c231275878abe58a55966fa9e0": "Seiten Filter", + "s18n-9cb1eef8966f93282524929f65c8b9ec": "Deaktiviere den Seiten-Filter", + "s18n-e89fd56cefec9baabcbe0db3e5a36962": "Nutze 'pageBegin'", + "s18n-3d08b5dcc1e5c3c7e7e2eaf2d0d6a12d": "Nutze 'pageEnd'", + "s18n-3771d05b6af4ebb0a303266c47809548": "Nutze 'siteBodyBegin'", + "s18n-42438edc41ab83312486009e3122e92b": "Nutze 'siteBodyEnd'", + "s18n-948da5199de32c7601a20b6107c31d4d": "Kommentar-Captcha", + "s18n-47e5c42fb9bdca3636a5d866a6794101": "Deaktiviere das Captcha", + "s18n-eedf6d23d18212016e22428658e17794": "Nutze den lokalen Captcha (von Gregway)", + "s18n-ab6ef7ef94efc86db78218c6c265243a": "Nutze OWASPs PureCaptcha", + "s18n-a58adce0085cc1a25fc8076e97c29d70": "Nutze Gregways Captcha (Die GD Bibliothek fehlt!)", + "s18n-09cddbc3627ea46a8dce692d64273b61": "Nutze Googles reCaptcha (Noch nicht verfügbar)", + "s18n-838a13e4fece7c272b960da3fb99f94d": "Kommentar-Template", + "s18n-6f95370a28520696b2a0ad34efc54d2d": "Kommentar-Reihenfolge", + "s18n-4a8dc1710396b21e7b1da8112c07c4ad": "Die neusten Kommentare zuerst", + "s18n-2dab3b12d0b0642c3964b37d675ff24b": "Die ältesten Kommentare zuerst", + "s18n-230c71c29590608034b4a590a67ace31": "Kommentar-Formular Position", + "s18n-37d988444dec2001c488806fc8401e25": "Zeige das Kommentare-Formular über den Kommentaren", + "s18n-16ac6c11951d825826f77a4097a1c2cb": "Zeige das Kommentare-Formular unter den Kommentaren", + "s18n-1e98ec9312b69676d5e3fe3caf8ecde1": "Kommentare pro Seite", + "s18n-0d46f4389ca5f882e24899fe489bf344": "Nutze '0' um alle Kommentare anzuzeigen!", + "s18n-7b6b84fbd65a4b712a5ba0dccce176d5": "Nutzungsbedingungen", + "s18n-9853383062a2e308d5aed35fe3da7953": "Deaktiviere dieses Feld", + "s18n-352fd1d7225b5ea02b8ddd9fad0d6e34": "Zeige eine Standard-Nachricht (See Zeichenketten)", + "s18n-71860c77c6745379b0d44304d66b6a13": "Seite", + "s18n-5dcb84333ae70a5bed60bf70d34dcd2b": "Zeige den Standard DSGVO Text oder wähle eine Seite für deie Nutzungsbedingungen!", + "s18n-2e3d9327c371afb7489f9b9278198622": "AJAX Script", + "s18n-c60cab6330745b41cbee05603eec6691": "AJAX Script einbetten", + "s18n-69b11f64af515ef979fbf28c5e06f370": "AJAX Script nicht einbetten", + "s18n-9bbec7b57565f06d73522669d3b836dc": "Das AJAX Script übergibt die Anfragen (Kommentare, Bewertungen) direkt ohne den Browser neu zu laden!", + "s18n-7c74c0d2d1c28d1568298c89742ce126": "Kommentar-Profilbild", + "s18n-aeab7c630dae161d8f6e2898dd83b471": "Nutze Gravatar", + "s18n-882e3436da897c055cc3f8bd2598b71a": "Nutze Identicon", + "s18n-d92d61ad0c0065170a37a1805ad1bc9e": "Nutze Mystery Men", + "s18n-b4a51a35344f9a8fa1139cfd968ab308": "Nutze & Bevorzuge das richtige Profilbild bei Nutzern", + "s18n-b50a4a96c25d745d73114af5a4b03145": "Kommentar-Gravatar", + "s18n-9ffb941a398ddee8e054eef3292c546e": "Nutze Mystery Person", + "s18n-a7dd12b1dab17d25467b0b0a4c8d4a92": "Nutze", + "s18n-0d2fc085ee57276417cf380027060760": "Das Standard-Gravatar Bild, wenn der Nutzer keines hat!", + "s18n-58e2aacf5792087168cbc62578584ecd": "Abonnement Einstellungen", + "s18n-8290ca86b8980a14bd46f34017e03f93": "Das Abonnement System ist noch nicht verfügbar!", + "s18n-0bd7ff1b4ac56a9616796bdc05609de2": "eMail Abonnement", + "s18n-208f156d4a803025c284bb595a7576b4": "Aktivieren", + "s18n-dc985a7c2144c6447674e674aee08441": "eMail 'Von' Adresse", + "s18n-f6db5b4db3f9c1ba0ffc091abc561802": "eMail 'Antwort an' Adresse", + "s18n-8d868315d258783a95336d1a6ce27e1e": "eMail Inahlt (Opt-In)", + "s18n-7721b2a2f2a453cc790e3ac7065e9b65": "Nutze die Standard Abonnements eMail", + "s18n-58ff11585a82c73f5117c91c29cb3f63": "eMail Inhalt (Benachrichtigung)", + "s18n-bea9ff19efca028c01617da5dce18171": "Nutze die Standard Benachrichtigungs eMail", + "s18n-e0024a8886a178d4f70b8c888b701680": "Finde weitere Informationen zu benutzerdefinierten eMails %s!", + "s18n-8bcf6629759bd278a5c6266bd9c054f8": "Zeichenketten", + "s18n-3979b4205954030810a8a87769348094": "Standard Danke Nachricht", + "s18n-620b528248b36bf743d1ad33e35022d6": "Danke Nachricht mit Abonnement", + "s18n-e83f6b01d1c81313f6b388281e13aacf": "Danke Nachricht für die Bewertung", + "s18n-01b7a6bc6b57783b4fd081085ff3271e": "Fehler: Unbekannter Fehler", + "s18n-19678b1419eff34dffd41d6778b1aa89": "Fehler: Benutzername ist ungültig", + "s18n-1255e6794d33b443bcee21279d9caa1a": "Fehler: eMail Adresse ist ungültig", + "s18n-ebf545757b92b3a553d83ea3db48beca": "Fehler: Kommentar-Text fehlt", + "s18n-f1bc53e2456c425578277adbc7c90f3a": "Fehler: Kommentar-Titel fehlt", + "s18n-53c8fdd7ed497dbacbef5fd5d4f38f3d": "Fehler: Nutzungsbedingungen fehlen", + "s18n-89f8cc478fdfc0a8f36c1b393f39677a": "Fehler: Als SPAM markiert", + "s18n-59ad5c9fcee1b28a5d004bcf684a5acd": "Fehler: Bereits abgestimmt", + "s18n-56ef2c600b4af0f9f7b35640525967ca": "Nutzungsbedingungen", + "s18n-08400c2e0f51197fdb3590461b15b2cc": "Nutzername oder eMail Adresse", + "s18n-b79edd2e426f90401c04869346b503c7": "Nutzer suchen", + "s18n-ee51fa9d5097c84d2fa6c885bf2d5d84": "Keine Nutzer verfügbar", + "s18n-14c4b06b824ec593239362517f538b29": "Benutzername", + "s18n-0c83f57c786a0b4a39efab23731c7ebc": "eMail", + "s18n-e1260894f59eeae98c8440899de4df8d": "Aktionem", + "s18n-4bc61296b766756f1c7296489633bf32": "Entfernen (Anonymisieren)", + "s18n-c0f4afd3614929f1c803f3a01414a6c7": "Entfernen (Vollständig)", + "s18n-6356f32d0c02c8f90cb59a77e16e8fe2": "Nutzer entblockieren", + "s18n-2327a01afbee025fb5913357c9d6b1b3": "Nutzer blockieren", + "s18n-76a0f6752a45d8af6343ef3e2b6f522a": "Einzelner Kommentar", + "s18n-c426859e50a35617d863cdad2b9c84aa": "Seiten-Kommentare", + "s18n-a64d776275f13a51790bb460774b9129": "Nutzer Kommentare", + "s18n-9bc65c2abec141778ffaa729489f3e87": "Nutzer", + "s18n-ccd1066343c95877b75b79d47c36bebe": "Konfiguration", + "s18n-5b49260517622682a058b69f996d06eb": "Vielen Dank für deinen Kommentar!", + "s18n-74196a783a6f1707a43cc8117f0d9c83": "Vielen Dank für deinen Kommentar, bitte bestätige noch dein Abonnement über die zugesandte eMail!", + "s18n-a939eb542e34cd502b3f7352b2e0f715": "Vielen Dank für deine Bewertung!", + "s18n-d2a9677817ee08ed05bf9fd868669756": "Ein unbekannter Fehler ist aufgetreten, bitte lade die Seite neu!", + "s18n-05b85714aa8f1b364f930e2539059b5e": "Ein Fehler ist aufgetreten: Der Benutzer ist ungültig (Max. 42 Zeichen)!", + "s18n-321e8b481f0ccd62df535256e8e6d2c6": "Ein Fehler ist aufgetreten: Die eMail Adresse ist ungültig!", + "s18n-fcd0c3a087c5123ffdecc20fe9015870": "Ein Fehler ist aufgetreten: Der Kommentar-Text fehlt!", + "s18n-2afa2d90ce3343fbe188b9f49ad5797d": "Ein Fehler ist aufgetreten: Der Kommentar-Titel fehlt!", + "s18n-bee1741efe0c735d2c7180771586faf0": "Ein Fehler ist aufgetreten: Du musst die Nutzungsbedingungen akzeptieren!", + "s18n-92fe96d6ccee901f94fad0000369a9b7": "Ein Fehler ist aufgetreten: Deine IP oder eMail Adresse wurde als SPAM markiert!", + "s18n-9b264fc6137096f8a40acde68f6ae562": "Ein Fehler ist aufgetreten: Du hast diesen Kommentar bereits bewertet!", + "s18n-ca62db4704290ef1a7e65df3ffc7983b": "Ich stimme der Speicherung der Daten (inkl. meiner anonymisierten IP) zu!", + "s18n-01c611362e8b046f32650b85ce161559": "Ein unbekannter Fehler ist aufgetreten!", + "s18n-c4f94c6995b0376f28276e432bed75fa": "Das Sicherheitstoken fehlt!", + "s18n-a573e7b86e41522c7a291846aa109104": "Das Sicherheitstoken ist ungültig!", + "s18n-3e8909518ce4728685aa09cdde3caa22": "Du hast nicht die Berechtigung diese Aktion aufzurufen!", + "s18n-b60a6a8a529a9f0497134205bab15e77": "Du hast nicht die Berechtigung diese Aktion duchzuführen!", + "s18n-3fee90e2f59aeb29b74c1c21648ba712": "Das Captcha Bild konnte erfolgreich erstellt werden!", + "s18n-56abae3e615ae5b5609c32852b777d46": "Die gewünschte Aktion ist ungültig doer unvollständig!", + "s18n-be5136b4f2b33c80e3afd377ee993acb": "Ein unbekannter Fehler ist aufgetreten!", + "s18n-46d4c97e91319867654f7cc80c439ba4": "Diese einzigartige Nutzer-ID existiert nicht!", + "s18n-af1ba1dd4eab5562f78c65bc89a0a7e9": "Die Aktion wurde erfolgreich durchgeführt!", + "s18n-a5193444ee82c18bac726b35a1704d03": "Die Einstellungen wurden erfolgreich aktualisiert!", + "s18n-717c8267d40664ccf7ef25a26ff9cde6": "Das Backup wurde erfolgreich erstellt!", + "s18n-a1e2b7401861cee01c172878f104bd8c": "Kommentare deaktivieren", + "s18n-15802277ea1cdfcbacd6308fa0c7c30f": "Snicker Plugin deaktivieren", + "s18n-877d58f21c87442efa4081112a6cb07a": "Du bist das das Snicker<\/b> Plugin zu deaktivieren, was sämtliche Kommentare löschen würde!", + "s18n-a4983c86683f8d8598c0513339550dc0": "Möchtest du die Kommentare vorher sichern?", + "s18n-4cce9e52118cc659be5070b2f08cdd91": "Das Backup wird unter %s gespeichert!", + "s18n-5f87fd2e0fa992d37c814bb4ca299646": "Ja, Backup erstellen", + "s18n-3d414feb412a1f4cd9324f6411c76329": "Nein, einfach deativieren", + "s18n-10aec35353f9c4096a71c38654c3d402": "Abbrechen", + "s18n-bc6d6a26d44b6f39a0e7b6c7787f3295": "Die Kommentar-Sektion auf dieser Seite wurde deaktiviert!", + "s18n-a8e30d73eddea9866cf99ecd6e8467b5": "Es wurden noch keine Kommentare verfasst, sei der erste!", + "s18n-b3afbadaa2f1c79f8b3999be7fd9719f": "Die Antwort auf das Captcha wurde nicht übermittelt oder war falsch!", + "s18n-fb74aafe8bf1fd4d8f7a6e1ff73028f7": "Die Kommentar ID existiert nicht oder ist ungültig!", + "s18n-25dea7cb70f98250b388f6ab0ddf20cb": "Der Kommentar Status existiert nicht oder ist ungültig!", + "s18n-2cd68855bdc54ff5e3c191a6333ff75d": "Der neue Kommentar Status konnte nicht gespeichert werden!", + "s18n-c0937505b7afa81a053077bc7ae369a5": "Der neue Kommentar Status wurde erfolgreich gespeichert!", + "s18n-6b2c8084c67f24bb73d95031bb570ef7": "Der Kommentar konnte nicht erfolgreich gelöscht werden!", + "s18n-376388311a80dbde63fde7f6c72081e0": "Der Kommentar konnte erfolgreich gelöscht werden!", + "s18n-83bbb9e8745cc95e730fe7b8de9345f1": "Angemeldet als %s (%s)", + "s18n-91fb98e1ac4cf76b7a5b8bae09051e2a": "Dein Benutzername", + "s18n-31f6da7a30e7acf1f82451bfd1a7f8fa": "Deine eMail Adresse", + "s18n-f794080a5a29e35233c82df85f1207eb": "Dein Kommentar...", + "s18n-a363b8d13575101a0226e8d0d054f2e7": "Antworten", + "s18n-f10db888c5e63b343000cffc038e0a46": "schrieb", + "s18n-3cc5bcf15d6b8faed118e2ce72d19a1e": "Ich stimme der %s zu!", + "s18n-2af0aab477f402e0f4ad7a27e6c9f952": "Vorherigen Kommentare", + "s18n-8538431db22040e2147b363f86a2e2f0": "Nächsten Kommentare", + "s18n-ae0dbd5cc42a6db191db5e0083bcb307": "Dieser Kommentare wurde noch nicht moderiert!", + "s18n-48df9c3f3cca3fb2b8bcf811633bee06": "Geschrieben von %s", + "s18n-81fdc9813cebe0553c55e78dc2b6029f": "am %s", + "s18n-be1ab1632e4285edc3733b142935c60b": "Gefällt Mir", + "s18n-bc8b79025e4595298669fd21da814941": "Gefällt Mir nicht", + "s18n-e84afaab83ecb301b3d97ce4174d2773": "Antworten" +} diff --git a/languages/en.json b/languages/en.json new file mode 100644 index 0000000..4748a7d --- /dev/null +++ b/languages/en.json @@ -0,0 +1,204 @@ +{ + "plugin-data": { + "name": "Snicker", + "description": "A native, AJAX-enabled FlatFile Comment system for bludit, including a comment subscription and completely compliant with the GDPR!" + }, + "s18n-a5d491060952aa8ad5fdee071be752de": "Comments", + "s18n-de95b43bceeb4b998aed4aed5cef1ae7": "Edit", + "s18n-f984023ed3d6df2326c9d59838c29792": "Update Comment", + "s18n-86448a506dd93303a72140b9124ee321": "Delete Comment", + "s18n-db10a8eb963bc0e5f4483ac9b5dc554c": "Comment Title", + "s18n-335630425567dbe91768a3beffdec752": "Comment Text", + "s18n-bd7e63f881c7f787a9a0dce20b7f9e5b": "Meta Settings", + "s18n-1acfe725df7bd12195751f0737c4d375": "Registered User", + "s18n-e988189db402fab453f72052629c02cd": "Comment Username", + "s18n-db84c6236ca6a01b9189504d78c012a5": "Comment Email", + "s18n-7c6c2e5d48ab37a007cbf70d3ea25fa4": "Pending", + "s18n-787d5f05953ec39b108869dfdd7733e6": "Approved", + "s18n-c7537d6d48ecf261749c09a9f284bd45": "Rejected", + "s18n-e09f6a7593f8ae3994ea57e1117f67ec": "Spam", + "s18n-7a5115c2c1eb662308decbec83593494": "View Page", + "s18n-0572a05cd6d6360391993a611099542f": "Comment Title or Excerpt", + "s18n-428f14500191b5d53675da4a96bc8bba": "Search Comments", + "s18n-e0be71bccdeceb713fba3c222f79a3c5": "No Comments available", + "s18n-06d4cd63bde972fc66a0aed41d2f5c51": "Comment", + "s18n-02bd92faa38aaa6cc0ea75e59937a1ef": "Author", + "s18n-ebb67a4271abe715344471b0f16321f6": "Actions", + "s18n-58566b9a9b2733b0ceacb2186672b5d1": "Show all replies", + "s18n-5f44c0081bd862a77ba8b24e923cadf1": "Reply To", + "s18n-eb399bcaca686f8609137153307eecf1": "Change", + "s18n-2736f4347985da50dc023444c193bfea": "Edit Comment", + "s18n-a107bf4b12e36a07161a26d95b03bc81": "Approve Comment", + "s18n-da937abd19cd9e1430470b8a471a41d4": "Reject Comment", + "s18n-32cb4199893d9948cc0853eef244f1fc": "Mark as Spam", + "s18n-a6494adfb72d12d3a4da66855c284ec6": "Back to Pending", + "s18n-1bda80f2be4d3658e0baa43fbe7ae8c1": "View", + "s18n-c9ae5a4214e87ad6fbed44a267471eee": "Save Settings", + "s18n-e124d357c3c832434a8676a5e18db842": "General Settings", + "s18n-b62a9dcd666f3ff44197cf21ac66507d": "Comment Moderation", + "s18n-7f0217cdcdd58ba86aae84d9d3d79f81": "Moderate", + "s18n-1a1dc91c907325c69271ddf0c944bc72": "Pass", + "s18n-df6963dc912cde9baeef10343167ba01": "each Comment", + "s18n-39ba4181e212acf183ac965c9b37da89": "Unless the user is logged in", + "s18n-0dac4426a017f0a0370db32776bc30bb": "Unless the user is admin or the content author", + "s18n-c70aa8b8fd6f2652eda2d5366faf1de5": "Unless the user has an already approved comment", + "s18n-8b26cab9d444760b4bcc65dc4d8634f8": "Allow Comments", + "s18n-67614909bf9de326de71946036de39f1": "... on Public Pages", + "s18n-a7c384c1b60785c40bc3a4e4dfc5a108": "... on Sticky Pages", + "s18n-9907b78f8745810599dbf6fd29a01364": "... on Static Pages", + "s18n-c89cbecce48d04cd76c0b95c8128ad97": "Enable (Optional)", + "s18n-e9d51286fdd0ff058650392fc8b6ae30": "Enable (Required)", + "s18n-0aaa87422396fdd678498793b6d5250e": "Disable", + "s18n-8da2c8185edfeb1765526f8e2e4f388d": "Comment Limit", + "s18n-77d6d185c93549dab24f29ff2e3b25a8": "Use '0' to disable any limit!", + "s18n-1ef52691308c8add87723a4103a561c2": "Comment Depth", + "s18n-d102731a5fedff24f30e24e883ef4636": "Comment Markup", + "s18n-6640979a191e66655c26c59d404bf955": "Allow Basic HTML", + "s18n-915009e874f8bed1845060012f826fcd": "Allow Markdown", + "s18n-a9a36cb3d8f4f7297ebca99a322d6342": "Comment Voting", + "s18n-2123546d1ff8b0cb035df0c0b0d06825": "Store Votes made by Guests in the", + "s18n-dead693ab29895d302fca0e6baad6182": "Cookie Storage", + "s18n-02b68043bdcae159e83199d64a5abd7d": "Session Storage", + "s18n-40bd8791e523e91219886c35622163fc": "Database Storage", + "s18n-8717cfca734e8987971f63b20eeb8024": "What?", + "s18n-5a8cbcf57f5b59f0d4b8ded97d018399": "The Cookie Storage<\/b> is located on the Computer of the user. So you don't have the full control AND you require the appropriate permissions from the user.", + "s18n-a09dbdf907873e66d9d644cca71970d5": "The Session Storage<\/b> is just stored temporary on the server, it gets cleaned up when the user closes the browser. Therefore you don't need any permissions from the user.", + "s18n-71892ebe01ba92d1f163dc37d818b5ff": "The Database Storage<\/b> generates and stores an anonymized but assignable value of the user, which also requires the appropriate permissions from the user.", + "s18n-7c35a0bcb0b0678f0829036eead5ddca": "Please Note:<\/b> You are responsible for obtaining the appropriate permissions, Snicker just handles the permissions for data sent (and stored) via the comment form!", + "s18n-82e5228061f185ee185bd9f3ecba4ee7": "Allow to %s comments", + "s18n-61b58693e0eceeb27ce0cc3b25b3bf31": "Frontend Settings", + "s18n-b3c1c2c231275878abe58a55966fa9e0": "Page Filter", + "s18n-9cb1eef8966f93282524929f65c8b9ec": "Disable Page Filter", + "s18n-e89fd56cefec9baabcbe0db3e5a36962": "Use 'pageBegin'", + "s18n-3d08b5dcc1e5c3c7e7e2eaf2d0d6a12d": "Use 'pageEnd'", + "s18n-3771d05b6af4ebb0a303266c47809548": "Use 'siteBodyBegin'", + "s18n-42438edc41ab83312486009e3122e92b": "Use 'siteBodyEnd'", + "s18n-948da5199de32c7601a20b6107c31d4d": "Comment Captcha", + "s18n-47e5c42fb9bdca3636a5d866a6794101": "Disable Captcha", + "s18n-ab6ef7ef94efc86db78218c6c265243a": "Use OWASP's PureCaptcha", + "s18n-a58adce0085cc1a25fc8076e97c29d70": "Use Gregway's Captcha", + "s18n-9d2f2ec577e7383b88fd481d6c566c5e": "Use Gregway's Captcha (GD library is missing!)", + "s18n-09cddbc3627ea46a8dce692d64273b61": "Use Googles reCaptcha (Not available yet)", + "s18n-838a13e4fece7c272b960da3fb99f94d": "Comment Template", + "s18n-6f95370a28520696b2a0ad34efc54d2d": "Comment Order", + "s18n-4a8dc1710396b21e7b1da8112c07c4ad": "Newest Comments First", + "s18n-2dab3b12d0b0642c3964b37d675ff24b": "Oldest Comments First", + "s18n-230c71c29590608034b4a590a67ace31": "Comment Form Position", + "s18n-37d988444dec2001c488806fc8401e25": "Show Comment Form above Comments", + "s18n-16ac6c11951d825826f77a4097a1c2cb": "Show Comment Form below Comments", + "s18n-1e98ec9312b69676d5e3fe3caf8ecde1": "Comments Per Page", + "s18n-0d46f4389ca5f882e24899fe489bf344": "Use '0' to show all available comments!", + "s18n-7b6b84fbd65a4b712a5ba0dccce176d5": "Terms of Use Checkbox", + "s18n-9853383062a2e308d5aed35fe3da7953": "Disable this field", + "s18n-352fd1d7225b5ea02b8ddd9fad0d6e34": "Show Message (See Strings)", + "s18n-71860c77c6745379b0d44304d66b6a13": "Page", + "s18n-5dcb84333ae70a5bed60bf70d34dcd2b": "Show the default GDPR Text or Select your own static 'Terms of Use' page!", + "s18n-2e3d9327c371afb7489f9b9278198622": "AJAX Script", + "s18n-c60cab6330745b41cbee05603eec6691": "Embed AJAX Script", + "s18n-69b11f64af515ef979fbf28c5e06f370": "Don't use AJAX", + "s18n-9bbec7b57565f06d73522669d3b836dc": "The AJAX Script hands over the request (comment, like, dislike) directly without reloading the page!", + "s18n-7c74c0d2d1c28d1568298c89742ce126": "Comment Avatar", + "s18n-aeab7c630dae161d8f6e2898dd83b471": "Use Gravatar", + "s18n-882e3436da897c055cc3f8bd2598b71a": "Use Identicon", + "s18n-d92d61ad0c0065170a37a1805ad1bc9e": "Use Mystery Men", + "s18n-b4a51a35344f9a8fa1139cfd968ab308": "Use & Prefer profile pictures on logged-in Users", + "s18n-b50a4a96c25d745d73114af5a4b03145": "Comment Gravatar", + "s18n-9ffb941a398ddee8e054eef3292c546e": "Show Mystery Person", + "s18n-a7dd12b1dab17d25467b0b0a4c8d4a92": "Show", + "s18n-0d2fc085ee57276417cf380027060760": "The default Gravatar image, if the user has no Gravatar!", + "s18n-58e2aacf5792087168cbc62578584ecd": "Subscription Settings", + "s18n-8290ca86b8980a14bd46f34017e03f93": "The Subscription system isn't available yet!", + "s18n-0bd7ff1b4ac56a9616796bdc05609de2": "Email Subscription", + "s18n-208f156d4a803025c284bb595a7576b4": "Enable", + "s18n-dc985a7c2144c6447674e674aee08441": "Email 'From' Address", + "s18n-f6db5b4db3f9c1ba0ffc091abc561802": "Email 'ReplyTo' Address", + "s18n-8d868315d258783a95336d1a6ce27e1e": "Email Body (Opt-In)", + "s18n-7721b2a2f2a453cc790e3ac7065e9b65": "Use default Subscription Email", + "s18n-58ff11585a82c73f5117c91c29cb3f63": "Email Body (Notification)", + "s18n-bea9ff19efca028c01617da5dce18171": "Use default Notification Email", + "s18n-e0024a8886a178d4f70b8c888b701680": "Read more about a custom Notification Emails %s!", + "s18n-8bcf6629759bd278a5c6266bd9c054f8": "Strings", + "s18n-3979b4205954030810a8a87769348094": "Default Thanks Message", + "s18n-620b528248b36bf743d1ad33e35022d6": "Thanks Message with Subscription", + "s18n-e83f6b01d1c81313f6b388281e13aacf": "Thanks Message for Voting", + "s18n-01b7a6bc6b57783b4fd081085ff3271e": "Error: Unknown Error, Try again", + "s18n-19678b1419eff34dffd41d6778b1aa89": "Error: Username is invalid", + "s18n-1255e6794d33b443bcee21279d9caa1a": "Error: Email Address is invalid", + "s18n-ebf545757b92b3a553d83ea3db48beca": "Error: Comment Text is missing", + "s18n-f1bc53e2456c425578277adbc7c90f3a": "Error: Comment Title is missing", + "s18n-53c8fdd7ed497dbacbef5fd5d4f38f3d": "Error: Terms not accepted", + "s18n-89f8cc478fdfc0a8f36c1b393f39677a": "Error: Marked as SPAM", + "s18n-59ad5c9fcee1b28a5d004bcf684a5acd": "Error: Already Voted", + "s18n-56ef2c600b4af0f9f7b35640525967ca": "Terms of Use", + "s18n-08400c2e0f51197fdb3590461b15b2cc": "Username or Email Address", + "s18n-b79edd2e426f90401c04869346b503c7": "Search Users", + "s18n-ee51fa9d5097c84d2fa6c885bf2d5d84": "No Users available", + "s18n-14c4b06b824ec593239362517f538b29": "Username", + "s18n-0c83f57c786a0b4a39efab23731c7ebc": "Email", + "s18n-e1260894f59eeae98c8440899de4df8d": "Handle", + "s18n-4bc61296b766756f1c7296489633bf32": "Delete (Anonymize)", + "s18n-c0f4afd3614929f1c803f3a01414a6c7": "Delete (Completely)", + "s18n-6356f32d0c02c8f90cb59a77e16e8fe2": "Unblock User", + "s18n-2327a01afbee025fb5913357c9d6b1b3": "Block User", + "s18n-76a0f6752a45d8af6343ef3e2b6f522a": "Single Comment", + "s18n-c426859e50a35617d863cdad2b9c84aa": "Page Comments", + "s18n-a64d776275f13a51790bb460774b9129": "User Comments", + "s18n-9bc65c2abec141778ffaa729489f3e87": "Users", + "s18n-ccd1066343c95877b75b79d47c36bebe": "Configuration", + "s18n-5b49260517622682a058b69f996d06eb": "Thanks for your comment!", + "s18n-74196a783a6f1707a43cc8117f0d9c83": "Thanks for your comment, please confirm your subscription via the link we sent to your Email address!", + "s18n-a939eb542e34cd502b3f7352b2e0f715": "Thanks for voting this comment!", + "s18n-d2a9677817ee08ed05bf9fd868669756": "An unknown error occured, please reload the page and try it again!", + "s18n-05b85714aa8f1b364f930e2539059b5e": "An error occured: The passed Username is invalid or too long (42 characters only)!", + "s18n-321e8b481f0ccd62df535256e8e6d2c6": "An error occured: The passed Email address is invalid!", + "s18n-fcd0c3a087c5123ffdecc20fe9015870": "An error occured: The comment text is missing!", + "s18n-2afa2d90ce3343fbe188b9f49ad5797d": "An error occured: The comment title is missing!", + "s18n-bee1741efe0c735d2c7180771586faf0": "An error occured: You need to accept the Terms to comment!", + "s18n-92fe96d6ccee901f94fad0000369a9b7": "An error occured: Your IP address or Email address has been marked as Spam!", + "s18n-9b264fc6137096f8a40acde68f6ae562": "An error occured: You already rated this comment!", + "s18n-ca62db4704290ef1a7e65df3ffc7983b": "I agree that my data (incl. my anonymized IP address) gets stored!", + "s18n-01c611362e8b046f32650b85ce161559": "An unknown error occured!", + "s18n-c4f94c6995b0376f28276e432bed75fa": "The CSRF Token is missing!", + "s18n-a573e7b86e41522c7a291846aa109104": "The CSRF Token is invalid!", + "s18n-3e8909518ce4728685aa09cdde3caa22": "You don't have the permission to call this action!", + "s18n-b60a6a8a529a9f0497134205bab15e77": "You don't have the permission to perform this action!", + "s18n-3fee90e2f59aeb29b74c1c21648ba712": "The Captcha image could be successfully created!", + "s18n-56abae3e615ae5b5609c32852b777d46": "The passed action is unknown or invalid!", + "s18n-be5136b4f2b33c80e3afd377ee993acb": "An unknown error is occured!", + "s18n-46d4c97e91319867654f7cc80c439ba4": "A unique user ID does not exist!", + "s18n-af1ba1dd4eab5562f78c65bc89a0a7e9": "The action has been performed successfully!", + "s18n-a5193444ee82c18bac726b35a1704d03": "The settings has been updated successfully!", + "s18n-717c8267d40664ccf7ef25a26ff9cde6": "The backup has been created successfully!", + "s18n-a1e2b7401861cee01c172878f104bd8c": "Disallow Comments", + "s18n-15802277ea1cdfcbacd6308fa0c7c30f": "Snicker Plugin Deactivation", + "s18n-877d58f21c87442efa4081112a6cb07a": "You are about to deactivate the Snicker<\/b> Plugin, which will delete all written comments!", + "s18n-a4983c86683f8d8598c0513339550dc0": "Do you want to backup your comments before?", + "s18n-4cce9e52118cc659be5070b2f08cdd91": "The backup will be stored in %s!", + "s18n-5f87fd2e0fa992d37c814bb4ca299646": "Yes, create a backup", + "s18n-3d414feb412a1f4cd9324f6411c76329": "No, just deactivate", + "s18n-10aec35353f9c4096a71c38654c3d402": "Cancel", + "s18n-bc6d6a26d44b6f39a0e7b6c7787f3295": "The comment section on this page has been disabled!", + "s18n-a8e30d73eddea9866cf99ecd6e8467b5": "Currently there are no comments, so be the first!", + "s18n-b3afbadaa2f1c79f8b3999be7fd9719f": "The answer to the Captcha hasn't been passed or is wrong!", + "s18n-fb74aafe8bf1fd4d8f7a6e1ff73028f7": "The comment UID doesn't exist or is invalid!", + "s18n-25dea7cb70f98250b388f6ab0ddf20cb": "The comment status is unknown or invalid!", + "s18n-2cd68855bdc54ff5e3c191a6333ff75d": "The new comment status couldn't be updated!", + "s18n-c0937505b7afa81a053077bc7ae369a5": "The new comment status has been stored successfully!", + "s18n-6b2c8084c67f24bb73d95031bb570ef7": "The comment couldn't deleted!", + "s18n-376388311a80dbde63fde7f6c72081e0": "The comment has been deleted!", + "s18n-83bbb9e8745cc95e730fe7b8de9345f1": "Logged in as %s (%s)", + "s18n-91fb98e1ac4cf76b7a5b8bae09051e2a": "Your Username", + "s18n-31f6da7a30e7acf1f82451bfd1a7f8fa": "Your Email Address", + "s18n-f794080a5a29e35233c82df85f1207eb": "Your Comment...", + "s18n-a363b8d13575101a0226e8d0d054f2e7": "Answer", + "s18n-f10db888c5e63b343000cffc038e0a46": "wrote", + "s18n-3cc5bcf15d6b8faed118e2ce72d19a1e": "I agree the %s!", + "s18n-2af0aab477f402e0f4ad7a27e6c9f952": "Previous Comments", + "s18n-8538431db22040e2147b363f86a2e2f0": "Next Comments", + "s18n-ae0dbd5cc42a6db191db5e0083bcb307": "This comment hasn't been moderated yet!", + "s18n-48df9c3f3cca3fb2b8bcf811633bee06": "Written by %s", + "s18n-81fdc9813cebe0553c55e78dc2b6029f": "on %s", + "s18n-be1ab1632e4285edc3733b142935c60b": "Like", + "s18n-bc8b79025e4595298669fd21da814941": "Dislike", + "s18n-e84afaab83ecb301b3d97ce4174d2773": "Reply" +} diff --git a/languages/es.json b/languages/es.json new file mode 100644 index 0000000..e96c7e3 --- /dev/null +++ b/languages/es.json @@ -0,0 +1,200 @@ +{ + "plugin-data": { + "name": "Snicker", + "description": "Sistema de comentarios para Bludit. Incluye suscripción a comentarios y es completamente compatible con el RGPD (Reglamento General de Protección de datos). Usa tecnologías AJAX y Flatfile. Ligero y seguro." }, + "s18n-a5d491060952aa8ad5fdee071be752de": "Comentarios", + "s18n-de95b43bceeb4b998aed4aed5cef1ae7": "Editar", + "s18n-f984023ed3d6df2326c9d59838c29792": "Actualizar", + "s18n-86448a506dd93303a72140b9124ee321": "Borrar", + "s18n-db10a8eb963bc0e5f4483ac9b5dc554c": "Título de comentario", + "s18n-335630425567dbe91768a3beffdec752": "Texto de comentario", + "s18n-bd7e63f881c7f787a9a0dce20b7f9e5b": "Configuración de metadatos", + "s18n-1acfe725df7bd12195751f0737c4d375": "Registro de usuario", + "s18n-e988189db402fab453f72052629c02cd": "Usuario", + "s18n-db84c6236ca6a01b9189504d78c012a5": "Email", + "s18n-7c6c2e5d48ab37a007cbf70d3ea25fa4": "Pendiente", + "s18n-787d5f05953ec39b108869dfdd7733e6": "Aprobado", + "s18n-c7537d6d48ecf261749c09a9f284bd45": "Rechazado", + "s18n-e09f6a7593f8ae3994ea57e1117f67ec": "Spam", + "s18n-7a5115c2c1eb662308decbec83593494": "Ver página", + "s18n-0572a05cd6d6360391993a611099542f": "Título de comentario", + "s18n-428f14500191b5d53675da4a96bc8bba": "Buscar comentarios", + "s18n-e0be71bccdeceb713fba3c222f79a3c5": "No hay comentarios aun", + "s18n-06d4cd63bde972fc66a0aed41d2f5c51": "Comentar", + "s18n-02bd92faa38aaa6cc0ea75e59937a1ef": "Autor", + "s18n-ebb67a4271abe715344471b0f16321f6": "Acciones", + "s18n-58566b9a9b2733b0ceacb2186672b5d1": "Mostrar todas las respuestas", + "s18n-5f44c0081bd862a77ba8b24e923cadf1": "Responder a", + "s18n-eb399bcaca686f8609137153307eecf1": "Cambiar", + "s18n-2736f4347985da50dc023444c193bfea": "Editar Comentario", + "s18n-a107bf4b12e36a07161a26d95b03bc81": "Aprobar Comentario", + "s18n-da937abd19cd9e1430470b8a471a41d4": "Rechazar Comentario", + "s18n-32cb4199893d9948cc0853eef244f1fc": "Marcar como espam", + "s18n-a6494adfb72d12d3a4da66855c284ec6": "Archivar como pendiente", + "s18n-1bda80f2be4d3658e0baa43fbe7ae8c1": "Ver", + "s18n-c9ae5a4214e87ad6fbed44a267471eee": "Guardar la configuración", + "s18n-e124d357c3c832434a8676a5e18db842": "Configuración General", + "s18n-b62a9dcd666f3ff44197cf21ac66507d": "Moderación", + "s18n-7f0217cdcdd58ba86aae84d9d3d79f81": "Moderar", + "s18n-1a1dc91c907325c69271ddf0c944bc72": "Aprobar", + "s18n-df6963dc912cde9baeef10343167ba01": "por defecto", + "s18n-39ba4181e212acf183ac965c9b37da89": "Aprobar usuarios identificados", + "s18n-0dac4426a017f0a0370db32776bc30bb": "Aprobar roles de admin o autor", + "s18n-c70aa8b8fd6f2652eda2d5366faf1de5": "Aprobar si ya tiene algún comentario aprobado", + "s18n-8b26cab9d444760b4bcc65dc4d8634f8": "Permitir comentarios", + "s18n-67614909bf9de326de71946036de39f1": "... en páginas Publicadas", + "s18n-a7c384c1b60785c40bc3a4e4dfc5a108": "... en páginas Ancladas (sticky)", + "s18n-9907b78f8745810599dbf6fd29a01364": "... en páginas Estáticas", + "s18n-c89cbecce48d04cd76c0b95c8128ad97": "Activo (Opcional)", + "s18n-e9d51286fdd0ff058650392fc8b6ae30": "Activo (Requerido)", + "s18n-0aaa87422396fdd678498793b6d5250e": "Inactivo", + "s18n-8da2c8185edfeb1765526f8e2e4f388d": "Limite de comentarios", + "s18n-77d6d185c93549dab24f29ff2e3b25a8": "'0' si no deseas límite!", + "s18n-1ef52691308c8add87723a4103a561c2": "Extensión de comentario", + "s18n-d102731a5fedff24f30e24e883ef4636": "Formato de comentario", + "s18n-6640979a191e66655c26c59d404bf955": "Permitir HTML", + "s18n-915009e874f8bed1845060012f826fcd": "Permitir Markdown", + "s18n-a9a36cb3d8f4f7297ebca99a322d6342": "Votos de comentarios", + "s18n-2123546d1ff8b0cb035df0c0b0d06825": "Los votos se almacenan en", + "s18n-dead693ab29895d302fca0e6baad6182": "Cookies", + "s18n-02b68043bdcae159e83199d64a5abd7d": "Sesiones", + "s18n-40bd8791e523e91219886c35622163fc": "Base de datos", + "s18n-8717cfca734e8987971f63b20eeb8024": "Qué?", + "s18n-5a8cbcf57f5b59f0d4b8ded97d018399": "Las Cookies<\/b> se localizan en la computadora del usuario. Precisas su consentimiento y no tienes control", + "s18n-a09dbdf907873e66d9d644cca71970d5": "Las Sesiones<\/b> se guardan en el servidor temporalmente. Desaparecen cuando caducan o el usuario cierra el navegador. No precisas permiso del usuario.", + "s18n-71892ebe01ba92d1f163dc37d818b5ff": "La Base de datos<\/b> se guardan indefinidamente en el servido, pero como identifican al usuario, precisaras de su permiso.", + "s18n-7c35a0bcb0b0678f0829036eead5ddca": "Atención!!:<\/b> usted es responsable de obtener los permisos apropiados, Snicker solo maneja los permisos para el envío de datos (y el almacenamiento) a través del formulario de comentarios!", + "s18n-82e5228061f185ee185bd9f3ecba4ee7": "Permitir a %s comentarios", + "s18n-61b58693e0eceeb27ce0cc3b25b3bf31": "Configuración de la vista de usuario", + "s18n-b3c1c2c231275878abe58a55966fa9e0": "Posición en la página", + "s18n-9cb1eef8966f93282524929f65c8b9ec": "Sin posición (deberá habilitarse en la plantilla)", + "s18n-e89fd56cefec9baabcbe0db3e5a36962": "Usar comienzo de página 'pageBegin'", + "s18n-3d08b5dcc1e5c3c7e7e2eaf2d0d6a12d": "Usar final de página 'pageEnd'", + "s18n-3771d05b6af4ebb0a303266c47809548": "Usar comienzo de cuerpo html 'siteBodyBegin'", + "s18n-42438edc41ab83312486009e3122e92b": "Usar final de cuerpo html 'siteBodyEnd'", + "s18n-948da5199de32c7601a20b6107c31d4d": "Captcha", + "s18n-47e5c42fb9bdca3636a5d866a6794101": "Deshabilitar Captcha", + "s18n-eedf6d23d18212016e22428658e17794": "Usar local Captcha (by Gregway)", + "s18n-09cddbc3627ea46a8dce692d64273b61": "Usar Googles reCaptcha (aun no disponible)", + "s18n-838a13e4fece7c272b960da3fb99f94d": "Plantilla de comentarios", + "s18n-6f95370a28520696b2a0ad34efc54d2d": "Orden de comentarios", + "s18n-4a8dc1710396b21e7b1da8112c07c4ad": "Primero recientes", + "s18n-2dab3b12d0b0642c3964b37d675ff24b": "Primero antiguos", + "s18n-230c71c29590608034b4a590a67ace31": "Posición del formulario", + "s18n-37d988444dec2001c488806fc8401e25": "Enseñar comentarios antes del formulario", + "s18n-16ac6c11951d825826f77a4097a1c2cb": "Enseñar comentarios tras el formulario", + "s18n-1e98ec9312b69676d5e3fe3caf8ecde1": "Comentarios por página", + "s18n-0d46f4389ca5f882e24899fe489bf344": "'0' si no deseas límite!", + "s18n-7b6b84fbd65a4b712a5ba0dccce176d5": "Casilla de Términos de Uso", + "s18n-9853383062a2e308d5aed35fe3da7953": "Desactivar este campo", + "s18n-352fd1d7225b5ea02b8ddd9fad0d6e34": "Mostrar mensaje (ver Frases por Defecto)", + "s18n-71860c77c6745379b0d44304d66b6a13": "Página", + "s18n-5dcb84333ae70a5bed60bf70d34dcd2b": "Mostrar texto por defecto de RGPD (Reglamento General de Protección de datos) o tu propia página!", + "s18n-2e3d9327c371afb7489f9b9278198622": "AJAX Script", + "s18n-c60cab6330745b41cbee05603eec6691": "AJAX Script disponible", + "s18n-69b11f64af515ef979fbf28c5e06f370": "No usar AJAX", + "s18n-9bbec7b57565f06d73522669d3b836dc": "Con AJAX acciones como comentar, me gusta, o no me gusta se realizan sin tener que actualizar la página!", + "s18n-7c74c0d2d1c28d1568298c89742ce126": "Avatares", + "s18n-aeab7c630dae161d8f6e2898dd83b471": "Usar Gravatar", + "s18n-882e3436da897c055cc3f8bd2598b71a": "Usar Identicon", + "s18n-d92d61ad0c0065170a37a1805ad1bc9e": "Usar Persona Misteriosa", + "s18n-b4a51a35344f9a8fa1139cfd968ab308": "Usar imagen de perfil de usuario si está registrado en la web", + "s18n-b50a4a96c25d745d73114af5a4b03145": "Configuaración Gravatar", + "s18n-9ffb941a398ddee8e054eef3292c546e": "Mostrar Persona Misteriosa", + "s18n-a7dd12b1dab17d25467b0b0a4c8d4a92": "Mostrar", + "s18n-0d2fc085ee57276417cf380027060760": "Imagen Gravatar por defecto si el usuario no dispone de Gravatar!", + "s18n-58e2aacf5792087168cbc62578584ecd": "Configuración de subscripción", + "s18n-8290ca86b8980a14bd46f34017e03f93": "El sistema de suscripción aun no está disponible!", + "s18n-0bd7ff1b4ac56a9616796bdc05609de2": "Subscripción por Email", + "s18n-208f156d4a803025c284bb595a7576b4": "Activo", + "s18n-dc985a7c2144c6447674e674aee08441": "Email 'From'", + "s18n-f6db5b4db3f9c1ba0ffc091abc561802": "Email 'ReplyTo'", + "s18n-8d868315d258783a95336d1a6ce27e1e": "Cuerpo de Email (Opt-In)", + "s18n-7721b2a2f2a453cc790e3ac7065e9b65": "Usar el email por defecto para la suscripción", + "s18n-58ff11585a82c73f5117c91c29cb3f63": "Cuerpo de Email (Notificación)", + "s18n-bea9ff19efca028c01617da5dce18171": "Usar el email por defecto para las notificaciones", + "s18n-e0024a8886a178d4f70b8c888b701680": "Conoce más sobre las notificaciones de Email %s!", + "s18n-8bcf6629759bd278a5c6266bd9c054f8": "Frases por Defecto", + "s18n-3979b4205954030810a8a87769348094": "Mensajes por defecto de agradecimiento", + "s18n-620b528248b36bf743d1ad33e35022d6": "Por suscripción", + "s18n-e83f6b01d1c81313f6b388281e13aacf": "Por votar", + "s18n-01b7a6bc6b57783b4fd081085ff3271e": "Opss!: Algo salió mal. Prueba de nuevo.", + "s18n-19678b1419eff34dffd41d6778b1aa89": "Opss!: Nombre de usuario incorrecto", + "s18n-1255e6794d33b443bcee21279d9caa1a": "Opss!: Dirección de correo incorrecta", + "s18n-ebf545757b92b3a553d83ea3db48beca": "Opss!: Texto de comentario vacío", + "s18n-f1bc53e2456c425578277adbc7c90f3a": "Opss!: Título de comentario vacío", + "s18n-53c8fdd7ed497dbacbef5fd5d4f38f3d": "Opss!: No has aceptado los términos", + "s18n-89f8cc478fdfc0a8f36c1b393f39677a": "Opss!: Se ha marcado como Spam", + "s18n-59ad5c9fcee1b28a5d004bcf684a5acd": "Opss!: Ya ha sido votado", + "s18n-56ef2c600b4af0f9f7b35640525967ca": "Términos de uso", + "s18n-08400c2e0f51197fdb3590461b15b2cc": "Nombre de usuario o Email", + "s18n-b79edd2e426f90401c04869346b503c7": "Busqueda de usuarios", + "s18n-ee51fa9d5097c84d2fa6c885bf2d5d84": "No hay usuarios disponibles", + "s18n-14c4b06b824ec593239362517f538b29": "Nombre de usuarios", + "s18n-0c83f57c786a0b4a39efab23731c7ebc": "Email", + "s18n-e1260894f59eeae98c8440899de4df8d": "Encargarse de", + "s18n-4bc61296b766756f1c7296489633bf32": "Borrar (Anonimizar)", + "s18n-c0f4afd3614929f1c803f3a01414a6c7": "Borrar (Completamente)", + "s18n-6356f32d0c02c8f90cb59a77e16e8fe2": "Usuario desbloqueado", + "s18n-2327a01afbee025fb5913357c9d6b1b3": "Usuario bloqueado", + "s18n-76a0f6752a45d8af6343ef3e2b6f522a": "Único comentario", + "s18n-c426859e50a35617d863cdad2b9c84aa": "Página de comentarios", + "s18n-a64d776275f13a51790bb460774b9129": "Comentarios de usuarios", + "s18n-9bc65c2abec141778ffaa729489f3e87": "Usuarios", + "s18n-ccd1066343c95877b75b79d47c36bebe": "Configuración", + "s18n-5b49260517622682a058b69f996d06eb": "Gracias por comentar!", + "s18n-74196a783a6f1707a43cc8117f0d9c83": "Gracias por tu comentario. Revisa tu correo electrónico para confirmar la subscripción!", + "s18n-a939eb542e34cd502b3f7352b2e0f715": "Gracias por valorar este comentario!", + "s18n-d2a9677817ee08ed05bf9fd868669756": "Opss!: Algo salió mal, actualiza e intentalo de nuevo!", + "s18n-05b85714aa8f1b364f930e2539059b5e": "Opss! Nombre de usuario inválido o demasiado largo (hasta 42 caracteres)!", + "s18n-321e8b481f0ccd62df535256e8e6d2c6": "Opss!: Dirección de correo inválida!", + "s18n-fcd0c3a087c5123ffdecc20fe9015870": "Opss!: No hay texto en tu comentario!", + "s18n-2afa2d90ce3343fbe188b9f49ad5797d": "Opss!: Falta el título!", + "s18n-bee1741efe0c735d2c7180771586faf0": "Opss!: No aceptaste los términos!", + "s18n-92fe96d6ccee901f94fad0000369a9b7": "Opss!: No eres bien recibido, por espamer!", + "s18n-9b264fc6137096f8a40acde68f6ae562": "Opss!: Ya votaste este comentario!", + "s18n-ca62db4704290ef1a7e65df3ffc7983b": "Conforme con almacenar mis datos (incluida dirección IP)!", + "s18n-01c611362e8b046f32650b85ce161559": "Opss!: Algo salió mal", + "s18n-c4f94c6995b0376f28276e432bed75fa": "Token perdido!", + "s18n-a573e7b86e41522c7a291846aa109104": "Token inválido!", + "s18n-3e8909518ce4728685aa09cdde3caa22": "No tienes suficientes permisos!", + "s18n-b60a6a8a529a9f0497134205bab15e77": "No tienes suficientes permisos!", + "s18n-56abae3e615ae5b5609c32852b777d46": "Acción desconocida o inválida!", + "s18n-be5136b4f2b33c80e3afd377ee993acb": "Opss!: Algo salió mal", + "s18n-46d4c97e91319867654f7cc80c439ba4": "Usuario sin ID (identificador)!", + "s18n-af1ba1dd4eab5562f78c65bc89a0a7e9": "Acción exitosa!", + "s18n-a5193444ee82c18bac726b35a1704d03": "Actualizado con éxito!", + "s18n-717c8267d40664ccf7ef25a26ff9cde6": "Copia de seguridad realizada con éxito!", + "s18n-a1e2b7401861cee01c172878f104bd8c": "Comentarios no disponibles", + "s18n-15802277ea1cdfcbacd6308fa0c7c30f": "Desactivación Snicker Plugin", + "s18n-877d58f21c87442efa4081112a6cb07a": "Desactivar Snicker<\/b> Plugin, podría borrar todos los comentarios!", + "s18n-a4983c86683f8d8598c0513339550dc0": "Deseas realizar una copia de seguridad antes?", + "s18n-4cce9e52118cc659be5070b2f08cdd91": "La copia de seguridad será guardad en %s!", + "s18n-5f87fd2e0fa992d37c814bb4ca299646": "Si, crear copia de seguridad", + "s18n-3d414feb412a1f4cd9324f6411c76329": "No, solo desactiva", + "s18n-10aec35353f9c4096a71c38654c3d402": "Cancelar", + "s18n-bc6d6a26d44b6f39a0e7b6c7787f3295": "Se han desactivado los comentarios para esta página!", + "s18n-a8e30d73eddea9866cf99ecd6e8467b5": "No hay comentarios. Se el primero!", + "s18n-b3afbadaa2f1c79f8b3999be7fd9719f": "Captcha incorrecto!", + "s18n-fb74aafe8bf1fd4d8f7a6e1ff73028f7": "UID de comentario inexistente o incorrecta!", + "s18n-25dea7cb70f98250b388f6ab0ddf20cb": "Estado de comentario inexistente o incorrecto!", + "s18n-2cd68855bdc54ff5e3c191a6333ff75d": "No se ha podido actualizar el nuevo estado de comentario!", + "s18n-c0937505b7afa81a053077bc7ae369a5": "Estado de comentario actualizado con éxito!", + "s18n-6b2c8084c67f24bb73d95031bb570ef7": "No se ha podido borrar el comentario!", + "s18n-376388311a80dbde63fde7f6c72081e0": "Comentario borrado con éxito!", + "s18n-83bbb9e8745cc95e730fe7b8de9345f1": "Identificado como %s (%s)", + "s18n-91fb98e1ac4cf76b7a5b8bae09051e2a": "Nombre de usuario", + "s18n-31f6da7a30e7acf1f82451bfd1a7f8fa": "Dirección de correo", + "s18n-f794080a5a29e35233c82df85f1207eb": "Comentario...", + "s18n-a363b8d13575101a0226e8d0d054f2e7": "Respuestas", + "s18n-f10db888c5e63b343000cffc038e0a46": "Escribe", + "s18n-3cc5bcf15d6b8faed118e2ce72d19a1e": "De acuerdo con %s!", + "s18n-2af0aab477f402e0f4ad7a27e6c9f952": "Comentarios anteriores", + "s18n-8538431db22040e2147b363f86a2e2f0": "Comentarios siguientes", + "s18n-ae0dbd5cc42a6db191db5e0083bcb307": "Este comentario aun no ha sido moderado!", + "s18n-48df9c3f3cca3fb2b8bcf811633bee06": "Escrito por %s", + "s18n-81fdc9813cebe0553c55e78dc2b6029f": "sobre %s", + "s18n-be1ab1632e4285edc3733b142935c60b": "Me gusta", + "s18n-bc8b79025e4595298669fd21da814941": "No me gusta", + "s18n-e84afaab83ecb301b3d97ce4174d2773": "Responder" +} diff --git a/languages/fa_IR.json b/languages/fa_IR.json new file mode 100644 index 0000000..924de7e --- /dev/null +++ b/languages/fa_IR.json @@ -0,0 +1,201 @@ +{ + "plugin-data": { + "name": "ارسال دیدگاه Snicker", + "description": "یک سیستم بومی ارسال دیدگاه فلت-فایل آجاکسی برای بلودیت، شامل اشتراک به دیدگاه‌ها و کاملاً سازگار با GDPR!" + }, + "s18n-a5d491060952aa8ad5fdee071be752de": "ارسال دیدگاه", + "s18n-de95b43bceeb4b998aed4aed5cef1ae7": "ویرایش", + "s18n-f984023ed3d6df2326c9d59838c29792": "بروزرسانی دیدگاه", + "s18n-86448a506dd93303a72140b9124ee321": "حذف دیدگاه", + "s18n-db10a8eb963bc0e5f4483ac9b5dc554c": "عنوان دیدگاه", + "s18n-335630425567dbe91768a3beffdec752": "متن دیدگاه", + "s18n-bd7e63f881c7f787a9a0dce20b7f9e5b": "ابر تنظیمات", + "s18n-1acfe725df7bd12195751f0737c4d375": "کاربر ثبت شده", + "s18n-e988189db402fab453f72052629c02cd": "نام کاربری دیدگاه", + "s18n-db84c6236ca6a01b9189504d78c012a5": "ایمیل دیدگاه", + "s18n-7c6c2e5d48ab37a007cbf70d3ea25fa4": "در انتظار", + "s18n-787d5f05953ec39b108869dfdd7733e6": "تائید شده", + "s18n-c7537d6d48ecf261749c09a9f284bd45": "رد شده", + "s18n-e09f6a7593f8ae3994ea57e1117f67ec": "هرزنامه", + "s18n-7a5115c2c1eb662308decbec83593494": "مشاهده صفحه", + "s18n-0572a05cd6d6360391993a611099542f": "عنوان و یا گزیده دیدگاه", + "s18n-428f14500191b5d53675da4a96bc8bba": "جستجوی دیدگاه‌ها", + "s18n-e0be71bccdeceb713fba3c222f79a3c5": "دیدگاهی در دسترس نیست", + "s18n-06d4cd63bde972fc66a0aed41d2f5c51": "دیدگاه", + "s18n-02bd92faa38aaa6cc0ea75e59937a1ef": "نویسنده", + "s18n-ebb67a4271abe715344471b0f16321f6": "عملیات", + "s18n-58566b9a9b2733b0ceacb2186672b5d1": "نمایش تمام پاسخ ها", + "s18n-5f44c0081bd862a77ba8b24e923cadf1": "پاسخ به", + "s18n-eb399bcaca686f8609137153307eecf1": "تغییر", + "s18n-2736f4347985da50dc023444c193bfea": "ویرایش دیدگاه", + "s18n-a107bf4b12e36a07161a26d95b03bc81": "تائید دیدگاه", + "s18n-da937abd19cd9e1430470b8a471a41d4": "رد دیدگاه", + "s18n-32cb4199893d9948cc0853eef244f1fc": "هرزنامه است", + "s18n-a6494adfb72d12d3a4da66855c284ec6": "بازگشت به در انتظار", + "s18n-1bda80f2be4d3658e0baa43fbe7ae8c1": "مشاهده", + "s18n-c9ae5a4214e87ad6fbed44a267471eee": "ذخیره تنظیمات", + "s18n-e124d357c3c832434a8676a5e18db842": "تنظیمات عمومی", + "s18n-b62a9dcd666f3ff44197cf21ac66507d": "مدیریت دیدگاه‌ها", + "s18n-7f0217cdcdd58ba86aae84d9d3d79f81": "مدیریت", + "s18n-1a1dc91c907325c69271ddf0c944bc72": "تائید", + "s18n-df6963dc912cde9baeef10343167ba01": "هر دیدگاه", + "s18n-39ba4181e212acf183ac965c9b37da89": "درصورت ورود به سیستم", + "s18n-0dac4426a017f0a0370db32776bc30bb": "در صورت مدیر یا نویسنده بودن", + "s18n-c70aa8b8fd6f2652eda2d5366faf1de5": "در صورت داشتن دیدگاه از قبل", + "s18n-8b26cab9d444760b4bcc65dc4d8634f8": "پذیرفتن دیدگاه", + "s18n-67614909bf9de326de71946036de39f1": "... در نوشته های عمومی", + "s18n-a7c384c1b60785c40bc3a4e4dfc5a108": "... در نوشته های چسبنده", + "s18n-9907b78f8745810599dbf6fd29a01364": "... در نوشته های استاتیک", + "s18n-c89cbecce48d04cd76c0b95c8128ad97": "فعال (اختیاری)", + "s18n-e9d51286fdd0ff058650392fc8b6ae30": "فعال (الزامی)", + "s18n-0aaa87422396fdd678498793b6d5250e": "غیرفعال", + "s18n-8da2c8185edfeb1765526f8e2e4f388d": "محدودیت دیدگاه", + "s18n-77d6d185c93549dab24f29ff2e3b25a8": "برای غیرفعال کردن هر محدودیتی از '0' استفاده کنید", + "s18n-1ef52691308c8add87723a4103a561c2": "عمق دیدگاه", + "s18n-d102731a5fedff24f30e24e883ef4636": "Markup دیدگاه", + "s18n-6640979a191e66655c26c59d404bf955": "پذیرفتن HTML ابتدایی", + "s18n-915009e874f8bed1845060012f826fcd": "پذیرفتن Markdown", + "s18n-a9a36cb3d8f4f7297ebca99a322d6342": "امتیاز به دیدگاه", + "s18n-2123546d1ff8b0cb035df0c0b0d06825": "ذخیره امتیازهای مهمانان در", + "s18n-dead693ab29895d302fca0e6baad6182": "ذخیره سازی کوکی", + "s18n-02b68043bdcae159e83199d64a5abd7d": "ذخیره سازی نشست", + "s18n-40bd8791e523e91219886c35622163fc": "ذخیره سازی پایگاه داده", + "s18n-8717cfca734e8987971f63b20eeb8024": "توضیحات؟", + "s18n-5a8cbcf57f5b59f0d4b8ded97d018399": "ذخیره سازی کوکی بر روی کامپیوتر کاربر قرار دارد. بنابراین شما کنترل کامل نداشته و نیاز به مجوز مناسب از طرف کاربر را دارید.", + "s18n-a09dbdf907873e66d9d644cca71970d5": "ذخیره سازی نشست فقط به صورت موقت در سرور ذخیره می‌شود، زمانی که کاربر مرورگرش را ببندد این اطلاعات هم پاک می‌شود. بنابراین شما به هیچ مجوزی از طرف کاربر نیازی ندارید.", + "s18n-71892ebe01ba92d1f163dc37d818b5ff": "ذخیره سازی پایگاه داده تولید و در یک مقدار ناشناس ذخیره می‌شود، ولی قابل تعیین توسط کاربر است، همچنین نیاز به مجوزهای مناسب از طرف کاربر می‌باشد.", + "s18n-7c35a0bcb0b0678f0829036eead5ddca": " لطفاً توجه کنید: شما مسئولیت اخذ مجوزهای مناسب هستید، Snicker فقط مجوزهای ارسال (و ذخیره سازی) اطلاعات از طریق فرم دیدگاه را بکار می‌گیرد!", + "s18n-82e5228061f185ee185bd9f3ecba4ee7": "پذیرفتن به %s دیدگاه", + "s18n-61b58693e0eceeb27ce0cc3b25b3bf31": "تنظیمات محیط کاربری", + "s18n-b3c1c2c231275878abe58a55966fa9e0": "فیلتر صفحه", + "s18n-9cb1eef8966f93282524929f65c8b9ec": "غیرفعال کردن فیلتر صفحه", + "s18n-e89fd56cefec9baabcbe0db3e5a36962": "استفاده از 'pageBegin'", + "s18n-3d08b5dcc1e5c3c7e7e2eaf2d0d6a12d": "استفاده از 'pageEnd'", + "s18n-3771d05b6af4ebb0a303266c47809548": "استفاده از 'siteBodyBegin'", + "s18n-42438edc41ab83312486009e3122e92b": "استفاده از 'siteBodyEnd'", + "s18n-948da5199de32c7601a20b6107c31d4d": "کدامنیتی دیدگاه", + "s18n-47e5c42fb9bdca3636a5d866a6794101": "غیرفعال کردن دیدگاه", + "s18n-eedf6d23d18212016e22428658e17794": "استفاده از کدامنیتی محلی (مولف Gregway)", + "s18n-09cddbc3627ea46a8dce692d64273b61": "استفاده از reCaptcha گوگل (هنوز در دسترس نیست)", + "s18n-838a13e4fece7c272b960da3fb99f94d": "قالب دیدگاه", + "s18n-6f95370a28520696b2a0ad34efc54d2d": "ترتیب دیدگاه", + "s18n-4a8dc1710396b21e7b1da8112c07c4ad": "جدیدترین دیدگاه در ابتدا", + "s18n-2dab3b12d0b0642c3964b37d675ff24b": "جدیدترین دیدگاه در انتها", + "s18n-230c71c29590608034b4a590a67ace31": "موقعیت فرم دیدگاه", + "s18n-37d988444dec2001c488806fc8401e25": "نمایش فرم دیدگاه بالای دیدگاه‌ها", + "s18n-16ac6c11951d825826f77a4097a1c2cb": "نمایش فرم دیدگاه پایین دیدگاه‌ها", + "s18n-1e98ec9312b69676d5e3fe3caf8ecde1": "دیدگاه در هر صفحه", + "s18n-0d46f4389ca5f882e24899fe489bf344": "برای نمایش تمام دیدگاه‌ها از '0' استفاده کنید", + "s18n-7b6b84fbd65a4b712a5ba0dccce176d5": "جعبه شرایط استفاده", + "s18n-9853383062a2e308d5aed35fe3da7953": "غیرفعال کردن این کادر", + "s18n-352fd1d7225b5ea02b8ddd9fad0d6e34": "نمایش پیام (متون جایگزین را ببینید)", + "s18n-71860c77c6745379b0d44304d66b6a13": "صفحه", + "s18n-5dcb84333ae70a5bed60bf70d34dcd2b": "متن پیش‌فرض GDPR نمایش داده شود و یا متن صفحه استاتیک 'قوانین استفاده' خود را انتخاب کنید!", + "s18n-2e3d9327c371afb7489f9b9278198622": "اسکریپت AJAX", + "s18n-c60cab6330745b41cbee05603eec6691": "استفاده از AJAX", + "s18n-69b11f64af515ef979fbf28c5e06f370": "از AJAX استفاده نشود", + "s18n-9bbec7b57565f06d73522669d3b836dc": "اسکریپت AJAX درخواست های (دیدگاه، like،dislike) را مستقیماً بکار می‌برد بدون اینکه صفحه مجدداً بارگذاری شود!", + "s18n-7c74c0d2d1c28d1568298c89742ce126": "آواتار دیدگاه", + "s18n-aeab7c630dae161d8f6e2898dd83b471": "استفاده از Gravatar", + "s18n-882e3436da897c055cc3f8bd2598b71a": "استفاده از Identicon", + "s18n-d92d61ad0c0065170a37a1805ad1bc9e": "استفاده از Mystery Men", + "s18n-b4a51a35344f9a8fa1139cfd968ab308": "ترجیح استفاده از تصویر پروفایل کاربران وارد شده به سیستم", + "s18n-b50a4a96c25d745d73114af5a4b03145": "دیدگاه Gravatar", + "s18n-9ffb941a398ddee8e054eef3292c546e": "نمایش Mystery Person", + "s18n-a7dd12b1dab17d25467b0b0a4c8d4a92": "نمایش", + "s18n-0d2fc085ee57276417cf380027060760": "تصویر پیش فرض Gravatar، در صورتی که کاربر Gravatar ندارد!", + "s18n-58e2aacf5792087168cbc62578584ecd": "تنظیمات اشتراک", + "s18n-8290ca86b8980a14bd46f34017e03f93": "سیستم اشتراک هنوز در دسترس نیست!", + "s18n-0bd7ff1b4ac56a9616796bdc05609de2": "اشتراک ایمیلی", + "s18n-208f156d4a803025c284bb595a7576b4": "فعال", + "s18n-dc985a7c2144c6447674e674aee08441": "آدرس ایمیل 'From'", + "s18n-f6db5b4db3f9c1ba0ffc091abc561802": "آدرس ایمیل 'ReplyTo'", + "s18n-8d868315d258783a95336d1a6ce27e1e": "متن ایمیل (Opt-In)", + "s18n-7721b2a2f2a453cc790e3ac7065e9b65": "استفاده از ایمیل اشتراک پیش فرض", + "s18n-58ff11585a82c73f5117c91c29cb3f63": "متن ایمیل (اطلاع رسانی)", + "s18n-bea9ff19efca028c01617da5dce18171": "استفاده از ایمیل اطلاع رسانی پیش فرض", + "s18n-e0024a8886a178d4f70b8c888b701680": "درمورد ایمیل اطلاع رسانی های سفارشی %s مطالعه کنید!", + "s18n-8bcf6629759bd278a5c6266bd9c054f8": "متون جایگزین", + "s18n-3979b4205954030810a8a87769348094": "پیام تشکر پیش فرض", + "s18n-620b528248b36bf743d1ad33e35022d6": "پیام تشکر به همراه اشتراک", + "s18n-e83f6b01d1c81313f6b388281e13aacf": "پیام تشکر برای امتیازدهی", + "s18n-01b7a6bc6b57783b4fd081085ff3271e": "خطا: خطای ناشناخته، دوباره تلاش کنید", + "s18n-19678b1419eff34dffd41d6778b1aa89": "خطا: نام کاربری معتبر نیست", + "s18n-1255e6794d33b443bcee21279d9caa1a": "خطا: آدرس ایمیل معتبر نیست", + "s18n-ebf545757b92b3a553d83ea3db48beca": "خطا: متن دیدگاه خالی است", + "s18n-f1bc53e2456c425578277adbc7c90f3a": "خطا: عنوان دیدگاه خالی است", + "s18n-53c8fdd7ed497dbacbef5fd5d4f38f3d": "خطا: قوانین پذیرفته نشده", + "s18n-89f8cc478fdfc0a8f36c1b393f39677a": "خطا: به عنوان هرزنامه شناخته شد", + "s18n-59ad5c9fcee1b28a5d004bcf684a5acd": "خطا: قبلا امتیاز دادید", + "s18n-56ef2c600b4af0f9f7b35640525967ca": "قوانین استفاده", + "s18n-08400c2e0f51197fdb3590461b15b2cc": "نام کاربری یا آدرس ایمیل", + "s18n-b79edd2e426f90401c04869346b503c7": "جستجوی کاربران", + "s18n-ee51fa9d5097c84d2fa6c885bf2d5d84": "هیچ کاربری وجود ندارد", + "s18n-14c4b06b824ec593239362517f538b29": "نام کاربرری", + "s18n-0c83f57c786a0b4a39efab23731c7ebc": "ایمیل", + "s18n-e1260894f59eeae98c8440899de4df8d": "بکارگیری", + "s18n-4bc61296b766756f1c7296489633bf32": "حذف (ناشناس)", + "s18n-c0f4afd3614929f1c803f3a01414a6c7": "حذف (به طور کامل)", + "s18n-6356f32d0c02c8f90cb59a77e16e8fe2": "آزاد کردن کاربر", + "s18n-2327a01afbee025fb5913357c9d6b1b3": "مسدود کردن کاربر", + "s18n-76a0f6752a45d8af6343ef3e2b6f522a": "یک دیدگاه", + "s18n-c426859e50a35617d863cdad2b9c84aa": "دیدگاه های نوشته", + "s18n-a64d776275f13a51790bb460774b9129": "دیدگاه های کاربر", + "s18n-9bc65c2abec141778ffaa729489f3e87": "کاربران", + "s18n-ccd1066343c95877b75b79d47c36bebe": "پیکربندی", + "s18n-5b49260517622682a058b69f996d06eb": "متشکرم از ارسال دیدگاه!", + "s18n-74196a783a6f1707a43cc8117f0d9c83": "متشکرم از ارسال دیدگاه! لطفاً اشتراک خود را از طریق لینکی که به آدرس ایمیل شما ارسال شده را تائید کنید", + "s18n-a939eb542e34cd502b3f7352b2e0f715": "متشکرم از اینکه به این دیدگاه امتیاز دادید!", + "s18n-d2a9677817ee08ed05bf9fd868669756": "خطای ناشناخته ای رخ داد، لطفا صفحه را مجددا بارگیری کرده و دوباره تلاش کنید!", + "s18n-05b85714aa8f1b364f930e2539059b5e": "خطایی رخ داد: نام کاربری ارائه شده نامعتبر و یا بسیار طولانی است(فقط 42 کاراکتر باشد)!", + "s18n-321e8b481f0ccd62df535256e8e6d2c6": "خطایی رخ داد: آدرس ایمیل ارائه شده معتبر نیست", + "s18n-fcd0c3a087c5123ffdecc20fe9015870": "خطایی رخ داد: متن دیدگاه خالی است!", + "s18n-2afa2d90ce3343fbe188b9f49ad5797d": "خطایی رخ داد: عنوان دیدگاه خالی است!", + "s18n-bee1741efe0c735d2c7180771586faf0": "خطایی رخ داد: برای ارسال دیدگاه باید قوانین را بپذیرید!", + "s18n-92fe96d6ccee901f94fad0000369a9b7": "خطایی رخ داد: آدرس IP و یا آدرس ایمیل شما به عنوان هرزنامه شناخته شده!", + "s18n-9b264fc6137096f8a40acde68f6ae562": "خطایی رخ داد: شما قبلا به این دیدگاه رای داده اید!", + "s18n-ca62db4704290ef1a7e65df3ffc7983b": "من موافقم که اطلاعات من (شامل آدرس IP ناشناس) من ذخیره شود!", + "s18n-01c611362e8b046f32650b85ce161559": "خطای ناشناخته رخ داد!", + "s18n-c4f94c6995b0376f28276e432bed75fa": "توکن CSRF وجود ندارد!", + "s18n-a573e7b86e41522c7a291846aa109104": "توکن CSRF معتبر نیست!", + "s18n-3e8909518ce4728685aa09cdde3caa22": "برای فراخوانی این اقدام مجوز لازم را ندارید!", + "s18n-b60a6a8a529a9f0497134205bab15e77": "برای اجرای این اقدام مجوز لازم را ندارید!", + "s18n-56abae3e615ae5b5609c32852b777d46": "اقدام انجام شده ناشناخته و یا معتبر نیست!", + "s18n-be5136b4f2b33c80e3afd377ee993acb": "خطای ناشناخته رخ داد!", + "s18n-46d4c97e91319867654f7cc80c439ba4": "شناسه منحصر به فرد کاربری وجود ندارد!", + "s18n-af1ba1dd4eab5562f78c65bc89a0a7e9": "اقدام با موفقیت انجام شد!", + "s18n-a5193444ee82c18bac726b35a1704d03": "تنظیمات با موفقیت به روز شد!", + "s18n-717c8267d40664ccf7ef25a26ff9cde6": "نسخه پشتیبان با موفقیت تهیه شد!", + "s18n-a1e2b7401861cee01c172878f104bd8c": "نپذیرفتن دیدگاه", + "s18n-15802277ea1cdfcbacd6308fa0c7c30f": "غیر فعال کردن پلاگین ارسال دیدگاه Snicker", + "s18n-877d58f21c87442efa4081112a6cb07a": "شما دارید پلاگین Snicker را غیرفعال می‌کنید، که تمام دیدگاه‌های نوشته شده حذف خواهند شد!", + "s18n-a4983c86683f8d8598c0513339550dc0": "آیا تمایل دارید قبل از اینکار از دیدگاهها نسخه پشتیبان تهیه کنید؟", + "s18n-4cce9e52118cc659be5070b2f08cdd91": "نسخه پشتیبان در %s ذخیره می شود!", + "s18n-5f87fd2e0fa992d37c814bb4ca299646": "بله، تهیه پشتیبان", + "s18n-3d414feb412a1f4cd9324f6411c76329": "خیر، غیرفعال کردن", + "s18n-10aec35353f9c4096a71c38654c3d402": "لغو", + "s18n-bc6d6a26d44b6f39a0e7b6c7787f3295": "ارسال دیدگاه در این صفحه غیرفعال است!", + "s18n-a8e30d73eddea9866cf99ecd6e8467b5": "اولین نفری باشید که دیدگاهی ارسال می کند!", + "s18n-b3afbadaa2f1c79f8b3999be7fd9719f": "پاسخ به کدامنیتی انجام نشده و یا اشتباه وارد کردید!", + "s18n-fb74aafe8bf1fd4d8f7a6e1ff73028f7": "شناسه منحصر به فرد دیدگاه وجود نداشته و یا معتبر نیست!", + "s18n-25dea7cb70f98250b388f6ab0ddf20cb": "وضعیت دیدگاه ناشناخته و یا معتبر نیست!", + "s18n-2cd68855bdc54ff5e3c191a6333ff75d": "وضعیت دیدگاه جدید نمی تواند با موفقیت به روز شود!", + "s18n-c0937505b7afa81a053077bc7ae369a5": "وضعیت دیدگاه جدید با موفقیت ذخیره شد!", + "s18n-6b2c8084c67f24bb73d95031bb570ef7": "دیدگاه نمی تواند با موفقیت حذف شود!", + "s18n-376388311a80dbde63fde7f6c72081e0": "دیدگاه با موفقیت حذف شد!", + "s18n-83bbb9e8745cc95e730fe7b8de9345f1": "ورود به نام %s (%s)", + "s18n-91fb98e1ac4cf76b7a5b8bae09051e2a": "نام شما", + "s18n-31f6da7a30e7acf1f82451bfd1a7f8fa": "آدرس ایمیل شما", + "s18n-f794080a5a29e35233c82df85f1207eb": "متن دیدگاه...", + "s18n-a363b8d13575101a0226e8d0d054f2e7": "کدامنیتی", + "s18n-f10db888c5e63b343000cffc038e0a46": "نوشته:", + "s18n-3cc5bcf15d6b8faed118e2ce72d19a1e": "با %s موافقت می کنم!", + "s18n-2af0aab477f402e0f4ad7a27e6c9f952": "دیدگاه‌های قبلی", + "s18n-8538431db22040e2147b363f86a2e2f0": "دیدگاههای بعدی", + "s18n-ae0dbd5cc42a6db191db5e0083bcb307": "این دیدگاه هنوز تائید نشده!", + "s18n-48df9c3f3cca3fb2b8bcf811633bee06": "نوشته %s", + "s18n-81fdc9813cebe0553c55e78dc2b6029f": "در مورخه %s", + "s18n-be1ab1632e4285edc3733b142935c60b": "Like", + "s18n-bc8b79025e4595298669fd21da814941": "Dislike", + "s18n-e84afaab83ecb301b3d97ce4174d2773": "پاسخ" + } \ No newline at end of file diff --git a/languages/nl_NL.json b/languages/nl_NL.json new file mode 100644 index 0000000..bacbb7c --- /dev/null +++ b/languages/nl_NL.json @@ -0,0 +1,204 @@ +{ + "plugin-data": { + "name": "Snicker", + "description": "Een native, AJAX-enabled FlatFile reactiesysteem voor bludit volgens de GDPR-normen en met ondersteuning voor het abonneren op reacties." + }, + "s18n-a5d491060952aa8ad5fdee071be752de": "Reacties", + "s18n-de95b43bceeb4b998aed4aed5cef1ae7": "Bewerken", + "s18n-f984023ed3d6df2326c9d59838c29792": "Reactie bijwerken", + "s18n-86448a506dd93303a72140b9124ee321": "Reactie verwijderen", + "s18n-db10a8eb963bc0e5f4483ac9b5dc554c": "Titel", + "s18n-335630425567dbe91768a3beffdec752": "Bericht", + "s18n-bd7e63f881c7f787a9a0dce20b7f9e5b": "Metadata", + "s18n-1acfe725df7bd12195751f0737c4d375": "Geregistreerde gebruiker", + "s18n-e988189db402fab453f72052629c02cd": "Gebruikersnaam", + "s18n-db84c6236ca6a01b9189504d78c012a5": "E-mail", + "s18n-7c6c2e5d48ab37a007cbf70d3ea25fa4": "Wordt beoordeeld", + "s18n-787d5f05953ec39b108869dfdd7733e6": "Goedgekeurd", + "s18n-c7537d6d48ecf261749c09a9f284bd45": "Afgekeurd", + "s18n-e09f6a7593f8ae3994ea57e1117f67ec": "Spam", + "s18n-7a5115c2c1eb662308decbec83593494": "Pagina bekijken", + "s18n-0572a05cd6d6360391993a611099542f": "Titel of samenvatting reactie", + "s18n-428f14500191b5d53675da4a96bc8bba": "Zoeken", + "s18n-e0be71bccdeceb713fba3c222f79a3c5": "Geen reacties beschikbaar", + "s18n-06d4cd63bde972fc66a0aed41d2f5c51": "Reactie", + "s18n-02bd92faa38aaa6cc0ea75e59937a1ef": "Auteur", + "s18n-ebb67a4271abe715344471b0f16321f6": "Acties", + "s18n-58566b9a9b2733b0ceacb2186672b5d1": "Alle antwoorden tonen", + "s18n-5f44c0081bd862a77ba8b24e923cadf1": "Antwoorden op", + "s18n-eb399bcaca686f8609137153307eecf1": "Wijzigen", + "s18n-2736f4347985da50dc023444c193bfea": "Reactie bewerken", + "s18n-a107bf4b12e36a07161a26d95b03bc81": "Reactie goedkeuren", + "s18n-da937abd19cd9e1430470b8a471a41d4": "Reactie afkeuren", + "s18n-32cb4199893d9948cc0853eef244f1fc": "Markeren als Spam", + "s18n-a6494adfb72d12d3a4da66855c284ec6": "Terug naar reacties in afwachting", + "s18n-1bda80f2be4d3658e0baa43fbe7ae8c1": "Bekijken", + "s18n-c9ae5a4214e87ad6fbed44a267471eee": "Instellingen opslaan", + "s18n-e124d357c3c832434a8676a5e18db842": "Algemene instellingen", + "s18n-b62a9dcd666f3ff44197cf21ac66507d": "Reactiebeheer", + "s18n-7f0217cdcdd58ba86aae84d9d3d79f81": "Modereren", + "s18n-1a1dc91c907325c69271ddf0c944bc72": "Goedkeuren", + "s18n-df6963dc912cde9baeef10343167ba01": "iedere reactie", + "s18n-39ba4181e212acf183ac965c9b37da89": "Tenzij de gebruiker is ingelogd", + "s18n-0dac4426a017f0a0370db32776bc30bb": "Tenzij de gebruiker beheerder of auteur van de pagina is", + "s18n-c70aa8b8fd6f2652eda2d5366faf1de5": "Tenzij er reeds een eerdere reactie van de gebruiker is goedgekeurd", + "s18n-8b26cab9d444760b4bcc65dc4d8634f8": "Reacties toestaan", + "s18n-67614909bf9de326de71946036de39f1": "... op gepubliceerde pagina's", + "s18n-a7c384c1b60785c40bc3a4e4dfc5a108": "... op vastgezette pagina's", + "s18n-9907b78f8745810599dbf6fd29a01364": "... op statische pagina's", + "s18n-c89cbecce48d04cd76c0b95c8128ad97": "Inschakelen (optioneel)", + "s18n-e9d51286fdd0ff058650392fc8b6ae30": "Inschakelen (vereist)", + "s18n-0aaa87422396fdd678498793b6d5250e": "Uitschakelen", + "s18n-8da2c8185edfeb1765526f8e2e4f388d": "Limiet voor reacties", + "s18n-77d6d185c93549dab24f29ff2e3b25a8": "Voer '0' in om geen limiet toe te passen.", + "s18n-1ef52691308c8add87723a4103a561c2": "Diepte reacties", + "s18n-d102731a5fedff24f30e24e883ef4636": "Formaat voor reacties", + "s18n-6640979a191e66655c26c59d404bf955": "HTML beperkt toestaan", + "s18n-915009e874f8bed1845060012f826fcd": "Markdown toestaan", + "s18n-a9a36cb3d8f4f7297ebca99a322d6342": "Stemmen op reacties", + "s18n-2123546d1ff8b0cb035df0c0b0d06825": "Stemmen door gasten opslaan in", + "s18n-dead693ab29895d302fca0e6baad6182": "Cookies", + "s18n-02b68043bdcae159e83199d64a5abd7d": "Sessie-opslag", + "s18n-40bd8791e523e91219886c35622163fc": "Database", + "s18n-8717cfca734e8987971f63b20eeb8024": "Wat betekent dit?", + "s18n-5a8cbcf57f5b59f0d4b8ded97d018399": "Cookies<\/b> worden opgeslagen op de computer van de gebruiker; u heeft geen volledige controle EN de gebruiker moet cookies toestaan.", + "s18n-a09dbdf907873e66d9d644cca71970d5": "Sessie-opslag<\/b> wordt slechts tijdelijk op de server bewaard en wordt verwijderd wanneer de gebruiker de browser afsluit. Daarom is er gn toestemming van de gebruiker benodigd.", + "s18n-71892ebe01ba92d1f163dc37d818b5ff": "Opslaan in de Database<\/b> genereert een gepseudonimiseerde waarde voor iedere gebruiker die stemt en slaat die op in de database. Daarom is er toestemming van de gebruiker benodigd.", + "s18n-7c35a0bcb0b0678f0829036eead5ddca": "Waarschuwing:<\/b> U bent zelf verantwoordelijk voor het verkrijgen van goedkeuring door gebruikers; Snicker behandelt alleen de machtigingen voor de gegevens die verstuurd en/of opgeslagen worden via het reactieformulier!", + "s18n-82e5228061f185ee185bd9f3ecba4ee7": "%s toestaan voor reacties", + "s18n-61b58693e0eceeb27ce0cc3b25b3bf31": "Instellingen front-end", + "s18n-b3c1c2c231275878abe58a55966fa9e0": "Paginafilter", + "s18n-9cb1eef8966f93282524929f65c8b9ec": "Paginafilter uitschakelen", + "s18n-e89fd56cefec9baabcbe0db3e5a36962": "'pageBegin' gebruiken", + "s18n-3d08b5dcc1e5c3c7e7e2eaf2d0d6a12d": "'pageEnd' gebruiken", + "s18n-3771d05b6af4ebb0a303266c47809548": "'siteBodyBegin' gebruiken", + "s18n-42438edc41ab83312486009e3122e92b": "'siteBodyEnd' gebruiken", + "s18n-948da5199de32c7601a20b6107c31d4d": "Captcha", + "s18n-47e5c42fb9bdca3636a5d866a6794101": "Captcha uitschakelen", + "s18n-ab6ef7ef94efc86db78218c6c265243a": "OWASP's PureCaptcha gebruiken", + "s18n-a58adce0085cc1a25fc8076e97c29d70": "Gregway's Captcha gebruiken", + "s18n-9d2f2ec577e7383b88fd481d6c566c5e": "Gregway's Captcha gebruiken (GD library niet beschikbaar!)", + "s18n-09cddbc3627ea46a8dce692d64273b61": "Googles reCaptcha gebruiken (nog niet beschikbaar)", + "s18n-838a13e4fece7c272b960da3fb99f94d": "Sjabloon", + "s18n-6f95370a28520696b2a0ad34efc54d2d": "Volgorde", + "s18n-4a8dc1710396b21e7b1da8112c07c4ad": "Nieuwste reacties eerst", + "s18n-2dab3b12d0b0642c3964b37d675ff24b": "Oudste reacties eerst", + "s18n-230c71c29590608034b4a590a67ace31": "Positie reactieformulier", + "s18n-37d988444dec2001c488806fc8401e25": "Boven reacties tonen", + "s18n-16ac6c11951d825826f77a4097a1c2cb": "Onder reacties tonen", + "s18n-1e98ec9312b69676d5e3fe3caf8ecde1": "Reacties per pagina", + "s18n-0d46f4389ca5f882e24899fe489bf344": "Voer '0' in om alle reacties te tonen.", + "s18n-7b6b84fbd65a4b712a5ba0dccce176d5": "Accepteren gebruiksvoorwaarden", + "s18n-9853383062a2e308d5aed35fe3da7953": "Dit veld uitschakelen", + "s18n-352fd1d7225b5ea02b8ddd9fad0d6e34": "Bericht tonen (zie Meldingen)", + "s18n-71860c77c6745379b0d44304d66b6a13": "Pagina", + "s18n-5dcb84333ae70a5bed60bf70d34dcd2b": "Toon de standaardtekst AVG (GDPR) of kies een eigen statische pagina voor de gebruiksvoorwaarden.", + "s18n-2e3d9327c371afb7489f9b9278198622": "AJAX-script", + "s18n-c60cab6330745b41cbee05603eec6691": "AJAX-script insluiten", + "s18n-69b11f64af515ef979fbf28c5e06f370": "AJAX niet gebruiken", + "s18n-9bbec7b57565f06d73522669d3b836dc": "The AJAX Script hands over the request (reactie, like, dislike) directly without reloading the page!", + "s18n-7c74c0d2d1c28d1568298c89742ce126": "Avatar", + "s18n-aeab7c630dae161d8f6e2898dd83b471": "Gravatar gebruiken", + "s18n-882e3436da897c055cc3f8bd2598b71a": "Identicon gebruiken", + "s18n-d92d61ad0c0065170a37a1805ad1bc9e": "Mystery Men gebruiken", + "s18n-b4a51a35344f9a8fa1139cfd968ab308": "Profielfoto's gebruiken voor ingelogde gebruikers", + "s18n-b50a4a96c25d745d73114af5a4b03145": "Gravatar", + "s18n-9ffb941a398ddee8e054eef3292c546e": "Mystery Person tonen", + "s18n-a7dd12b1dab17d25467b0b0a4c8d4a92": "Tonen", + "s18n-0d2fc085ee57276417cf380027060760": "Standaardafbeelding van Gravatar als de gebruiker geen eigen Gravatar heeft.", + "s18n-58e2aacf5792087168cbc62578584ecd": "Abonneren", + "s18n-8290ca86b8980a14bd46f34017e03f93": "De functie Abonneren is nog niet beschikbaar.", + "s18n-0bd7ff1b4ac56a9616796bdc05609de2": "Abonneren met e-mail", + "s18n-208f156d4a803025c284bb595a7576b4": "Inschakelen", + "s18n-dc985a7c2144c6447674e674aee08441": "E-mailadres afzender ('From')", + "s18n-f6db5b4db3f9c1ba0ffc091abc561802": "E-mailadres beantwoorden ('ReplyTo')", + "s18n-8d868315d258783a95336d1a6ce27e1e": "Inhoud e-mail bij abonneren", + "s18n-7721b2a2f2a453cc790e3ac7065e9b65": "Standaardinhoud voor e-mail bij abonneren gebruiken", + "s18n-58ff11585a82c73f5117c91c29cb3f63": "Inhoud e-mail bij notificatie", + "s18n-bea9ff19efca028c01617da5dce18171": "Standaardinhoud voor e-mail bij notificatie gebruiken", + "s18n-e0024a8886a178d4f70b8c888b701680": "Lees meer over eigen inhoud voor e-mail bij notificaties: %s.", + "s18n-8bcf6629759bd278a5c6266bd9c054f8": "Meldingen", + "s18n-3979b4205954030810a8a87769348094": "Standaardbericht voor bevestiging", + "s18n-620b528248b36bf743d1ad33e35022d6": "Bevestiging na abonneren", + "s18n-e83f6b01d1c81313f6b388281e13aacf": "Bevestiging na stemmen", + "s18n-01b7a6bc6b57783b4fd081085ff3271e": "Fout: onbekende fout, probeer het nogmaals", + "s18n-19678b1419eff34dffd41d6778b1aa89": "Fout: ongeldige gebruikersnaam", + "s18n-1255e6794d33b443bcee21279d9caa1a": "Fout: ongeldig e-mailadres", + "s18n-ebf545757b92b3a553d83ea3db48beca": "Fout: geen bericht ingevoerd", + "s18n-f1bc53e2456c425578277adbc7c90f3a": "Fout: geen titel ingevoerd", + "s18n-53c8fdd7ed497dbacbef5fd5d4f38f3d": "Fout: gebruiksvoorwaarden niet geaccepteerd", + "s18n-89f8cc478fdfc0a8f36c1b393f39677a": "Fout: gemarkeerd als SPAM", + "s18n-59ad5c9fcee1b28a5d004bcf684a5acd": "Fout: u heeft reeds gestemd", + "s18n-56ef2c600b4af0f9f7b35640525967ca": "Gebruiksvoorwaarden", + "s18n-08400c2e0f51197fdb3590461b15b2cc": "Gebruikersnaam of e-mailadres", + "s18n-b79edd2e426f90401c04869346b503c7": "Gebruikers zoeken", + "s18n-ee51fa9d5097c84d2fa6c885bf2d5d84": "Geen gebruikers gevonden", + "s18n-14c4b06b824ec593239362517f538b29": "Gebruikersnaam", + "s18n-0c83f57c786a0b4a39efab23731c7ebc": "E-mail", + "s18n-e1260894f59eeae98c8440899de4df8d": "Afhandelen", + "s18n-4bc61296b766756f1c7296489633bf32": "Verwijderen (pseudonimiseren)", + "s18n-c0f4afd3614929f1c803f3a01414a6c7": "Verwijderen (volledig)", + "s18n-6356f32d0c02c8f90cb59a77e16e8fe2": "Gebruiker deblokkeren", + "s18n-2327a01afbee025fb5913357c9d6b1b3": "Gebruiker blokkeren", + "s18n-76a0f6752a45d8af6343ef3e2b6f522a": "Enkele reactie", + "s18n-c426859e50a35617d863cdad2b9c84aa": "Reacties op pagina", + "s18n-a64d776275f13a51790bb460774b9129": "Reacties van gebruiker", + "s18n-9bc65c2abec141778ffaa729489f3e87": "Gebruikers", + "s18n-ccd1066343c95877b75b79d47c36bebe": "Configuratie", + "s18n-5b49260517622682a058b69f996d06eb": "Bedankt voor uw reactie!", + "s18n-74196a783a6f1707a43cc8117f0d9c83": "Bedankt voor uw reactie! Bevestig het abonneren op verdere reacties via de link in het bericht dat u per e-mail ontvangt.", + "s18n-a939eb542e34cd502b3f7352b2e0f715": "Bedankt voor het stemmen op deze reactie!", + "s18n-d2a9677817ee08ed05bf9fd868669756": "Onbekende fout opgetreden, herlaad de pagina probeer het nogmaals.", + "s18n-05b85714aa8f1b364f930e2539059b5e": "Fout opgetreden: de gebruikersnaam is ongeldig of langer dan 42 tekens.", + "s18n-321e8b481f0ccd62df535256e8e6d2c6": "Fout opgetreden: het e-mailadres is ongeldig.", + "s18n-fcd0c3a087c5123ffdecc20fe9015870": "Fout opgetreden: er is geen bericht ingevoerd.", + "s18n-2afa2d90ce3343fbe188b9f49ad5797d": "Fout opgetreden: er is geen titel ingevoerd.", + "s18n-bee1741efe0c735d2c7180771586faf0": "Fout opgetreden: de gebruiksvoorwaarden zijn niet geaccepteerd.", + "s18n-92fe96d6ccee901f94fad0000369a9b7": "Fout opgetreden: uw IP-adres of e-mailadres is gemarkeerd als SPAM.", + "s18n-9b264fc6137096f8a40acde68f6ae562": "Fout opgetreden: u heeft reeds gestemd op deze reactie.", + "s18n-ca62db4704290ef1a7e65df3ffc7983b": "Mijn gegevens (inclusief mijn gepseudonimiseerd IP-adres) mogen worden opgeslagen.", + "s18n-01c611362e8b046f32650b85ce161559": "Ongeldige fout opgetreden.", + "s18n-c4f94c6995b0376f28276e432bed75fa": "Het CSRF-token ontbreekt.", + "s18n-a573e7b86e41522c7a291846aa109104": "Het CSRF-token is ongeldig.", + "s18n-3e8909518ce4728685aa09cdde3caa22": "U heeft geen machtiging voor het aanroepen van deze functie.", + "s18n-b60a6a8a529a9f0497134205bab15e77": "U heeft geen machtiging voor het uitvoeren van deze actie.", + "s18n-3fee90e2f59aeb29b74c1c21648ba712": "De Captcha is succesvol gegenereerd.", + "s18n-56abae3e615ae5b5609c32852b777d46": "De actie is onbekend of ongeldig.", + "s18n-be5136b4f2b33c80e3afd377ee993acb": "Er is een onbekende fout opgetreden.", + "s18n-46d4c97e91319867654f7cc80c439ba4": "Geen uniek gebruikers-ID gevonden.", + "s18n-af1ba1dd4eab5562f78c65bc89a0a7e9": "Actie is succesvol afgerond.", + "s18n-a5193444ee82c18bac726b35a1704d03": "Instellingen zijn opgeslagen.", + "s18n-717c8267d40664ccf7ef25a26ff9cde6": "Backup is voltooid.", + "s18n-a1e2b7401861cee01c172878f104bd8c": "Reacties niet toestaan", + "s18n-15802277ea1cdfcbacd6308fa0c7c30f": "Snicker-plugin uitschakelen", + "s18n-877d58f21c87442efa4081112a6cb07a": "Het uitschakelen van de Snicker<\/b>-plugin zal alle reacties verwijderen!", + "s18n-a4983c86683f8d8598c0513339550dc0": "Eerst een backup maken van de reacties?", + "s18n-4cce9e52118cc659be5070b2f08cdd91": "De backup zal worden opgeslagen in %s.", + "s18n-5f87fd2e0fa992d37c814bb4ca299646": "Ja, backup maken", + "s18n-3d414feb412a1f4cd9324f6411c76329": "Nee, alleen uitschakelen", + "s18n-10aec35353f9c4096a71c38654c3d402": "Annuleren", + "s18n-bc6d6a26d44b6f39a0e7b6c7787f3295": "Reacties zijn voor deze pagina uitgeschakeld.", + "s18n-a8e30d73eddea9866cf99ecd6e8467b5": "Wees de eerste met een reactie hier!", + "s18n-b3afbadaa2f1c79f8b3999be7fd9719f": "Het antwoord op de Captcha is onjuist.", + "s18n-fb74aafe8bf1fd4d8f7a6e1ff73028f7": "UID voor de reactie bestaat niet of is ongeldig.", + "s18n-25dea7cb70f98250b388f6ab0ddf20cb": "Status van de reactie is onbekend of ongeldig.", + "s18n-2cd68855bdc54ff5e3c191a6333ff75d": "De nieuwe status van de reactie kon niet worden opgeslagen.", + "s18n-c0937505b7afa81a053077bc7ae369a5": "De nieuwe status van de reactie is opgeslagen.", + "s18n-6b2c8084c67f24bb73d95031bb570ef7": "De reactie kon niet worden verwijderd.", + "s18n-376388311a80dbde63fde7f6c72081e0": "De reactie is verwijderd.", + "s18n-83bbb9e8745cc95e730fe7b8de9345f1": "Ingelogd als %s (%s)", + "s18n-91fb98e1ac4cf76b7a5b8bae09051e2a": "Gebruikersnaam", + "s18n-31f6da7a30e7acf1f82451bfd1a7f8fa": "E-mailadres", + "s18n-f794080a5a29e35233c82df85f1207eb": "Reactie...", + "s18n-a363b8d13575101a0226e8d0d054f2e7": "Beantwoorden", + "s18n-f10db888c5e63b343000cffc038e0a46": "schreef", + "s18n-3cc5bcf15d6b8faed118e2ce72d19a1e": "Ik ga akkoord met de %s!", + "s18n-2af0aab477f402e0f4ad7a27e6c9f952": "Vorige", + "s18n-8538431db22040e2147b363f86a2e2f0": "Volgende", + "s18n-ae0dbd5cc42a6db191db5e0083bcb307": "Deze reactie moet nog worden goedgekeurd.", + "s18n-48df9c3f3cca3fb2b8bcf811633bee06": "Geschreven door %s", + "s18n-81fdc9813cebe0553c55e78dc2b6029f": "op %s", + "s18n-be1ab1632e4285edc3733b142935c60b": "Vind ik leuk", + "s18n-bc8b79025e4595298669fd21da814941": "Vind ik niet leuk", + "s18n-e84afaab83ecb301b3d97ce4174d2773": "Beantwoorden" +} diff --git a/metadata.json b/metadata.json new file mode 100644 index 0000000..25d251f --- /dev/null +++ b/metadata.json @@ -0,0 +1,10 @@ +{ + "author": "SamBrishes, pytesNET", + "email": "sam@pytes.net", + "website": "https://www.pytes.net", + "version": "0.1.2", + "releaseDate": "2019-05-06", + "license": "MIT", + "compatible": "3.5.0", + "notes": "" +} diff --git a/plugin.php b/plugin.php new file mode 100644 index 0000000..12ec807 --- /dev/null +++ b/plugin.php @@ -0,0 +1,893 @@ + + | @version 0.1.2 [0.1.0] - Alpha + | + | @website https://github.com/pytesNET/snicker + | @license X11 / MIT License + | @copyright Copyright © 2019 SamBrishes, pytesNET + */ + if(!defined("BLUDIT")){ die("Go directly to Jail. Do not pass Go. Do not collect 200 Cookies!"); } + + require_once "system/functions.php"; // Load Basic Functions + + class SnickerPlugin extends Plugin{ + /* + | BACKEND VARIABLES + */ + private $backend = false; // Is Backend + private $backendView = null; // Backend View / File + private $backendRequest = null; // Backend Request Type ("post", "get", "ajax") + + /* + | CONSTRUCTOR + | @since 0.1.0 + */ + public function __construct(){ + global $SnickerPlugin; + $SnickerPlugin = $this; + parent::__construct(); + } + + +## +## HELPER METHODs +## + + /* + | HELPER :: SELECTED + | @since 0.1.0 + | + | @param string The respective option key (used in `getValue()`). + | @param multi The value to compare with. + | @param bool TRUE to print `selected="selected"`, FALSE to return the string. + | Use `null` to return as boolean! + | + | @return multi The respective string, nothing or a BOOLEAN indicator. + */ + public function selected($field, $value = true, $print = true){ + if(sn_config($field) == $value){ + $selected = 'selected="selected"'; + } else { + $selected = ''; + } + if($print === null){ + return !empty($selected); + } + if(!$print){ + return $selected; + } + print($selected); + } + + /* + | HELPER :: CHECKED + | @since 0.1.0 + | + | @param string The respective option key (used in `getValue()`). + | @param multi The value to compare with. + | @param bool TRUE to print `checked="checked"`, FALSE to return the string. + | Use `null` to return as boolean! + | + | @return multi The respective string, nothing or a BOOLEAN indicator. + */ + public function checked($field, $value = true, $print = true){ + if(sn_config($field) == $value){ + $checked = 'checked="checked"'; + } else { + $checked = ''; + } + if($print === null){ + return !empty($checked); + } + if(!$print){ + return $checked; + } + print($checked); + } + + +## +## PLUGIN HOOKs +## + + /* + | PLUGIN :: GET VALUE + | @since 0.1.2 + */ + public function getValue($field, $html = true){ + if(isset($this->db[$field])){ + $data = strpos($field, "string_") === 0? sn__($this->db[$field]): $this->db[$field]; + return ($html)? $data: Sanitize::htmlDecode($data); + } + return isset($this->dbFields[$field])? $this->dbFields[$field]: null; + } + + /* + | PLUGIN :: INIT + | @since 0.1.0 + | @update 0.1.1 + */ + public function init(){ + global $url; + + // Init Default Settings + $this->dbFields = array( + "moderation" => true, + "moderation_loggedin" => true, + "moderation_approved" => true, + "comment_on_public" => true, + "comment_on_static" => false, + "comment_on_sticky" => true, + "comment_title" => "optional", + "comment_limit" => 0, + "comment_depth" => 3, + "comment_markup_html" => true, + "comment_markup_markdown" => false, + "comment_vote_storage" => "session", + "comment_enable_like" => true, + "comment_enable_dislike" => true, + "frontend_captcha" => function_exists("imagettfbbox")? "gregwar": "purecaptcha", + "frontend_recaptcha_public" => "", + "frontend_recaptcha_private"=> "", + "frontend_terms" => "default", + "frontend_filter" => "pageEnd", + "frontend_template" => "default", + "frontend_order" => "date_desc", + "frontend_form" => "top", + "frontend_per_page" => 15, + "frontend_ajax" => true, + "frontend_avatar" => "gravatar", + "frontend_avatar_users" => true, + "frontend_gravatar" => "mp", + "subscription" => false, + "subscription_from" => "ticker@{$_SERVER["SERVER_NAME"]}", + "subscription_reply" => "noreply@{$_SERVER["SERVER_NAME"]}", + "subscription_optin" => "default", + "subscription_ticker" => "default", + + // Frontend Messages, can be changed by the user + "string_success_1" => sn__("Thanks for your comment!"), + "string_success_2" => sn__("Thanks for your comment, please confirm your subscription via the link we sent to your eMail address!"), + "string_success_3" => sn__("Thanks for voting this comment!"), + "string_error_1" => sn__("An unknown error occured, please reload the page and try it again!"), + "string_error_2" => sn__("An error occured: The passed Username is invalid or too long (42 characters only)!"), + "string_error_3" => sn__("An error occured: The passed eMail address is invalid!"), + "string_error_4" => sn__("An error occured: The comment text is missing!"), + "string_error_5" => sn__("An error occured: The comment title is missing!"), + "string_error_6" => sn__("An error occured: You need to accept the Terms to comment!"), + "string_error_7" => sn__("An error occured: Your IP address or eMail address has been marked as Spam!"), + "string_error_8" => sn__("An error occured: You already rated this comment!"), + "string_terms_of_use" => sn__("I agree that my data (incl. my anonymized IP address) gets stored!") + ); + + // Check Backend + $this->backend = (trim($url->activeFilter(), "/") == ADMIN_URI_FILTER); + } + + /* + | PLUGIN :: OVERWRITE INSTALLED + | @since 0.1.0 + | @update 0.1.1 + */ + public function installed(){ + global $Snicker, // Main Comment Handler + $SnickerIndex, // Main Comment Indexer + $SnickerUsers, // Main Comment Users + $SnickerVotes; // Main Comment Votes + + if(file_exists($this->filenameDb)){ + if(!defined("SNICKER")){ + define("SNICKER", true); + define("SNICKER_PATH", PATH_PLUGINS . basename(__DIR__) . DS); + define("SNICKER_DOMAIN", DOMAIN_PLUGINS . basename(__DIR__) . "/"); + define("SNICKER_VERSION", "0.1.2"); + + // DataBases + define("DB_SNICKER_COMMENTS", $this->workspace() . "pages" . DS); + define("DB_SNICKER_INDEX", $this->workspace() . "comments-index.php"); + define("DB_SNICKER_USERS", $this->workspace() . "comments-users.php"); + define("DB_SNICKER_VOTES", $this->workspace() . "comments-votes.php"); + + // Pages Filter + if(!file_exists(DB_SNICKER_COMMENTS)){ + @mkdir(DB_SNICKER_COMMENTS); + } + + // Load Plugin + require_once "system/abstract.comments-theme.php"; + require_once "system/class.comment.php"; + require_once "system/class.comments.php"; + require_once "system/class.comments-index.php"; + require_once "system/class.comments-users.php"; + require_once "system/class.comments-votes.php"; + require_once "system/class.snicker.php"; + require_once "includes/autoload.php"; + } else { + $Snicker = new Snicker(); + $SnickerIndex = new CommentsIndex(); + $SnickerUsers = new CommentsUsers(); + $SnickerVotes = new CommentsVotes(); + $this->request(); + } + return true; + } + return false; + } + + +## +## API METHODs +## + + /* + | API :: HANDLE RESPONSE + | @since 0.1.0 + | + | @param array The response data, which MUST contain at least the status: + | "error" The error message (required). + | "success" The success message (required). + | + | :: NON-AJAX ONLY + | "referer" A referer URL (The current URL is used, if not present) + | + | :: AJAX-BASED ONLY + | :any Any additional data, which should return to the client. + | + | @return none This method calls the die(); method at any time! + */ + public function response($data = array(), $key = null){ + global $url; + + // Validate + if(isset($data["success"]) || isset($data["error"])){ + $status = isset($data["success"]); + } else { + $status = false; + $data["error"] = sn__("An unknown error occured!"); + } + + // POST Redirect + if($this->backendRequest !== "ajax"){ + if($status){ + $key = empty($key)? "snicker-success": $key; + Alert::set($data["success"], ALERT_STATUS_OK, $key); + } else { + $key = empty($key)? "snicker-alert": $key; + Alert::set($data["error"], ALERT_STATUS_FAIL, $key); + } + + if($data["referer"]){ + Redirect::url($data["referer"]); + } else { + $action = isset($_GET["snicker"])? $_GET["snicker"]: $_POST["snicker"]; + Redirect::url(HTML_PATH_ADMIN_ROOT . $url->slug() . "#{$action}"); + } + die(); + } + + // AJAX Print + if(!is_array($data)){ + $data = array(); + } + $data["status"] = ($status)? "success": "error"; + $data = json_encode($data); + + header("Content-Type: application/json"); + header("Content-Length: " . strlen($data)); + print($data); + die(); + } + + /* + | API :: HANDLE REQUESTS + | @since 0.1.0 + | @update 0.1.1 + */ + public function request(){ + global $login, $security, $url, $Snicker; + + // Get POST/GET Request + if(isset($_POST["action"]) && $_POST["action"] === "snicker"){ + $data = $_POST; + $this->backendRequest = "post"; + } else if(isset($_GET["action"]) && $_GET["action"] === "snicker"){ + $data = $_GET; + $this->backendRequest = "get"; + } + if(!(isset($data) && isset($data["snicker"]))){ + $this->backendRequest = null; + return null; + } + + // Get AJAX Request + $ajax = "HTTP_X_REQUESTED_WITH"; + if(strpos($url->slug(), "snicker/ajax") === 0){ + if(isset($_SERVER[$ajax]) && $_SERVER[$ajax] === "XMLHttpRequest"){ + $this->backendRequest = "ajax"; + } else { + return Redirect::url(HTML_PATH_ADMIN_ROOT . "snicker/"); + } + } else if(isset($_SERVER[$ajax]) && $_SERVER[$ajax] === "XMLHttpRequest"){ + print("Invalid AJAX Call"); die(); + } + if($this->backendRequest === "ajax" && !sn_config("frontend_template")){ + print("AJAX Calls has been disabled"); die(); + } + + // Start Session + if(!Session::started()){ + Session::start(); + } + + $key = null; + if(in_array($data["snicker"], array("add", "edit", "delete", "config", "users", "backup", "moderate"))){ + $key = "alert"; + } + + // Check CSRF Token + if(!empty($key)){ + if(!isset($data["tokenCSRF"])){ + return $this->response(array( + "error" => sn__("The CSRF Token is missing!") + )); + } + if(!$security->validateTokenCSRF($data["tokenCSRF"])){ + return $this->response(array( + "error" => sn__("The CSRF Token is invalid!") + )); + } + } + + // Check Permissions + if(!empty($key)){ + if(!is_a($login, "Login")){ + $login = new Login(); + } + if(!$login->isLogged()){ + return $this->response(array( + "error" => sn__("You don't have the permission to call this action!") + )); + } + if($login->role() !== "admin"){ + return $this->response(array( + "error" => sn__("You don't have the permission to perform this action!") + )); + } + } + + // Route + switch($data["snicker"]){ + case "comment": //@fallthrough + case "reply": //@fallthrough + case "add": + return $Snicker->writeComment($data["comment"], $key); + /* case "update": */ //@todo User can edit his own comments + case "edit": + return $Snicker->editComment($data["uid"], $data["comment"], $key); + /* case "remove": */ //@todo User can delete his own comments + case "delete": + return $Snicker->deleteComment($data["uid"], $key); + case "moderate": + return $Snicker->moderateComment($data["uid"], $data["status"], $key); + case "list": //@fallthrough + case "get": + return $Snicker->renderComment($data); + case "rate": + return $Snicker->rateComment($data["uid"], $data["type"]); + case "users": + return $this->user($data); + case "configure": + return $this->config($data); + case "backup": + return $this->backup(); + case "captcha": + return $this->response(array( + "success" => sn__("The Captcha Image could be successfully created!"), + "captcha" => $Snicker->generateCaptcha(150, 40, true) + )); + } + return $this->response(array( + "error" => sn__("The passed action is unknown or invalid!") + ), "alert"); + } + + /* + | API :: HANDLE USERs + | @since 0.1.0 + */ + private function user($data){ + global $SnickerIndex, $SnickerUsers; + + // Validate Data + if(!isset($data["uuid"]) || !isset($data["handle"])){ + return $this->response(array( + "error" => sn__("An unknown error is occured!") + ), "alert"); + } + + // Validata UUID + if(!$SnickerUsers->exists($data["uuid"])){ + return $this->response(array( + "error" => sn__("An unique user ID does not exist!") + ), "alert"); + } + + // Handle + if($data["handle"] === "delete"){ + $comments = $SnickerUsers->db[$data["uuid"]]["comments"]; + foreach($comments AS $uid){ + if(!$SnickerIndex->exists($uid)){ + continue; + } + $index = $SnickerIndex->getComment($uid); + $comment = new Comments($index["page_uuid"]); + + if(isset($data["anonymize"]) && $data["anonymize"] === "true"){ + $comment = new Comments($index["page_uuid"]); + $comment->edit($uid, array("author" => "anonymous")); + } else { + $comment = new Comments($index["page_uuid"]); + $comment->delete($uid); + } + } + $status = $SnickerUsers->delete($data["uuid"]); + } else if($data["handle"] === "block"){ + $status = $SnickerUsers->edit($data["uuid"], null, null, true); + } else if($data["handle"] === "unblock"){ + $status = $SnickerUsers->edit($data["uuid"], null, null, false); + } + + // Redirect + if(!isset($status)){ + return $this->response(array( + "error" => sn__("The passed action is unknown or invalid!") + ), "alert"); + } + if($status === false){ + return $this->response(array( + "error" => sn__("An unknown error is occured!") + ), "alert"); + } + return $this->response(array( + "success" => sn__("The action has been performed successfully!") + ), "alert"); + } + + /* + | API :: HANDLE CONFIGURATION + | @since 0.1.0 + | @update 0.2.0 + */ + private function config($data){ + global $pages, $Snicker; + $config = array(); + + // Validations + $text = array("frontend_recaptcha_public", "frontend_recaptcha_private"); + $numbers = array("comment_limit", "comment_depth", "frontend_per_page"); + $selects = array( + "comment_title" => array("optional", "required", "disabled"), + "comment_vote_storage" => array("cookie", "session", "database"), + "frontend_captcha" => array("disabled", "purecaptcha", "gregwar", "recaptchav2", "recaptchav3"), + "frontend_avatar" => array("gravatar", "identicon", "static", "initials"), + "frontend_gravatar" => array("mp", "identicon", "monsterid", "wavatar", "retro", "robohash", "blank"), + "frontend_filter" => array("disabled", "pageBegin", "pageEnd", "siteBodyBegin", "siteBodyEnd"), + "frontend_order" => array("date_desc", "date_asc"), + "frontend_form" => array("top", "bottom") + ); + $emails = array("subscription_from", "subscription_reply"); + $pageid = array("frontend_terms", "subscription_optin", "subscription_ticker"); + + // Loop DB Fields + foreach($this->dbFields AS $field => $value){ + if(!isset($data[$field])){ + $config[$field] = is_bool($value)? false: ""; + continue; + } + + // Sanitize Booleans + if(is_bool($value)){ + $config[$field] = ($data[$field] === "true" || $data[$field] === true); + continue; + } + + // Sanitize Numbers + if(in_array($field, $numbers)){ + if($data[$field] < 0 || !is_numeric($data[$field])){ + $config[$field] = 0; + } + $config[$field] = (int) $data[$field]; + continue; + } + + // Sanitize Selection + if(array_key_exists($field, $selects)){ + if(in_array($data[$field], $selects[$field])){ + $config[$field] = $data[$field]; + } else { + $config[$field] = $value; + } + continue; + } + + // Sanitize eMails + if(in_array($field, $emails)){ + if(Valid::email($data[$field])){ + $config[$field] = Sanitize::email($data[$field]); + } else { + $config[$field] = $value; + } + continue; + } + + // Sanitize Pages + if(in_array($field, $pageid)){ + $default = in_array($data[$field], array("default", "disabled")); + if($default || $pages->exists($data[$field])){ + $config[$field] = $data[$field]; + } else { + $config[$field] = $value; + } + continue; + } + + // Sanitize Template + if($field == "frontend_template"){ + if($Snicker->hasTheme($data[$field])){ + $config[$field] = $data[$field]; + } else { + $config[$field] = $value; + } + continue; + } + + // Sanitize Strings + if(strpos($field, "string_") === 0 || in_array($field, $text)){ + $config[$field] = Sanitize::html(strip_tags($data[$field])); + if(empty($config[$field])){ + $config[$field] = $value; + } + continue; + } + } + + // Save & Return + $this->db = array_merge($this->db, $config); + if(!$this->save()){ + return $this->response(array( + "error" => sn__("An unknown error is occured!") + ), "alert"); + } + return $this->response(array( + "success" => sn__("The settings has been updated successfully!") + ), "alert"); + } + + /* + | API :: CREATE BACKUP + | @since 0.1.0 + */ + private function backup(){ + $filename = "snicker-backup-" . time() . ".zip"; + + // Create Backup + $zip = new PIT\Zip(); + $zip->addFolder($this->workspace(), "/", true, true); + $zip->save(PATH_TMP . $filename); + + // Return + return $this->response(array( + "success" => sn__("The backup has been created successfully!"), + "referer" => DOMAIN_ADMIN . "uninstall-plugin/SnickerPlugin" + ), "alert"); + } + + +## +## BACKEND HOOKs +## + + /* + | HOOK :: INIT ADMINISTRATION + | @since 0.1.0 + */ + public function beforeAdminLoad(){ + global $url; + + // Check if the current View is the "snicker" + if(strpos($url->slug(), "snicker") !== 0){ + return false; + } + checkRole(array("admin")); + + // Set Backend View + $split = str_replace("snicker", "", trim($url->slug(), "/")); + if(!empty($split) && $split !== "/" && isset($_GET["uid"])){ + $this->backendView = "edit"; + } else { + $this->backendView = "index"; + } + } + + /* + | HOOK :: LOAD ADMINISTRATION FILES + | @since 0.1.0 + */ + public function adminHead(){ + global $page, $security, $url; + + $js = SNICKER_DOMAIN . "admin/js/"; + $css = SNICKER_DOMAIN . "admin/css/"; + $slug = explode("/", str_replace(HTML_PATH_ADMIN_ROOT, "", $url->uri())); + + // Admin Header + ob_start(); + if($slug[0] === "new-content" || $slug[0] === "edit-content"){ + ?> + + + + + getTokenCSRF(); + ?> + + backend || !$this->backendView){ + return false; + } + ob_start(); + } + + /* + | HOOK :: AFTER ADMIN CONTENT + | @since 0.1.0 + */ + public function adminBodyEnd(){ + global $url, $SnickerPlugin; + if(!$this->backend || !$this->backendView){ + $slug = explode("/", str_replace(HTML_PATH_ADMIN_ROOT, "", $url->uri())); + if($slug[0] === "plugins"){ + ?> + + backendView}.php")){ + require SNICKER_PATH . "admin" . DS . "{$this->backendView}.php"; + $add = ob_get_contents(); + } + ob_end_clean(); + + // Inject Code + if(isset($add) && !empty($add)){ + $regexp = "#(\
)(.*?)(\<\/div\>)#s"; + $content = preg_replace($regexp, "$1{$add}$3", $content); + } + print($content); + } + + /* + | HOOK :: SHOW SIDEBAR MENU + | @since 0.1.0 + */ + public function adminSidebar(){ + global $SnickerIndex; + + $count = $SnickerIndex->count("pending"); + $count = ($count > 99)? "99+": $count; + + ob_start(); + ?> + + Snicker + + + + + published() && !empty($page->uuid())){ + $comments = new Comments($page->uuid()); + } else { + $comments = false; + } + } + + /* + | HOOK :: FRONTEND HEADER + | @since 0.1.0 + */ + public function siteHead(){ + global $Snicker; + + if(($theme = $Snicker->getTheme()) === false){ + return false; + } + if(!empty($theme::SNICKER_JS)){ + $file = SNICKER_DOMAIN . "themes/" . sn_config("frontend_template") . "/" . $theme::SNICKER_JS; + ?> + + + + + + render()); + } + + /* + | HOOK :: FRONTEND CONTENT + | @since 0.1.0 + */ + public function pageBegin(){ + global $Snicker; + if(sn_config("frontend_filter") !== "pageBegin"){ + return false; // Owo + } + print($Snicker->render()); + } + + /* + | HOOK :: FRONTEND CONTENT + | @since 0.1.0 + */ + public function pageEnd(){ + global $Snicker; + if(sn_config("frontend_filter") !== "pageEnd"){ + return false; // owO + } + print($Snicker->render()); + } + + /* + | HOOK :: FRONTEND CONTENT + | @since 0.1.0 + */ + public function siteBodyEnd(){ + global $Snicker; + if(sn_config("frontend_filter") !== "siteBodyEnd"){ + return false; // OwO + } + print($Snicker->render()); + } + } diff --git a/system/abstract.comments-theme.php b/system/abstract.comments-theme.php new file mode 100644 index 0000000..e5ba5d3 --- /dev/null +++ b/system/abstract.comments-theme.php @@ -0,0 +1,46 @@ + + | @version 0.1.2 [0.1.0] - Alpha + | + | @website https://github.com/pytesNET/snicker + | @license X11 / MIT License + | @copyright Copyright © 2019 SamBrishes, pytesNET + */ + if(!defined("BLUDIT")){ die("Go directly to Jail. Do not pass Go. Do not collect 200 Cookies!"); } + + abstract class CommentsTheme{ + /* + | REQUIRED :: FORM + | @note This method renders the comment form used on the frontend. + | + | @param multi The previously passed username (on errors only) + | An `array(username, hash, nickname)` array if the user is logged in. + | @param string The previously passed email address (on errors only)! + | @param string The previously passed comment title (on errors only)! + | @param string The previously passed comment message (on errors only)! + */ + abstract public function form($username = "", $email = "", $title = "", $message = ""); + + /* + | REQUIRED :: COMMENT + | @note This method renders the single shown comments on the frontend. + | + | @param object The comment instance. + | @param string The unique comment UID. + */ + abstract public function comment($comment, $uid, $depth); + + /* + | REQUIRED :: PAGINATION + | @note This method renders the pagination for the comment section. + | + | @param string The called location: "top" or "bottom". + | @param int The current comment page.<, startin with 1. + | @param int The number of comments to be shown per page. + | @param int The total number of comments for the content page. + */ + abstract public function pagination($loction, $cpage, $limit, $count); + } diff --git a/system/class.comment.php b/system/class.comment.php new file mode 100644 index 0000000..d638114 --- /dev/null +++ b/system/class.comment.php @@ -0,0 +1,494 @@ + + | @version 0.1.2 [0.1.0] - Alpha + | + | @website https://github.com/pytesNET/snicker + | @license X11 / MIT License + | @copyright Copyright © 2019 SamBrishes, pytesNET + */ + if(!defined("BLUDIT")){ die("Go directly to Jail. Do not pass Go. Do not collect 200 Cookies!"); } + + class Comment{ + /* + | CONSTRUCTOR + | @since 0.1.0 + | + | @param multi The unique comment id or FALSE. + | @param multi The unique page comment ID. + */ + public function __construct($uid, $uuid){ + $this->vars["uid"] = $uid; + if($uid === false){ + $row = array( + "type" => "comment", + "depth" => 1, + "title" => "", + "comment" => "", + "rating" => [0, 0], + "page_uuid" => "", + "parent_uid" => "", + "author" => "", + "subscribe" => false, + "date" => "", + "dateModified" => "", + "dateAudit" => "", + "custom" => array() + ); + } else { + $comments = new Comments($uuid); + if(Text::isEmpty($uid) || !$comments->exists($uid)){ + // @todo Throw Error + } + $row = $comments->getCommentDB($uid); + } + + // Set Class + foreach($row AS $field => $value){ + if(strpos($field, "date") === 0){ + $this->vars["{$field}Raw"] = $value; + } else { + $this->vars[$field] = $value; + } + } + } + + /* + | PUBLIC :: GET VALUE + | @since 0.1.0 + | + | @param string The unique field key. + | @param multi The default value, which should return if the field key doesnt exist. + | + | @multi multi The respective field value on success, $default otherwise. + */ + public function getValue($field, $default = false){ + if(isset($this->vars[$field])){ + return $this->vars[$field]; + } + return $default; + } + + /* + | PUBLIC :: SET FIELD + | @since 0.1.0 + | + | @param string The unique field key. + | @param multi The respective field value, which you want to set. + | + | @return bool TRUE + */ + public function setField($field, $value = NULL){ + if(is_array($field)){ + foreach($field AS $k => $v){ + $this->setField($k, $v); + } + return true; + } + $this->vars[$field] = $value; + return true; + } + + + /* + | FIELD :: COMMENT RAW + | @since 0.1.0 + | + | @return string The (sanitized) raw content on success, FALSE on failure. + */ + public function commentRaw(){ + return $this->getValue("comment"); + } + + /* + | FIELD :: COMMENT + | @since 0.1.0 + | + | @return string The (sanitized) content on success, FALSE on failure. + */ + public function comment(){ + $content = $this->getValue("comment"); + if(sn_config("comment_markup_html")){ + $content = Sanitize::htmlDecode($content); + } + if(sn_config("comment_markup_markdown")){ + $parsedown = new Parsedown(); + $content = $parsedown->text($content); + } + return $content; + } + + /* + | FIELD :: GET UID + | @since 0.1.0 + */ + public function uid(){ + return $this->getValue("uid"); + } + public function key(){ + return $this->getValue("uid"); + } + + /* + | FIELD :: GET (COMMENT FILE) PATH + | @since 0.1.0 + */ + public function path(){ + return PATH_PAGES . $this->getValue("page_key") . DS . "comments"; + } + + /* + | FIELD :: GET (COMMENT FILE) PATH / FILE + | @since 0.1.0 + */ + public function file(){ + return $this->path() . DS . "c_" . $this->getValue("uid") . ".php"; + } + + /* + | FIELD :: GET TYPE + | @since 0.1.0 + */ + public function type(){ + return $this->getValue("type"); + } + public function isComment(){ + return $this->getValue("type") === "comment"; + } + public function isReply(){ + return $this->getValue("type") === "reply"; + } + public function isPingback(){ + return $this->getValue("type") === "pingback"; + } + + /* + | FIELD :: GET DEPTH + | @since 0.1.0 + */ + public function depth(){ + return (int) $this->getValue("depth"); + } + + /* + | FIELD :: TITLE + | @since 0.1.0 + | + | @param bool TRUE to sanitize the content, FALSE to return it plain. + | + | @return string The respective comment title as STRING. + */ + public function title($sanitize = true){ + if($sanitize){ + return Sanitize::html($this->getValue("title")); + } + return $this->getValue("title"); + } + + /* + | FIELD :: GET STATUS + | @since 0.1.0 + */ + public function status(){ + return $this->getValue("status"); + } + public function isPending(){ + return $this->getValue("status") === "pending"; + } + public function isPublic(){ + return $this->getValue("status") === "approved"; + } + public function isApproved(){ + return $this->getValue("status") === "approved"; + } + public function isRejected(){ + return $this->getValue("status") === "rejected"; + } + public function isSpam(){ + return $this->getValue("status") === "spam"; + } + + /* + | FIELD :: GET RATING + | @since 0.1.0 + */ + public function rating(){ + return $this->getValue("rating"); + } + + /* + | FIELD :: GET LIKE + | @since 0.1.0 + */ + public function like(){ + $rating = $this->getValue("rating"); + if(is_array($rating) && count($rating) >= 1){ + return $rating[0]; + } + return 0; + } + + /* + | FIELD :: GET DISLIKE + | @since 0.1.0 + */ + public function dislike(){ + $rating = $this->getValue("rating"); + if(is_array($rating) && count($rating) >= 2){ + return $rating[1]; + } + return 0; + } + + /* + | FIELD :: GET PAGE KEY + | @since 0.1.0 + */ + public function page_key(){ + return $this->getValue("page_key"); + } + + /* + | FIELD :: GET PAGE UUID + | @since 0.1.0 + */ + public function page_uuid(){ + return $this->getValue("page_uuid"); + } + + /* + | FIELD :: GET PARENT UID + | @since 0.1.0 + */ + public function parent_uid(){ + return $this->getValue("parent_uid"); + } + + /* + | FIELD :: GET PARENT + | @since 0.1.0 + */ + public function parent(){ + global $comments; + if($comments->exists($this->getValue("parent_uid"))){ + return new Comment($this->getValue("parent_uid")); + } + return false; + } + + /* + | FIELD :: GET CHILDREN + | @since 0.1.0 + | + | @param multi The single comment status which should return, multiple as ARRAY. + | Use `null` to return each children comment. + | @param string The return type, which allows the following strings: + | "uids" Return just the respective UID / keys + | "keys" Return just the respective UID / keys + | "objects" Return single Comment instances + | "arrays" Return the unformatted DB arrays + | + | @return multi FALSE on error, the respective array on succes. + */ + public function children($status = "approved", $return = "objects"){ + global $comments; + + // Check Parameter + if(is_string($status)){ + $status = array($status); + } + if(!is_array($status) && $status !== null){ + return false; + } + + // Get Children + $return = array(); + foreach($this->getDB(false) AS $uid => $value){ + if($value["parent"] !== $this->getValue("uid")){ + continue; + } + if(is_array($status) && !in_array($value["status"], $status)){ + continue; + } + + if($return === "uids" || $return == "keys"){ + $return[] = $uid; + } else if($return === "objects"){ + $return[$uid] = new Comment($uid); + } else { + $return[$uid] = $value; + } + } + return $return; + } + + /* + | FIELD :: GET UUID + | @since 0.1.0 + */ + public function uuid(){ + return $this->getValue("uuid"); + } + + /* + | FIELD :: GET USERNAME + | @since 0.1.0 + */ + public function username(){ + global $L, $users, $SnickerUsers; + + list($type, $id) = array_pad(explode("::", $this->getValue("author"), 2), 2, null); + switch($type){ + case "bludit": + if(!$users->exists($id)){ + break; + } + $user = new User($id); + return $user->nickname(); + case "guest": + if(!$SnickerUsers->exists($id)){ + break; + } + return $SnickerUsers->db[$id]["username"]; + case "unknown": + return $L->g("Unknown User"); + } + return false; + } + + /* + | FIELD :: GET EMAIL + | @since 0.1.0 + */ + public function email(){ + global $L, $users, $SnickerUsers; + + list($type, $id) = array_pad(explode("::", $this->getValue("author"), 2), 2, null); + switch($type){ + case "bludit": + if(!$users->exists($id)){ + break; + } + $user = new User($id); + return $user->email(); + case "guest": + if(!$SnickerUsers->exists($id)){ + break; + } + return $SnickerUsers->db[$id]["email"]; + case "unknown": + return "unknown@" . $_SERVER["SERVER_NAME"]; + } + return false; + } + + /* + | FIELD :: SUBSCRIBE + | @since 0.1.0 + */ + public function subscribe(){ + return $this->getValue("subscribe"); + } + public function hasSubscribed(){ + return $this->getValue("subscribe") === true; + } + + /* + | FIELD :: GET AVATAR + | @since 0.1.0 + */ + public function avatar($size = "64"){ + $user = $this->username(); + $email = md5(strtolower(trim($this->email()))); + $avatar = $this->avatar_url($size); + + // Force Profile Picture + $force = false; + if(sn_config("frontend_avatar_users")){ + $force = (strpos($avatar, DOMAIN_UPLOADS_PROFILES) !== false); + } + + // Return IMG Tag + if(sn_config("frontend_avatar") === "identicon" && !$force){ + return ''.$user.''; + } + return ''.$user.''; + } + + /* + | FIELD :: GET AVATAR URL + | @since 0.1.0 + */ + public function avatar_url($size = "64"){ + global $users; + + // Return Profile Picture + if(sn_config("frontend_avatar_users") && strpos($this->getValue("author"), "bludit") === 0){ + $username = substr($this->getValue("author"), strlen("bludit::")); + if($users->exists($username)){ + $user = new User($username); + if(($avatar = $user->profilePicture()) !== false){ + return $avatar; + } + } + } + + // Return Gravatar + if(sn_config("frontend_avatar") === "gravatar"){ + $hash = md5(strtolower(trim($this->email()))); + return "https://www.gravatar.com/avatar/{$hash}?s={$size}&d=" . sn_config("frontend_gravatar"); + } + + // Return Identicon + if(sn_config("frontend_avatar") === "identicon"){ + $hash = md5(strtolower(trim($this->email()))); + $ident = new Identicon\Identicon(); + return $ident->getImageDataUri($hash, $size); + } + + // Return Mystery Man + return SNICKER_DOMAIN . "includes/img/default-avatar.jpg"; + } + + /* + | FIELD :: GET / FORMAT DATE + | @since 0.1.0 + | + | @param string The respective format, which should be used for the output. + | + | @return string The formatted Date Output. + */ + public function date($format = false, $type = "date"){ + global $site; + $date = $this->getValue("{$type}Raw"); + return Date::format($date, DB_DATE_FORMAT, ($format? $format: $site->dateFormat())); + } + public function dateModified($format = false){ + return $this->date($format, "dateModified"); + } + public function dateAudit($format = false){ + return $this->date($format, "dateAudit"); + } + + /* + | FIELD :: GET CUSTOM + | @since 0.1.0 + | + | @param string The respective custom key, to get the value. + | Use `null` to get all custom values. + | + | @return multi The custom value, all customs as ARRAY or FALSE on failure. + */ + public function custom($key = NULL){ + $custom = $this->getValue("custom"); + if($key !== null){ + if(array_key_exists($key, $custom)){ + return $custom[$key]; + } + return false; + } + return $custom; + } + } diff --git a/system/class.comments-index.php b/system/class.comments-index.php new file mode 100644 index 0000000..f3f60fc --- /dev/null +++ b/system/class.comments-index.php @@ -0,0 +1,476 @@ + + | @version 0.1.2 [0.1.0] - Alpha + | + | @website https://github.com/pytesNET/snicker + | @license X11 / MIT License + | @copyright Copyright © 2019 SamBrishes, pytesNET + */ + if(!defined("BLUDIT")){ die("Go directly to Jail. Do not pass Go. Do not collect 200 Cookies!"); } + + class CommentsIndex extends dbJSON{ + /* + | DATABASE FIELDS + */ + protected $dbFields = array( + "title" => "", // Comment Title + "excerpt" => "", // Comment Excerpt (142) + "status" => "", // Comment Status + "page_uuid" => "", // Comment Page UUID + "parent_uid" => "", // Comment Parent UID + "author" => "", // Comment Author (bludt::username or guest::uuid) + "date" => "" // Comment Date + ); + + /* + | CONSTRUCTOR + | @since 0.1.0 + */ + public function __construct(){ + parent::__construct(DB_SNICKER_INDEX); + if(!file_exists(DB_SNICKER_INDEX)){ + $this->db = array(); + $this->save(); + } + } + + /* + | OVERWRITE :: EXISTS + | @since 0.1.0 + */ + public function exists($uid){ + return array_key_exists($uid, $this->db); + } + + /* + | HELPER :: LIMIT LIST + | @since 0.1.0 + */ + private function limitList($list, $page, $limit){ + if($limit == -1){ + return $list; + } + $offset = $limit * (max($page, 1) - 1); + $count = min(($offset + $limit - 1), count($list)); + if($offset < 0 || $offset > $count){ + return false; + } + return array_slice($list, $offset, $limit, true); + } + + /* + | DATA :: GET PENDING INDEX + | @since 0.1.0 + | + | @param bool TRUE to just return the keys, FALSE to return the complete array. + | + | @return array All pending comments with basic comment data as ARRAY. + */ + public function getPending($keys = false){ + $db = array(); + foreach($this->db AS $key => $value){ + if(!isset($value["status"]) || empty($value["status"])){ + continue; + } + if($value["status"] === "pending"){ + $db[$key] = $value; + } + } + if($keys){ + return array_keys($db); + } + return $db; + } + + /* + | DATA :: GET APPROVED INDEX + | @since 0.1.0 + | + | @param bool TRUE to just return the keys, FALSE to return the complete array. + | + | @return array All approved comments with basic comment data as ARRAY. + */ + public function getApproved($keys = false){ + $db = array(); + foreach($this->db AS $key => $value){ + if(!isset($value["status"]) || empty($value["status"])){ + continue; + } + if($value["status"] === "approved"){ + $db[$key] = $value; + } + } + if($keys){ + return array_keys($db); + } + return $db; + } + + /* + | DATA :: GET REJECTED INDEX + | @since 0.1.0 + | + | @param bool TRUE to just return the keys, FALSE to return the complete array. + | + | @return array All rejected comments with basic comment data as ARRAY. + */ + public function getRejected($keys = false){ + $db = array(); + foreach($this->db AS $key => $value){ + if(!isset($value["status"]) || empty($value["status"])){ + continue; + } + if($value["status"] === "rejected"){ + $db[$key] = $value; + } + } + if($keys){ + return array_keys($db); + } + return $db; + } + + /* + | DATA :: GET SPAM INDEX + | @since 0.1.0 + | + | @param bool TRUE to just return the keys, FALSE to return the complete array. + | + | @return array All spam comments with basic comment data as ARRAY. + */ + public function getSpam($keys = false){ + $db = array(); + foreach($this->db AS $key => $value){ + if(!isset($value["status"]) || empty($value["status"])){ + continue; + } + if($value["status"] === "spam"){ + $db[$key] = $value; + } + } + if($keys){ + return array_keys($db); + } + return $db; + } + + /* + | DATA :: COUNT COMMENTS + | @since 0.1.0 + | + | @param multi A single comment status as STRING, multiple as ARRAY. + | Use `null` to count all comments. + | + | @return int The number of comments of the respective index. + */ + public function count($status = array("approved")){ + if($status === null){ + return count($this->db); + } + if(!is_array($status)){ + $status = array($status); + } + + $count = 0; + foreach($this->db AS $key => $value){ + if(!isset($value["status"]) || empty($value["status"])){ + continue; + } + if(in_array($value["status"], $status)){ + $count++; + } + } + return $count; + } + + /* + | DATA :: GET COMMENT + | @since 0.1.0 + | + | @param string The desired comment UID. + | + | @return multi The comment index array on success, FALSE on failure + */ + public function getComment($uid){ + return array_key_exists($uid, $this->db)? $this->db[$uid]: false; + } + + /* + | DATA :: LIST COMMENTS + | @since 0.1.0 + | + | @param multi A single comment status as STRING, multiple as ARRAY. + | @param int The current comment page number, starting with 1. + | @param int The number of comments to be shown per page. + | + | @return array The respective unique comment IDs as ARRAY, FALSE on failure. + */ + public function getList($status = array("approved"), $page = 1, $limit = -1){ + if($status === null){ + return count($this->db); + } + if(!is_array($status)){ + $status = array($status); + } + + // Get List + $list = array(); + foreach($this->db AS $key => $value){ + if(!isset($value["status"]) || empty($value["status"])){ + continue; + } + if(in_array($value["status"], $status)){ + $list[] = $key; + } + } + return $this->limitList($list, $page, $limit); + } + + /* + | DATA :: LIST COMMENTS BY UUID + | @since 0.1.0 + | + | @param multi A single page UUID as STRING, multiple as ARRAY. + | @param int The current comment page number, starting with 1. + | @param int The number of comments to be shown per page. + | + | @return array The respective unique comment IDs as ARRAY, FALSE on failure. + */ + public function getListByUUID($uuid, $page = 1, $limit = -1){ + if(!is_array($uuid)){ + $uuid = array($uuid); + } + + // Get List + $list = array(); + foreach($this->db AS $key => $value){ + if(!isset($value["page_uuid"]) || empty($value["page_uuid"])){ + continue; + } + if(in_array($value["page_uuid"], $uuid)){ + $list[] = $key; + } + } + return $this->limitList($list, $page, $limit); + } + + /* + | DATA :: LIST COMMENTS BY PARENT + | @since 0.1.0 + | + | @param multi A single comment UID as STRING. + | @param int The current comment page number, starting with 1. + | @param int The number of comments to be shown per page. + | + | @return array The respective unique comment IDs as ARRAY, FALSE on failure. + */ + public function getListByParent($uid, $page = 1, $limit = -1){ + if(!is_string($uid) || !$this->exists($uid)){ + return array(); + } + + // Get List + $list = array($uid); + foreach($this->db AS $key => $value){ + if(!isset($value["parent_uid"]) || empty($value["parent_uid"])){ + continue; + } + if($value["parent_uid"] === $uid){ + $list[] = $key; + } + } + return $this->limitList($list, $page, $limit); + } + + /* + | DATA :: LIST COMMENTS BY USER + | @since 0.1.0 + | + | @param string A single username, unique user id or eMail address. + | @param int The current comment page number, starting with 1. + | @param int The number of comments to be shown per page. + | + | @return array The respective unique comment IDs as ARRAY, FALSE on failure. + */ + public function getListByUser($string, $page = 1, $limit = -1){ + global $users, $SnickerUsers; + + // Get Member / Guest + $guest = false; + $member = false; + if(Valid::email($string)){ + if(($user = $users->getByEmail($string)) !== false){ + $member = "bludit::{$user}"; + } + } else { + if($users->exists($string)){ + $member = "bludit::{$string}"; + } + } + if(($user = $SnickerUsers->get($string)) !== false){ + $guest = "guest::{$user["uuid"]}"; + } + if(!$member && !$guest){ + return array(); + } + + // Get List + $list = array(); + foreach($this->db AS $key => $value){ + if(!isset($value["author"]) || empty($value["author"])){ + continue; + } + if($value["author"] == $member || $value["author"] == $guest){ + $list[] = $key; + } + } + return $this->limitList($list, $page, $limit); + } + + /* + | DATA :: SEARCH COMMENTS BY TITLE & EXCERPT + | @since 0.1.0 + | + | @param string The string to be searched. + | @param int The current comment page number, starting with 1. + | @param int The number of comments to be shown per page. + | + | @return array The respective unique comment IDs as ARRAY, FALSE on failure. + */ + public function searchComments($search, $page = 1, $limit = -1){ + $list = array(); + foreach($this->db AS $key => $value){ + if(isset($value["title"]) && stripos($value["title"], $search) !== false){ + $list[] = $key; + } else if(isset($value["excerpt"]) && stripos($value["excerpt"], $search) !== false){ + $list[] = $key; + } + } + return $this->limitList($list, $page, $limit); + } + + + /* + | HANDLE :: ADD COMMENT + | @since 0.1.0 + | + | @param string The unique comment ID. + | @param array The comment array. + | + | @return bool TRUE if everything is fluffy, FALSE if not. + */ + public function add($uid, $comment){ + $row = array(); + foreach($this->dbFields AS $field => $value){ + if(isset($comment[$field])){ + $final = is_string($comment[$field])? Sanitize::html($comment[$field]): $comment[$field]; + } else { + $final = $value; + } + settype($final, gettype($value)); + $row[$field] = $final; + } + + // Format Excerpt + $row["excerpt"] = strip_tags($comment["comment"]); + if(strlen($row["excerpt"]) > 142){ + $row["excerpt"] = substr($row["excerpt"], 0, 139) . "..."; + } + + // Insert and Return + $this->db[$uid] = $row; + $this->sortBy(); + if($this->save() !== true){ + Log::set(__METHOD__, "error-update-db"); + return false; + } + return true; + } + + /* + | HANDLE :: UPDATE COMMENT + | @since 0.1.0 + | + | @param string The unique comment ID. + | @param array The comment array. + | + | @return bool TRUE if everything is fluffy, FALSE if not. + */ + public function edit($uid, $comment){ + if(!$this->exists($uid)){ + $this->log(__METHOD__, "error-comment-uid", array($uid)); + return false; + } + $data = $this->db[$uid]; + + // Loop Fields + $row = array(); + foreach($this->dbFields AS $field => $value){ + if(isset($comment[$field])){ + $final = is_string($comment[$field])? Sanitize::html($comment[$field]): $comments[$field]; + } else { + $final = $data[$field]; + } + settype($final, gettype($value)); + $row[$field] = $final; + } + + // Format Excerpt + $row["excerpt"] = strip_tags($comment["comment"]); + if(strlen($row["excerpt"]) > 142){ + $row["excerpt"] = substr($row["excerpt"], 0, 139) . "..."; + } + + // Update and Return + $this->db[$uid] = $row; + if($this->save() !== true){ + Log::set(__METHOD__, "error-update-db"); + return false; + } + return true; + } + + /* + | HANDLE :: DELETE COMMENT + | @since 0.1.0 + | + | @param string The unique comment ID. + | + | @return bool TRUE if everything is fluffy, FALSE if not. + */ + public function delete($uid){ + if(!$this->exists($uid)){ + return false; + } + unset($this->db[$uid]); + if($this->save() !== true){ + Log::set(__METHOD__, "error-update-db"); + return false; + } + return true; + } + + /* + | INTERNAL :: SORT COMMENTS + | @since 0.1.0 + | + | @return bool TRUE + */ + public function sortBy(){ + global $SnickerPlugin; + + if($SnickerPlugin->getValue("frontend_order") === "date_asc"){ + uasort($this->db, function($a, $b){ + return $a["date"] > $b["date"]; + }); + } else if($SnickerPlugin->getValue("frontend_order") === "date_desc"){ + uasort($this->db, function($a, $b){ + return $a["date"] < $b["date"]; + }); + } + return true; + } + } diff --git a/system/class.comments-users.php b/system/class.comments-users.php new file mode 100644 index 0000000..a67f374 --- /dev/null +++ b/system/class.comments-users.php @@ -0,0 +1,392 @@ + + | @version 0.1.2 [0.1.0] - Alpha + | + | @website https://github.com/pytesNET/snicker + | @license X11 / MIT License + | @copyright Copyright © 2019 SamBrishes, pytesNET + */ + if(!defined("BLUDIT")){ die("Go directly to Jail. Do not pass Go. Do not collect 200 Cookies!"); } + + class CommentsUsers extends dbJSON{ + /* + | DATABASE FIELDS + */ + protected $dbFields = array( + "username" => "", // Username + "email" => "", // User eMail Address + "hash" => "", // Hashed IP + User Agent + "blocked" => false, // Blocked? + "comments" => array() // Page UIDs => array(CommentUIDs) + ); + + /* + | CONSTRUCTOR + | @since 0.1.0 + */ + public function __construct(){ + parent::__construct(DB_SNICKER_USERS); + if(!file_exists(DB_SNICKER_USERS)){ + $this->db = array(); + $this->save(); + } + } + + /* + | GET COMMENTS BY UNIQUE USER ID + | @since 0.1.0 + | + | @param string The unique user ID as string (or the user eMail address). + | @param bool TRUE to just return the keys, FALSE to return it as Comment objects. + | + | @return multi The comment keys / objects as ARRAY, FALSE on failure. + */ + public function getComments($uuid, $keys = true){ + global $Snicker; + + // Validate Data + if(Valid::email($uuid) !== false){ + $uuid = md5(strtolower(Sanitize::email($uuid))); + } + if(!array_key_exists($uuid, $this->db)){ + return false; + } + + // Return Keys + $data = $this->db[$uuid]["comments"]; + if($keys === true){ + return $data; + } + + // Return Objects + foreach($data AS &$key){ + $key = $Snicker->getComment($key); + } + return $key; + } + + /* + | EXISTS + | @since 0.1.0 + */ + public function exists($uid){ + return isset($this->db[$uid]); + } + + /* + | GET USER BY UUID + | @since 0.1.0 + | + | @param string The unique user ID as string (or the user eMail address). + | + | @return multi The user database array on success, FALSE on failure. + */ + public function get($uuid){ + if(Valid::email($uuid) !== false){ + $uuid = md5(strtolower(Sanitize::email($uuid))); + } + if(!array_key_exists($uuid, $this->db)){ + return false; + } + $data = $this->db[$uuid]; + $data["uuid"] = $uuid; + return $data; + } + + /* + | GET CURRENT USER ID + | @since 0.1.0 + | + | @return multi The user UUID on success, FALSE on failure. + */ + public function getCurrent(){ + global $security; + $hash = md5($security->getUserIp() . $_SERVER["HTTP_USER_AGENT"]); + foreach($this->db AS $uuid => $fields){ + if($fields["hash"] === $hash){ + return $uuid; + } + } + return false; + } + + /* + | GET USER + | @since 0.1.0 + | + | @param string Get the user by Comment Author STRING. + | + | @return multi The user data array on success, FALSE on failure. + */ + public function getByString($string){ + global $users; + + // Check User Instance + if(strpos($string, "bludit::") === 0){ + $username = substr($string, strlen("bludit::")); + if($users->exists($username)){ + $user = $users->getUserDB($username); + $user["username"] = $user["nickname"]; + return $user; + } + return false; + } + + // Check Guest Instance + if(strpos($string, "guest::") === 0){ + $uuid = substr($string, strlen("guest::")); + if($this->exists($uuid)){ + return $this->db[$uuid]; + } + return false; + } + + // Return as Anonymous + return array( + "username" => "Anonymous", + "email" => "anonymous@" . $_SERVER["SERVER_NAME"] + ); + } + + /* + | GET LIST + | @since 0.1.0 + | + | @param string The string to be searched or NULL. + | @param int The current comment page number, starting with 1. + | @param int The number of comments to be shown per page. + | + | @return array The respective user keys with an ARRAY or FALSE on failure. + */ + public function getList($search = null, $page = 1, $limit = -1){ + if($search !== null){ + $list = array(); + foreach($this->db AS $uuid => $fields){ + if(stripos($fields["username"], $search) === false){ + continue; + } + if(stripos($fields["email"], $search) === false){ + continue; + } + $list[$uuid] = $fields; + } + } else { + $list = $this->db; + } + + // Limit + if($limit == -1){ + return $list; + } + + // Offset + $offset = $limit * ($page - 1); + $count = min(($offset + $limit - 1), count($list)); + if($offset < 0 || $offset > $count){ + return false; + } + return array_slice($list, $offset, $limit, true); + } + + /* + | MAIN USER HANDLER + | @since 0.1.0 + | + | @param string The username as STRING. + | @param string The email address as STRING. + | + | @return multi The (new) UUID on success, FALSE on failure. + */ + public function user($username, $email){ + global $security; + + // Validate Username + $username = Sanitize::html(strip_tags(trim($username))); + if(empty($username) || strlen($username) > 42){ + return false; + } + + // Validate eMail Address + $email = strtolower(Sanitize::email($email)); + if(empty($email) || Valid::email($email) === false){ + return false; + } + + // Check User + $uuid = md5($email); + if(array_key_exists($uuid, $this->db)){ + return $uuid; + } + + // Add User + $this->db[$uuid] = array( + "username" => $username, + "email" => $email, + "hash" => md5($security->getUserIp() . $_SERVER["HTTP_USER_AGENT"]), + "blocked" => false, + "comments" => array() + ); + if(!$this->save()){ + return false; + } + return $uuid; + } + public function add($username, $email, $meta = array()){ + return $this->user($username, $email, $meta); + } + + /* + | EDIT USER DATA + | @since 0.1.0 + | + | @param string The unique user ID as string (or the user eMail address). + | @param multi The new username (or NULL to keep the existing one). + | @param multi The new eMail address (or NULL to keep the existing one). + | ATTENTION: The new eMail address CANNOT be used already! + | ATTENTION: The new eMail address CHANGES the unique user id (UUID)! + | @param multi TRUE to block the user, FALSE to unblock, null to keep the current. + | + | @return multi The (new) UUID on success, FALSE on failure. + */ + public function edit($uuid, $username = null, $email = null, $blocked = null){ + if(Valid::email($uuid) !== false){ + $uuid = md5(strtolower(Sanitize::email($uuid))); + } + if(!array_key_exists($uuid, $this->db)){ + return false; + } + $data = $this->db[$uuid]; + + // Change Username + if($username !== null){ + $username = Sanitize::html(strip_tags(trim($username))); + if(empty($username) || strlen($username) > 42){ + return false; + } + $data["username"] = $username; + } + + // Change eMail + if($email !== null){ + $email = strtolower(Sanitize::email($uuid)); + if(Valid::email($email) === false){ + return false; + } + $data["email"] = $email; + $newuuid = md5($email); + } + + // Change Blocked + if(is_bool($blocked)){ + $data["blocked"] = $blocked; + } + + // Update UUID + if(isset($newuuid) && $uuid !== $newuuid){ + unset($this->db[$uuid]); + $uuid = $newuuid; + } + + // Store new Data + $this->db[$uuid] = $data; + if(!$this->save()){ + return false; + } + return $uuid; + } + + /* + | ADD COMMENT ID TO USER + | @since 0.1.0 + | + | @param string The unique user ID as string (or the user eMail address). + | @param string The unique comment ID as STRING. + | + | @return bool TRUE on success, FALSE on failure. + */ + public function addComment($uuid, $uid){ + if(Valid::email($uuid) !== false){ + $uuid = md5(strtolower(Sanitize::email($uuid))); + } + if(!array_key_exists($uuid, $this->db)){ + return false; + } + + // Add Comment UID + $user = $this->db[$uuid]; + if(!isset($user["comments"]) || !is_array($user["comments"])){ + $user["comments"] = array(); + } + if(!in_array($uid, $user["comments"])){ + $user["comments"][] = $uid; + } + + // Save & Return + $this->db[$uuid] = $user; + if(!$this->save()){ + return false; + } + return true; + } + + /* + | DELETE COMMENT ID TO USER + | @since 0.1.0 + | + | @param string The unique user ID as string (or the user eMail address). + | @param string The unique comment ID as STRING. + | + | @return bool TRUE on success, FALSE on failure. + */ + public function deleteComment($uuid, $uid){ + if(Valid::email($uuid) !== false){ + $uuid = md5(strtolower(Sanitize::email($uuid))); + } + if(!array_key_exists($uuid, $this->db)){ + return false; + } + + // Delete Comment UID + $user = $this->db[$uuid]; + if(!isset($user["comments"])){ + $user["comments"] = array(); + } + if(in_array($uid, $user["comments"])){ + unset($user["comments"][array_search($uid, $user["comments"])]); + } + + // Save & Return + $this->db[$uuid] = $user; + if(!$this->save()){ + return false; + } + return true; + } + + /* + | DELETE USER + | @since 0.1.0 + | + | @param string The unique user ID as string (or the user eMail address). + | + | @return bool TRUE on success, FALSE on failure. + */ + public function delete($uuid){ + if(Valid::email($uuid) !== false){ + $uuid = md5(strtolower(Sanitize::email($uuid))); + } + if(!array_key_exists($uuid, $this->db)){ + return false; + } + + // Delete & Return + unset($this->db[$uuid]); + if(!$this->save()){ + return false; + } + return true; + } + } diff --git a/system/class.comments-votes.php b/system/class.comments-votes.php new file mode 100644 index 0000000..52286b8 --- /dev/null +++ b/system/class.comments-votes.php @@ -0,0 +1,226 @@ + + | @version 0.1.2 [0.1.0] - Alpha + | + | @website https://github.com/pytesNET/snicker + | @license X11 / MIT License + | @copyright Copyright © 2019 SamBrishes, pytesNET + */ + if(!defined("BLUDIT")){ die("Go directly to Jail. Do not pass Go. Do not collect 200 Cookies!"); } + + class CommentsVotes extends dbJSON{ + const KEY = "snicker-ratings"; + + /* + | DATABASE FIELDS + */ + protected $dbFields = array( ); + + /* + | CONSTRUCTOR + | @since 0.1.0 + */ + public function __construct(){ + parent::__construct(DB_SNICKER_VOTES); + if(!file_exists(DB_SNICKER_VOTES)){ + $this->db = array(); + $this->save(); + } + } + + /* + | HANDLE :: CURRENT USER + | @since 0.1.0 + */ + public function currentUser(){ + global $login, $security; + + if(!is_a($login, "Login")){ + $login = new Login(); + } + + // Get Current User + if($login->isLogged()){ + return "bludit::" . $login->username(); + } + return "guest::" . md5($security->getUserIp() . $_SERVER["HTTP_USER_AGENT"]); + } + + /* + | HANDLE :: HAS VOTED + | @since 0.1.0 + */ + public function hasVoted($uid, $vote = null){ + $user = $this->currentUser(); + $config = sn_config("comment_vote_storage"); + + // Database Storage + $db = strpos($user, "bludit::") === 0 || $config === "database"; + if($db){ + if(!array_key_exists($user, $this->db)){ + return false; + } + $data = $this->db[$user]; + } else { + $store = ($config == "cookie")? "Cookie": "Session"; + $data = $store::get(self::KEY); + $data = !empty($data)? @unserialize($data): false; + if(!is_array($data)){ + return false; + } + } + + // Check Data + if(!array_key_exists($uid, $data)){ + return false; + } + return ($vote === null || $data[$uid] === $vote); + } + public function hasLiked($uid){ + return $this->hasVoted($uid, "like"); + } + public function hasDisliked($uid){ + return $this->hasVoted($uid, "dislike"); + } + + /* + | HANDLE :: ADD NEW COMMENT VOTING + | @since 0.1.0 + */ + public function add($uid, $vote = "like"){ + $user = $this->currentUser(); + $config = sn_config("comment_vote_storage"); + + // Database Storage + $db = strpos($user, "bludit::") === 0 || $config === "database"; + if($db){ + if(!array_key_exists($user, $this->db)){ + $this->db[$user] = array(); + } + if(array_key_exists($uid, $this->db[$user])){ + return false; + } + $this->db[$user][$uid] = $vote; + return $this->save() !== false; + } + + // Cookie | Session Storage + $store = ($config == "cookie")? "Cookie": "Session"; + $data = $store::get(self::KEY); + $data = !empty($data)? @unserialize($data): false; + if(is_array($data)){ + if(array_key_exists($uid, $data)){ + return false; + } + } else { + $data = array(); + } + $data[$uid] = $vote; + $store::set(self::KEY, serialize($data)); + return true; + } + + /* + | HANDLE :: EDIT COMMENT VOTING + | @since 0.1.0 + */ + public function edit($uid, $vote = "like"){ + $user = $this->currentUser(); + $config = sn_config("comment_vote_storage"); + + // Database Storage + $db = strpos($user, "bludit::") === 0 || $config === "database"; + if($db){ + if(!array_key_exists($user, $this->db)){ + $this->db[$user] = array(); + } + if(array_key_exists($uid, $this->db[$user]) && $this->db[$user][$uid] === $vote){ + return false; + } + $this->db[$user][$uid] = $vote; + return $this->save() !== false; + } + + // Cookie | Session Storage + $store = ($config == "cookie")? "Cookie": "Session"; + $data = $store::get(self::KEY); + $data = !empty($data)? @unserialize($data): false; + if(is_array($data)){ + if(array_key_exists($uid, $data) && $data[$uid] === $vote){ + return false; + } + } else { + $data = array(); + } + $data[$uid] = $vote; + $store::set(self::KEY, serialize($data)); + return true; + } + + /* + | HANDLE :: DELETE COMMENT VOTING + | @since 0.1.0 + */ + public function delete($uid){ + $user = $this->currentUser(); + $config = sn_config("comment_vote_storage"); + + // Database Storage + $db = strpos($user, "bludit::") === 0 || $config === "database"; + if($db){ + if(!array_key_exists($user, $this->db)){ + return true; + } + if(!array_key_exists($uid, $this->db[$user])){ + return true; + } + unset($this->db[$user][$uid]); + return $this->save() !== false; + } + + // Cookie | Session Storage + $store = ($config == "cookie")? "Cookie": "Session"; + $data = $store::get(self::KEY); + $data = !empty($data)? @unserialize($data): false; + if(!is_array($data)){ + return true; + } + if(!array_key_exists($uid, $data)){ + return true; + } + unset($data[$uid]); + $store::set(self::KEY, serialize($data)); + return true; + } + + /* + | HANDLE :: DELETE BY USER + | @since 0.1.0 + */ + public function deleteByUser($user){ + $config = sn_config("comment_vote_storage"); + + // Database Storage + $db = strpos($user, "bludit::") === 0 || $config === "database"; + if($db){ + if(!array_key_exists($user, $this->db)){ + return true; + } + unset($this->db[$user]); + return $this->save() !== false; + } + + // Cookie | Session Storage + $store = ($config == "cookie")? "Cookie": "Session"; + $data = $store::get(self::KEY); + $data = !empty($data)? @unserialize($data): false; + if(!is_array($data)){ + return true; + } + $store::set(self::KEY, serialize(array())); + return true; + } + } diff --git a/system/class.comments.php b/system/class.comments.php new file mode 100644 index 0000000..502e498 --- /dev/null +++ b/system/class.comments.php @@ -0,0 +1,564 @@ + + | @version 0.1.2 [0.1.0] - Alpha + | + | @website https://github.com/pytesNET/snicker + | @license X11 / MIT License + | @copyright Copyright © 2019 SamBrishes, pytesNET + */ + if(!defined("BLUDIT")){ die("Go directly to Jail. Do not pass Go. Do not collect 200 Cookies!"); } + + class Comments extends dbJSON{ + /* + | PAGE UUID + */ + protected $uuid; + + /* + | DATABASE FIELDS + */ + protected $dbFields = array( + "type" => "comment", // Comment Type ("comment", "reply", "pingback") + "depth" => 1, // Comment Depth (starting with 1) + "title" => "", // Comment Title + "status" => "", // Comment Status ("pending", "approved", "rejected", "spam") + "comment" => "", // Comment Content + "rating" => [0, 0], // Comment Rating + "page_uuid" => "", // Comment Page UUID + "parent_uid" => "", // Comment Parent UID + + "author" => "", // Comment Author (bludt::username or guest::uuid) + "subscribe" => false, // eMail Subscription + + "date" => "", // Date Comment Written + "dateModified" => "", // Date Comment Modified + "dateAudit" => "", // Date Comment Audit + "custom" => array(), // Custom Data + ); + + /* + | CONSTRUCTOR + | @since 0.1.0 + | + | @param string The UUID of the respective page. + */ + public function __construct($uuid){ + global $pages; + + // Get Page + if($pages->getByUUID($uuid) === false){ + $error = "The Page UUID couldn't be found in the database [{$uuid}]"; + Log::set(__METHOD__ . LOG_SEP . $error); + throw new Exception($error); + } + $this->uuid = $uuid; + parent::__construct(DB_SNICKER_COMMENTS . "comments-{$uuid}.php"); + } + + /* + | HELPER :: FILL LOG FILE + | @since 0.1.0 + | + | @param string The respective method for the log (Use __METHOD__) + | @param string The respective error message to be logged. + | @param array Additional values AS array for the `vsprintf` function. + */ + private function log($method, $string, $args){ + $strings = array( + "error-comment-uid" => "The comment UID is invalid or does not exist [%s]", + "error-page-uuid" => "The page uuid is invalid or does not exist [%s]", + "error-create-dir" => "The comment directory could not be created [%s]", + "error-create-file" => "The comment file could not be created [%s]", + "error-comment-file" => "The comment file does not exist [%s]", + "error-comment-update" => "The comment file could not be updated [%s]", + "error-comment-remove" => "The comment file could not be deleted [%s]", + "error-update-db" => "The comment database could not be updated" + ); + if(array_key_exists($string, $strings)){ + $string = $strings[$string]; + } + Log::set($method . LOG_SEP . vsprintf("Error occured: {$string}", $args), LOG_TYPE_ERROR); + } + + /* + | HELPER :: GENERATE UNIQUE COMMENT ID + | @since 0.1.0 + */ + private function generateUID(){ + if(function_exists("random_bytes")){ + return md5(bin2hex(random_bytes(16)) . time()); + } else if(function_exists("openssl_random_pseudo_bytes")){ + return md5(bin2hex(openssl_random_pseudo_bytes(16)) . time()); + } + return md5(uniqid() . time()); + } + + + /* + | PUBLIC :: GET DEFAULT FIELDS + | @since 0.1.0 + | + | @return array An array with all default fields and values per entry. + */ + public function getDefaultFields(){ + return $this->dbFields; + } + + + /* + | DATA :: GET DATABASE + | @since 0.1.0 + | + | @param bool TRUE to just return the keys, FALSE to return the complete DB. + | + | @return array The complete database entries (or keys) within an ARRAY. + */ + public function getDB($keys = true){ + return ($keys)? array_keys($this->db): $this->db; + } + + /* + | DATA :: CHECK IF COMMENT ITEM EXISTS + | @since 0.1.0 + | + | @param string The unique comment ID. + | + | @return bool TRUE if the comment ID exists, FALSE if not. + */ + public function exists($uid){ + return isset($this->db[$uid]); + } + + /* + | DATA :: GET COMMENT ITEM + | @since 0.1.0 + | + | @param string The unique comment ID. + | + | @return array The comment data array on success, FALSE on failure. + */ + public function getCommentDB($uid){ + return ($this->exists($uid))? $this->db[$uid]: false; + } + + /* + | DATA :: LIST COMMENTS + | @since 0.1.0 + | + | @param int The current comment page number, starting with 1. + | @param int The number of comments to be shown per page. + | @param multi The desired comment type as STRING, multiple as ARRAY. + | Pass `null` to get each comment type. + | @param multi The desired comment status as STRING, multiple as ARRAY. + | Pass `null` to get each comment status. + | + | @return array The respective database keys with an ARRAY or FALSE on failure. + */ + public function getList($page, $limit, $type = array("comment", "reply"), $status = array("approved")){ + $type = is_string($type)? array($type): $type; + if(!is_array($type)){ + $type = null; + } + + $status = is_string($status)? array($status): $status; + if(!is_array($status)){ + $type = null; + } + + // Format List + $list = array(); + foreach($this->db AS $key => $fields){ + if($type !== null && !in_array($fields["type"], $type)){ + continue; + } + if($status !== null && !in_array($fields["status"], $status)){ + continue; + } + array_push($list, $key); + } + + // Limit + if($limit == -1){ + return $list; + } + + // Offset + $offset = $limit * ($page - 1); + $count = min(($offset + $limit - 1), count($list)); + if($offset < 0 || $offset > $count){ + return false; + } + return array_slice($list, $offset, $limit, true); + } + + /* + | DATA :: GENERATE A DEPTH LIST + | @since 0.1.0 + | + | @param int The current comment page number, starting with 1. + | @param int The number of comments to be shown per page. + | @param multi The desired comment type as STRING, multiple as ARRAY. + | Pass `null` to get each comment type. + | @param multi The desired comment status as STRING, multiple as ARRAY. + | Pass `null` to get each comment status. + | + | @return array The respective database keys with an ARRAY or FALSE on failure. + */ + public function getDepthList($page, $limit, $type = array("comment", "reply"), $status = array("approved")){ + global $login, $SnickerUsers; + $this->sortBy(); + + // Validate Parameters + $type = is_string($type)? array($type): $type; + if(!is_array($type)){ + $type = null; + } + $status = is_string($status)? array($status): $status; + if(!is_array($status)){ + $type = null; + } + + // Get User Pending + if(in_array("pending", $status)){ + if(!is_a($login, "Login")){ + $login = new Login(); + } + if($login->isLogged()){ + $user = "bludit::" . $login->username(); + } else { + if(($user = $SnickerUsers->getCurrent()) !== false){ + $user = "guest::" . $user; + } + } + } + + // Format List + $list = array(); + $children = array(); + foreach($this->db AS $key => $fields){ + if($type !== null && !in_array($fields["type"], $type)){ + continue; + } + if($status !== null && !in_array($fields["status"], $status)){ + continue; + } + if($fields["status"] === "pending" && $fields["author"] !== $user){ + continue; + } + + if(!empty($fields["parent_uid"])){ + if(!array_key_exists($fields["parent_uid"], $children)){ + $children[$fields["parent_uid"]] = array(); + } + array_push($children[$fields["parent_uid"]], $key); + } else { + array_push($list, $key); + } + } + + // Offset + $count = 0; + $offset = $limit * ($page - 1); + for(; $count < $offset ;){ + $key = array_shift($list); + + $count++; + if(array_key_exists($key, $children)){ + $count += count($children[$key]); + unset($children[$key]); + } + } + + // Generator + $count = 0; + foreach($list AS $key){ + if($count >= $limit){ + break; + } + + $count++; + yield $key; + if(!array_key_exists($key, $children)){ + continue; + } + + $loop = $key; + $depth = array(); + while(true){ + if(empty($depth) && empty($children[$key])){ + break; + } + if(array_key_exists($loop, $children) && count($children[$loop]) > 0){ + array_push($depth, $loop); + $loop = array_shift($children[$loop]); + $count++; + yield $loop; + } else { + $loop = array_pop($depth); + continue; + } + } + } + } + + /* + | DATA :: COUNT COMMENTS + | @since 0.1.0 + | + | @param multi The desired comment type as STRING, multiple as ARRAY. + | Pass `null` to get each comment type. + | + | @return int The total number of comments. + */ + public function count($type = array("comment", "reply")){ + $type = is_string($type)? array($type): $type; + if(!is_array($type)){ + $type = null; + } + + // Count All + if($type === null){ + return count($this->db); + } + + // Count + $count = 0; + foreach($this->db AS $key => $fields){ + if(!in_array($fields["type"], $type)){ + continue; + } + $count++; + } + return $count; + } + + + /* + | HANDLE :: ADD A NEW COMMENT + | @since 0.1.0 + | + | @param array The respective comment array. + | + | @return multi The comment UID on success, FALSE on failure. + */ + public function add($args){ + global $SnickerIndex, $SnickerUsers; + + // Loop Default Fields + $row = array(); + foreach($this->dbFields AS $field => $value){ + if(isset($args[$field])){ + $final = $args[$field]; + } else { + $final = $value; + } + settype($final, gettype($value)); + $row[$field] = $final; + } + + // Create (U)UIDs + $uid = $this->generateUID(); + $row["page_uuid"] = $this->uuid; + + // Validate Parent UID + if(!empty($row["parent_uid"]) && !$this->exists($row["parent_uid"])){ + $row["parent_uid"] = null; + } + + // Validate Type and Depth + if(!empty($row["parent_uid"])){ + $row["type"] = "reply"; + $row["depth"] = $this->db[$row["parent_uid"]]["depth"] + 1; + } else { + $row["type"] = "comment"; + $row["depth"] = 1; + } + + // Validata Status + if(!in_array($row["status"], array("pending", "approved", "rejected", "spam"))){ + $row["status"] = "pending"; + } + + // Sanitize Strings + $row["title"] = Sanitize::html(strip_tags($row["title"])); + $row["author"] = Sanitize::html($row["author"]); + + // Sanitize Comment + $allowed = "




"; + $allowed .= "