aboutsummaryrefslogtreecommitdiff
path: root/bootstrap
diff options
context:
space:
mode:
authorThedro Neely <thedroneely@gmail.com>2018-08-30 04:40:53 -0400
committerThedro Neely <thedroneely@gmail.com>2018-08-30 04:40:53 -0400
commit2659205908bd5cab508f4ff817123673e078ab74 (patch)
treee455f36b124e9b98f7005e6d4310b71881734623 /bootstrap
downloadedwinmattiacci.com-2659205908bd5cab508f4ff817123673e078ab74.tar.gz
edwinmattiacci.com-2659205908bd5cab508f4ff817123673e078ab74.tar.bz2
edwinmattiacci.com-2659205908bd5cab508f4ff817123673e078ab74.zip
Initialize Repo: First Commit
Diffstat (limited to 'bootstrap')
-rw-r--r--bootstrap/Bootstrap.php13
-rw-r--r--bootstrap/Functions.php58
-rw-r--r--bootstrap/Request.php14
-rw-r--r--bootstrap/Router.php42
-rw-r--r--bootstrap/Routes.php32
-rw-r--r--bootstrap/comments/admin/admin.css102
-rw-r--r--bootstrap/comments/admin/admin.html56
-rw-r--r--bootstrap/comments/admin/admin.js41
-rw-r--r--bootstrap/comments/admin/index.php58
-rw-r--r--bootstrap/comments/admin/views/blocklist/blocklist.html44
-rw-r--r--bootstrap/comments/admin/views/blocklist/blocklist.js30
-rw-r--r--bootstrap/comments/admin/views/blocklist/index.php104
-rw-r--r--bootstrap/comments/admin/views/documentation/documentation.html27
-rw-r--r--bootstrap/comments/admin/views/documentation/index.php38
-rw-r--r--bootstrap/comments/admin/views/example/example.html30
-rw-r--r--bootstrap/comments/admin/views/example/index.php38
-rw-r--r--bootstrap/comments/admin/views/login/index.php92
-rw-r--r--bootstrap/comments/admin/views/login/login.css27
-rw-r--r--bootstrap/comments/admin/views/login/login.html37
-rw-r--r--bootstrap/comments/admin/views/login/login.js10
-rw-r--r--bootstrap/comments/admin/views/moderation/index.php88
-rw-r--r--bootstrap/comments/admin/views/moderation/moderation.css3
-rw-r--r--bootstrap/comments/admin/views/moderation/moderation.html30
-rw-r--r--bootstrap/comments/admin/views/moderation/threads.css27
-rw-r--r--bootstrap/comments/admin/views/moderation/threads.html34
-rw-r--r--bootstrap/comments/admin/views/moderation/threads.js21
-rw-r--r--bootstrap/comments/admin/views/moderation/threads.php33
-rw-r--r--bootstrap/comments/admin/views/settings/index.php515
-rw-r--r--bootstrap/comments/admin/views/settings/settings.css15
-rw-r--r--bootstrap/comments/admin/views/settings/settings.html41
-rw-r--r--bootstrap/comments/admin/views/shared/update-elements.js12
-rw-r--r--bootstrap/comments/admin/views/updates/index.php38
-rw-r--r--bootstrap/comments/admin/views/updates/updates.html27
-rw-r--r--bootstrap/comments/admin/views/url-queries/index.php134
-rw-r--r--bootstrap/comments/admin/views/url-queries/url-queries.html44
-rw-r--r--bootstrap/comments/admin/views/url-queries/url-queries.js36
-rw-r--r--bootstrap/comments/admin/views/view-setup.php107
-rw-r--r--bootstrap/comments/api/backend/count-link-ajax.php76
-rw-r--r--bootstrap/comments/api/backend/latest-ajax.php207
-rw-r--r--bootstrap/comments/api/count-link.php95
-rw-r--r--bootstrap/comments/api/frontends/count-link/constructor.js43
-rw-r--r--bootstrap/comments/api/frontends/count-link/getcommentcount.js19
-rw-r--r--bootstrap/comments/api/frontends/count-link/instantiate.js4
-rw-r--r--bootstrap/comments/api/frontends/latest/addcontrols.js22
-rw-r--r--bootstrap/comments/api/frontends/latest/addratings.js22
-rw-r--r--bootstrap/comments/api/frontends/latest/constructor.js79
-rw-r--r--bootstrap/comments/api/frontends/latest/init.js37
-rw-r--r--bootstrap/comments/api/frontends/latest/instantiate.js4
-rw-r--r--bootstrap/comments/api/json.php76
-rw-r--r--bootstrap/comments/api/latest.php154
-rw-r--r--bootstrap/comments/api/rss.php327
-rw-r--r--bootstrap/comments/backend/classes/avatars.php90
-rw-r--r--bootstrap/comments/backend/classes/commentfiles.php106
-rw-r--r--bootstrap/comments/backend/classes/commentparser.php227
-rw-r--r--bootstrap/comments/backend/classes/commentsui.php634
-rw-r--r--bootstrap/comments/backend/classes/cookies.php128
-rw-r--r--bootstrap/comments/backend/classes/database.php395
-rw-r--r--bootstrap/comments/backend/classes/datafiles.php176
-rw-r--r--bootstrap/comments/backend/classes/defaultlogin.php110
-rw-r--r--bootstrap/comments/backend/classes/encryption.php191
-rw-r--r--bootstrap/comments/backend/classes/formui.php1093
-rw-r--r--bootstrap/comments/backend/classes/hashover.php440
-rw-r--r--bootstrap/comments/backend/classes/htmltag.php282
-rw-r--r--bootstrap/comments/backend/classes/javascriptbuild.php144
-rw-r--r--bootstrap/comments/backend/classes/javascriptminifier.php121
-rw-r--r--bootstrap/comments/backend/classes/locale.php177
-rw-r--r--bootstrap/comments/backend/classes/login.php202
-rw-r--r--bootstrap/comments/backend/classes/markdown.php126
-rw-r--r--bootstrap/comments/backend/classes/metadata.php104
-rw-r--r--bootstrap/comments/backend/classes/misc.php148
-rw-r--r--bootstrap/comments/backend/classes/parsejson.php90
-rw-r--r--bootstrap/comments/backend/classes/parsesql.php170
-rw-r--r--bootstrap/comments/backend/classes/parsexml.php147
-rw-r--r--bootstrap/comments/backend/classes/phpmode.php440
-rw-r--r--bootstrap/comments/backend/classes/postdata.php93
-rw-r--r--bootstrap/comments/backend/classes/secrets.php41
-rw-r--r--bootstrap/comments/backend/classes/settings.php341
-rw-r--r--bootstrap/comments/backend/classes/setup.php446
-rw-r--r--bootstrap/comments/backend/classes/sourcecode.php474
-rw-r--r--bootstrap/comments/backend/classes/spamcheck.php168
-rw-r--r--bootstrap/comments/backend/classes/statistics.php91
-rw-r--r--bootstrap/comments/backend/classes/templater.php124
-rw-r--r--bootstrap/comments/backend/classes/thread.php228
-rw-r--r--bootstrap/comments/backend/classes/writecomments.php871
-rw-r--r--bootstrap/comments/backend/comments-ajax.php208
-rw-r--r--bootstrap/comments/backend/form-actions.php114
-rw-r--r--bootstrap/comments/backend/javascript-setup.php44
-rw-r--r--bootstrap/comments/backend/json-setup.php41
-rw-r--r--bootstrap/comments/backend/like.php176
-rw-r--r--bootstrap/comments/backend/load-comments.php71
-rw-r--r--bootstrap/comments/backend/locales/da.php206
-rw-r--r--bootstrap/comments/backend/locales/de.php206
-rw-r--r--bootstrap/comments/backend/locales/el.php206
-rw-r--r--bootstrap/comments/backend/locales/en.php206
-rw-r--r--bootstrap/comments/backend/locales/es.php206
-rw-r--r--bootstrap/comments/backend/locales/fa.php206
-rw-r--r--bootstrap/comments/backend/locales/fr.php213
-rw-r--r--bootstrap/comments/backend/locales/jp.php206
-rw-r--r--bootstrap/comments/backend/locales/ko.php206
-rw-r--r--bootstrap/comments/backend/locales/lt.php215
-rw-r--r--bootstrap/comments/backend/locales/nl.php206
-rw-r--r--bootstrap/comments/backend/locales/pl.php206
-rw-r--r--bootstrap/comments/backend/locales/pt-br.php206
-rw-r--r--bootstrap/comments/backend/locales/ro.php206
-rw-r--r--bootstrap/comments/backend/locales/tr.php206
-rw-r--r--bootstrap/comments/backend/locales/zh-cn.php206
-rw-r--r--bootstrap/comments/backend/nocache-headers.php25
-rw-r--r--bootstrap/comments/backend/php-setup.php33
-rw-r--r--bootstrap/comments/backend/source-viewer.html27
-rw-r--r--bootstrap/comments/backend/source-viewer.php121
-rw-r--r--bootstrap/comments/backend/standard-setup.php25
-rw-r--r--bootstrap/comments/comments.php265
-rw-r--r--bootstrap/comments/frontend/addcomments.js43
-rw-r--r--bootstrap/comments/frontend/addcontrols.js113
-rw-r--r--bootstrap/comments/frontend/addratings.js54
-rw-r--r--bootstrap/comments/frontend/ajax.js71
-rw-r--r--bootstrap/comments/frontend/ajaxedit.js29
-rw-r--r--bootstrap/comments/frontend/ajaxpost.js29
-rw-r--r--bootstrap/comments/frontend/appendcomments.js50
-rw-r--r--bootstrap/comments/frontend/appendcss.js66
-rw-r--r--bootstrap/comments/frontend/appendrss.js17
-rw-r--r--bootstrap/comments/frontend/backendpath.js10
-rw-r--r--bootstrap/comments/frontend/cancelswitcher.js40
-rw-r--r--bootstrap/comments/frontend/classes.js59
-rw-r--r--bootstrap/comments/frontend/cloneobject.js5
-rw-r--r--bootstrap/comments/frontend/comments.js400
-rw-r--r--bootstrap/comments/frontend/commentvalidator.js51
-rw-r--r--bootstrap/comments/frontend/constructor.js51
-rw-r--r--bootstrap/comments/frontend/datetime.js136
-rw-r--r--bootstrap/comments/frontend/displayerror.js12
-rw-r--r--bootstrap/comments/frontend/editcomment.js96
-rw-r--r--bootstrap/comments/frontend/elements.js82
-rw-r--r--bootstrap/comments/frontend/emailvalidator.js42
-rw-r--r--bootstrap/comments/frontend/embedimage.js36
-rw-r--r--bootstrap/comments/frontend/eoltrim.js5
-rw-r--r--bootstrap/comments/frontend/formattingonclick.js25
-rw-r--r--bootstrap/comments/frontend/formevents.js17
-rw-r--r--bootstrap/comments/frontend/getallcomments.js25
-rw-r--r--bootstrap/comments/frontend/getbackendqueries.js28
-rw-r--r--bootstrap/comments/frontend/getmainelement.js38
-rw-r--r--bootstrap/comments/frontend/gettitle.js5
-rw-r--r--bootstrap/comments/frontend/geturl.js35
-rw-r--r--bootstrap/comments/frontend/hidemorelink.js39
-rw-r--r--bootstrap/comments/frontend/htmltonodelist.js5
-rw-r--r--bootstrap/comments/frontend/incrementcounts.js11
-rw-r--r--bootstrap/comments/frontend/init.js250
-rw-r--r--bootstrap/comments/frontend/instantiate.js4
-rw-r--r--bootstrap/comments/frontend/instantiator.js66
-rw-r--r--bootstrap/comments/frontend/likecomment.js78
-rw-r--r--bootstrap/comments/frontend/markdown.js97
-rw-r--r--bootstrap/comments/frontend/messages.js178
-rw-r--r--bootstrap/comments/frontend/mouseoverchanger.js23
-rw-r--r--bootstrap/comments/frontend/onready.js14
-rw-r--r--bootstrap/comments/frontend/openembeddedimage.js50
-rw-r--r--bootstrap/comments/frontend/optionalmethod.js11
-rw-r--r--bootstrap/comments/frontend/parseall.js27
-rw-r--r--bootstrap/comments/frontend/permalinks.js57
-rw-r--r--bootstrap/comments/frontend/postcomment.js30
-rw-r--r--bootstrap/comments/frontend/postrequest.js102
-rw-r--r--bootstrap/comments/frontend/regex.js10
-rw-r--r--bootstrap/comments/frontend/replytocomment.js55
-rw-r--r--bootstrap/comments/frontend/script.js13
-rw-r--r--bootstrap/comments/frontend/showmorecomments.js69
-rw-r--r--bootstrap/comments/frontend/sortcomments.js235
-rw-r--r--bootstrap/comments/frontend/strings.js104
-rw-r--r--bootstrap/comments/frontend/uncollapsecommentslink.js35
-rw-r--r--bootstrap/comments/frontend/uncollapseinterface.js37
-rw-r--r--bootstrap/comments/frontend/uncollapseinterfacelink.js24
-rw-r--r--bootstrap/comments/frontend/validatecomment.js28
-rw-r--r--bootstrap/comments/frontend/validateemail.js20
-rw-r--r--bootstrap/comments/images/avatar.pngbin0 -> 569 bytes
-rw-r--r--bootstrap/comments/images/avatar.svg15
-rw-r--r--bootstrap/comments/images/deleted-icon.pngbin0 -> 912 bytes
-rw-r--r--bootstrap/comments/images/deleted-icon.svg25
-rw-r--r--bootstrap/comments/images/error-icon.pngbin0 -> 336 bytes
-rw-r--r--bootstrap/comments/images/error-icon.svg15
-rw-r--r--bootstrap/comments/images/favicon.icobin0 -> 1150 bytes
-rw-r--r--bootstrap/comments/images/favicon.pngbin0 -> 306 bytes
-rw-r--r--bootstrap/comments/images/first-comment.pngbin0 -> 502 bytes
-rw-r--r--bootstrap/comments/images/first-comment.svg15
-rw-r--r--bootstrap/comments/images/hashover-logo.pngbin0 -> 19552 bytes
-rw-r--r--bootstrap/comments/images/hashover-logo.svg2
-rw-r--r--bootstrap/comments/images/inputs-and-buttons.pngbin0 -> 3797 bytes
-rw-r--r--bootstrap/comments/images/inputs-and-buttons.svg33
-rw-r--r--bootstrap/comments/images/loading-bltr.gifbin0 -> 2508 bytes
-rw-r--r--bootstrap/comments/images/loading-ctrl.gifbin0 -> 599 bytes
-rw-r--r--bootstrap/comments/images/loading-ltr.gifbin0 -> 864 bytes
-rw-r--r--bootstrap/comments/images/loading.gifbin0 -> 525 bytes
-rw-r--r--bootstrap/comments/images/pending-icon.pngbin0 -> 473 bytes
-rw-r--r--bootstrap/comments/images/pending-icon.svg15
-rw-r--r--bootstrap/comments/images/place-holder.pngbin0 -> 6597 bytes
-rw-r--r--bootstrap/comments/images/place-holder.svg33
-rw-r--r--bootstrap/comments/images/white-noise.pngbin0 -> 5361 bytes
-rw-r--r--bootstrap/comments/themes/1.0-ported/comments.css801
-rw-r--r--bootstrap/comments/themes/1.0-ported/comments.html30
-rw-r--r--bootstrap/comments/themes/1.0-ported/images/inputs-and-buttons.pngbin0 -> 4323 bytes
-rw-r--r--bootstrap/comments/themes/default-borderless/comments.css1172
-rw-r--r--bootstrap/comments/themes/default-borderless/latest.css14
-rw-r--r--bootstrap/comments/themes/default/comments.css1132
-rw-r--r--bootstrap/comments/themes/default/comments.html29
-rw-r--r--bootstrap/comments/themes/default/general.css125
-rw-r--r--bootstrap/comments/themes/default/latest.css14
-rw-r--r--bootstrap/comments/themes/default/latest.html14
-rw-r--r--bootstrap/comments/themes/default/special.css160
-rw-r--r--bootstrap/database/Connection.php18
-rw-r--r--bootstrap/database/QueryBuilder.php31
206 files changed, 23378 insertions, 0 deletions
diff --git a/bootstrap/Bootstrap.php b/bootstrap/Bootstrap.php
new file mode 100644
index 0000000..2754a1d
--- /dev/null
+++ b/bootstrap/Bootstrap.php
@@ -0,0 +1,13 @@
+<?php
+
+/* composer autoloader */
+require '../vendor/autoload.php';
+
+/* core functions */
+require '../bootstrap/Functions.php';
+
+/* source config file */
+$config = include '../AppConfig.php';
+
+/* database query setup */
+$contact['database'] = new QueryBuilder(Connection::make($config['database']));
diff --git a/bootstrap/Functions.php b/bootstrap/Functions.php
new file mode 100644
index 0000000..2308f9b
--- /dev/null
+++ b/bootstrap/Functions.php
@@ -0,0 +1,58 @@
+<?php
+
+// PHP mailer namespace.
+use PHPMailer\PHPMailer\PHPMailer;
+use PHPMailer\PHPMailer\Exception;
+
+function sendMail($name, $email, $message)
+{
+ // Require mail config
+ $config = require '../AppConfig.php';
+
+ $mail = new PHPMailer(true);
+ try {
+ //Server settings
+ //$mail->SMTPDebug = 2; // Enable verbose debug output
+ $mail->isSMTP(); // Set mailer to use SMTP
+ $mail->Host = $config['mail']['host']; // Specify main and backup SMTP servers
+ $mail->SMTPAuth = true; // Enable SMTP authentication
+ $mail->Username = $config['mail']['username']; // SMTP username
+ $mail->Password = $config['mail']['password']; // SMTP password
+ $mail->SMTPSecure = 'ssl'; // Enable TLS encryption, `ssl` also accepted
+ $mail->Port = $config['mail']['port']; // TCP port to connect to
+
+ //Recipients
+ $mail->setFrom('edwinmattiacci@yahoo.com', 'Edwin Mattiacci');
+ $mail->addAddress('edwinmattiacci@yahoo.com', 'Edwin Mattiacci');
+ $mail->addReplyTo($email, $name);
+
+ //Content
+ $mail->isHTML(true);
+ $mail->Subject = 'New message from ' . $name;
+ $mail->Body = $message;
+ $mail->AltBody = $message;
+
+ $mail->send();
+ } catch (Exception $e) {
+ require '../views/senterror.view.php';
+ }
+}
+
+function webTitle()
+{
+ switch ($_SERVER['REQUEST_URI']) {
+ case '/feedback/':
+ $uri = $_SERVER['REQUEST_URI'];
+ echo $titleHeader = 'Feedback';
+ break;
+
+ case '/contact/':
+ $uri = $_SERVER['REQUEST_URI'];
+ echo $titleHeader = 'Contact';
+ break;
+
+ default:
+ $uri = '/';
+ echo $titleHeader = 'Voiceover';
+ }
+}
diff --git a/bootstrap/Request.php b/bootstrap/Request.php
new file mode 100644
index 0000000..cc8e687
--- /dev/null
+++ b/bootstrap/Request.php
@@ -0,0 +1,14 @@
+<?php
+
+class Request
+{
+ public static function uri()
+ {
+ return (string) trim($_SERVER['REQUEST_URI'], "/");
+ }
+
+ public static function method()
+ {
+ return $_SERVER['REQUEST_METHOD'];
+ }
+}
diff --git a/bootstrap/Router.php b/bootstrap/Router.php
new file mode 100644
index 0000000..a098c30
--- /dev/null
+++ b/bootstrap/Router.php
@@ -0,0 +1,42 @@
+<?php
+
+class Router
+{
+ protected $routes = [
+ 'GET' => [],
+ 'POST' => [],
+ 'HEAD' => []
+ ];
+
+ public function get($uri, $controller)
+ {
+ $this->routes['GET'][$uri] = $controller;
+ }
+
+ public function post($uri, $controller)
+ {
+ $this->routes['POST'][$uri] = $controller;
+ }
+
+ public function head($uri, $controller)
+ {
+ $this->routes['HEAD'][$uri] = $controller;
+ }
+
+ public static function load($file)
+ {
+ $router = new static;
+ require $file;
+ return $router;
+ }
+
+ public function direct($uri, $requestType)
+ {
+ if (is_array($this->routes[$requestType]) && array_key_exists($uri, $this->routes[$requestType])) {
+ return $this->routes[$requestType][$uri];
+ }
+ // throw new Exception('No route defined for this URI: "'.$uri.'"');
+ http_response_code(404);
+ die(require '../views/404.view.php');
+ }
+}
diff --git a/bootstrap/Routes.php b/bootstrap/Routes.php
new file mode 100644
index 0000000..4cfe5de
--- /dev/null
+++ b/bootstrap/Routes.php
@@ -0,0 +1,32 @@
+<?php
+
+/* public routes */
+
+$router->get('', '../controllers/index.controller.php');
+$router->get('contact', '../controllers/contact.php');
+$router->get('feedback', '../controllers/feedback.php');
+$router->get('comments', '../controllers/comments.php');
+$router->get('construction', '../controllers/construction.php');
+$router->get('hashget', '../controllers/comments.php');
+$router->get('hashover', '../controllers/comments.php');
+
+$router->head('', '../controllers/index.controller.php');
+$router->head('contact', '../controllers/contact.php');
+$router->head('feedback', '../controllers/feedback.php');
+$router->head('comments', '../controllers/comments.php');
+$router->head('construction', '../controllers/construction.php');
+$router->head('hashget', '../controllers/comments.php');
+$router->head('hashover', '../controllers/comments.php');
+
+$router->post('?sent', '../controllers/mail.controller.php');
+$router->post('hashpost', '../controllers/comments.php');
+
+/* backend routes */
+
+$router->get('latestajax', '../bootstrap/comments/backend/latest-ajax.php');
+$router->get('countlinkajax', '../bootstrap/comments/backend/count-link-ajax.php');
+
+$router->post('commentsajax', '../bootstrap/comments/backend/comments-ajax.php');
+$router->post('formactions', '../bootstrap/comments/backend/form-actions.php');
+$router->post('loadcomments', '../bootstrap/comments/backend/load-comments.php');
+$router->post('likecomment', '../bootstrap/comments/backend/like.php');
diff --git a/bootstrap/comments/admin/admin.css b/bootstrap/comments/admin/admin.css
new file mode 100644
index 0000000..f2be72b
--- /dev/null
+++ b/bootstrap/comments/admin/admin.css
@@ -0,0 +1,102 @@
+body, html {
+ width: 100%;
+ min-width: 100%;
+ height: 100%;
+ min-height: 100%;
+}
+
+body {
+ padding: 0px 0px 0px 230px;
+}
+
+#sidebar {
+ position: absolute;
+ top: 0px;
+ left: 0px;
+ width: 230px;
+ height: 100%;
+ border-right: #CCCCCC;
+ border-right: 1px solid #CCCCCC;
+ background-color: #F5F5F5;
+ overflow: auto;
+}
+
+#navigation {
+ border-top: 1px solid #CCCCCC;
+ border-bottom: 1px solid #CCCCCC;
+}
+
+#logo,
+#navigation div,
+#navigation a {
+ position: relative;
+ display: block;
+ width: 100%;
+ vertical-align: top;
+ overflow: hidden;
+}
+
+#navigation div {
+ border: none;
+ border-bottom: 1px solid #CCCCCC;
+}
+
+#navigation div:last-child {
+ border-bottom: none;
+}
+
+#logo,
+#navigation a {
+ padding: 15px;
+ margin: 0px;
+ border: none;
+}
+
+#navigation a,
+#navigation:hover a.active {
+ border: none;
+ border-right: 4px solid transparent;
+ color: #222222;
+}
+
+#navigation a.active {
+ color: #266394;
+ border-right: 4px solid #3485C7;
+ background-color: #EEEEEE;
+ font-weight: bold;
+}
+
+#navigation:hover a.active {
+ background-color: transparent;
+}
+
+#navigation a:hover,
+#navigation a.active:hover {
+ color: #266394;
+ border-color: #3485C7;
+ background-color: #EEEEEE;
+}
+
+div#logo,
+div#logo a {
+ height: 234px;
+}
+
+div#logo a,
+#navigation:hover div#logo a {
+ border: none;
+ border-bottom: 4px solid transparent;
+}
+
+div#logo a:hover,
+div#logo a.active {
+ border: none;
+ border-bottom: 4px solid #5E94FF;
+}
+
+#content {
+ width: 100%;
+ height: 100%;
+ vertical-align: top;
+ background-color: #FFFFFF;
+}
diff --git a/bootstrap/comments/admin/admin.html b/bootstrap/comments/admin/admin.html
new file mode 100644
index 0000000..155d743
--- /dev/null
+++ b/bootstrap/comments/admin/admin.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+
+<html lang="en" dir="ltr">
+ <head>
+ <title>HashOver - {hashover:title}</title>
+
+ <link type="text/css" href="../themes/default/general.css" rel="stylesheet">
+ <link type="text/css" href="../themes/default/special.css" rel="stylesheet">
+ <link type="text/css" href="admin.css" rel="stylesheet">
+
+ <script type="text/javascript" src="admin.js"></script>
+
+ <meta http-equiv="Content-type" content="text/html; charset=utf-8">
+ <meta http-equiv="Content-Language" content="EN">
+
+ <link type="image/x-icon" href="../images/favicon.png" rel="shortcut icon">
+ <link type="image/x-icon" href="../images/favicon.png" rel="icon">
+ <link href="https://www.gnu.org/licenses/agpl" rel="copyright">
+ </head>
+
+ <body>
+ <div id="sidebar">
+ <div id="logo">
+ <img src="../images/hashover-logo.png" alt="HashOver" width="100%">
+ </div>
+
+ <div id="navigation">
+ <div>
+ <a class="view-link" href="views/moderation/" target="content">{hashover:moderation}</a>
+ </div>
+
+ <div>
+ <a class="view-link" href="views/blocklist/" target="content">{hashover:block-ip-addresses}</a>
+ </div>
+
+ <div>
+ <a class="view-link" href="views/url-queries/" target="content">{hashover:filter-url-queries}</a>
+ </div>
+
+ <div>
+ <a class="view-link" href="views/updates/" target="content">{hashover:check-for-updates}</a>
+ </div>
+
+ <div>
+ <a class="view-link" href="views/documentation/" target="content">{hashover:documentation}</a>
+ </div>
+
+ <div>
+ <a class="view-link" href="views/settings/" target="content">{hashover:settings}</a>
+ </div>
+ </div>
+ </div>
+
+ <iframe id="content" name="content" src="views/moderation/" frameborder="0"></iframe>
+ </body>
+</html>
diff --git a/bootstrap/comments/admin/admin.js b/bootstrap/comments/admin/admin.js
new file mode 100644
index 0000000..046735b
--- /dev/null
+++ b/bootstrap/comments/admin/admin.js
@@ -0,0 +1,41 @@
+// Wait for the page HTML to be parsed
+document.addEventListener ('DOMContentLoaded', function () {
+ // Get view links
+ var viewLinks = document.getElementsByClassName ('view-link');
+
+ // Get content frame
+ var content = document.getElementById ('content');
+
+ // Execute a given function for each view link
+ function eachViewLink (callback)
+ {
+ for (var i = 0, il = viewLinks.length; i < il; i++) {
+ callback (viewLinks[i]);
+ }
+ }
+
+ // Remove active class from all view links
+ function clearViewTabs ()
+ {
+ eachViewLink (function (link) {
+ link.className = 'view-link';
+ });
+ }
+
+ // Automatically select the proper view tab on page load
+ content.onload = function ()
+ {
+ // Remove active class from all view links
+ clearViewTabs ();
+
+ // Select active proper tab for currently loaded view
+ eachViewLink (function (link) {
+ var regex = new RegExp (link.getAttribute ('href'));
+ var frameUrl = content.contentDocument.location.href;
+
+ if (regex.test (decodeURIComponent (frameUrl))) {
+ link.className += ' active';
+ }
+ });
+ };
+}, false);
diff --git a/bootstrap/comments/admin/index.php b/bootstrap/comments/admin/index.php
new file mode 100644
index 0000000..d7a29bc
--- /dev/null
+++ b/bootstrap/comments/admin/index.php
@@ -0,0 +1,58 @@
+<?php namespace HashOver;
+
+// Copyright (C) 2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// HashOver is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// HashOver is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with HashOver. If not, see <http://www.gnu.org/licenses/>.
+
+
+// Do some standard HashOver setup work
+require (realpath ('../backend/standard-setup.php'));
+
+// Autoload class files
+spl_autoload_register (function ($uri) {
+ $uri = str_replace ('\\', '/', strtolower ($uri));
+ $class_name = basename ($uri);
+
+ if (!@include (realpath ('../backend/classes/' . $class_name . '.php'))) {
+ echo '"' . $class_name . '.php" file could not be included!';
+ exit;
+ }
+});
+
+try {
+ // Instantiate HashOver class
+ $hashover = new \HashOver ();
+ $hashover->initiate ();
+ $hashover->finalize ();
+
+ // Template data
+ $template = array (
+ 'title' => $hashover->locale->text['admin'],
+ 'moderation' => $hashover->locale->text['moderation'],
+ 'block-ip-addresses' => $hashover->locale->text['block-ip-addresses'],
+ 'filter-url-queries' => $hashover->locale->text['filter-url-queries'],
+ 'check-for-updates' => $hashover->locale->text['check-for-updates'],
+ 'documentation' => $hashover->locale->text['documentation'],
+ 'settings' => $hashover->locale->text['settings']
+ );
+
+ // Load and parse HTML template
+ echo $hashover->templater->parseTemplate ('admin.html', $template);
+
+} catch (\Exception $error) {
+ $misc = new Misc ('php');
+ $message = $error->getMessage ();
+ $misc->displayError ($message);
+}
diff --git a/bootstrap/comments/admin/views/blocklist/blocklist.html b/bootstrap/comments/admin/views/blocklist/blocklist.html
new file mode 100644
index 0000000..86dd089
--- /dev/null
+++ b/bootstrap/comments/admin/views/blocklist/blocklist.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+
+<html lang="en" dir="ltr">
+ <head>
+ <title>HashOver - {hashover:title}</title>
+
+ <link type="text/css" href="../../../themes/default/general.css" rel="stylesheet">
+ <link type="text/css" href="../../../themes/default/special.css" rel="stylesheet">
+
+ <script type="text/javascript" src="../shared/update-elements.js"></script>
+ <script type="text/javascript" src="blocklist.js"></script>
+
+ <meta http-equiv="Content-type" content="text/html; charset=utf-8">
+ <meta http-equiv="Content-Language" content="EN">
+
+ <link type="image/x-icon" href="../../../images/favicon.png" rel="shortcut icon">
+ <link type="image/x-icon" href="../../../images/favicon.png" rel="icon">
+ <link href="https://www.gnu.org/licenses/agpl" rel="copyright">
+ </head>
+
+ <body>
+ <h1 class="underlined">
+ <span id="title">{hashover:title}</span>
+
+ {hashover:logout}
+ </h1>
+
+ <div class="p-spaced muted-text">
+ {hashover:sub-title}
+ {hashover:message}
+ </div>
+
+ <form method="post">
+ <p id="ip-list">
+ {hashover:inputs}
+ </p>
+
+ <p>
+ <input id="save-button" type="submit" value="{hashover:save-button}">
+ <input id="new-button" type="button" value="+">
+ </p>
+ </form>
+ </body>
+</html>
diff --git a/bootstrap/comments/admin/views/blocklist/blocklist.js b/bootstrap/comments/admin/views/blocklist/blocklist.js
new file mode 100644
index 0000000..4b04dbf
--- /dev/null
+++ b/bootstrap/comments/admin/views/blocklist/blocklist.js
@@ -0,0 +1,30 @@
+// Wait for the page HTML to be parsed
+document.addEventListener ('DOMContentLoaded', function () {
+ // Get the "New Address" and "Save" buttons
+ var newButton = document.getElementById ('new-button');
+ var ipList = document.getElementById ('ip-list');
+ var saveButton = document.getElementById ('save-button');
+
+ newButton.onclick = function ()
+ {
+ // Create input and indentation
+ var addresses = document.getElementsByClassName ('addresses');
+ var indentation = document.createTextNode ('\n\t\t\t\t');
+
+ // Clone the first address field
+ var input = addresses[0].cloneNode (true);
+
+ // Remove its value
+ input.value = '';
+
+ // Append indentation and input to IP address list
+ ipList.appendChild (indentation);
+ ipList.appendChild (input);
+ };
+
+ // Disable the "Save" button when clicked
+ saveButton.onclick = function ()
+ {
+ this.disabled = true;
+ };
+}, false);
diff --git a/bootstrap/comments/admin/views/blocklist/index.php b/bootstrap/comments/admin/views/blocklist/index.php
new file mode 100644
index 0000000..8d27633
--- /dev/null
+++ b/bootstrap/comments/admin/views/blocklist/index.php
@@ -0,0 +1,104 @@
+<?php namespace HashOver;
+
+// Copyright (C) 2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// HashOver is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// HashOver is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with HashOver. If not, see <http://www.gnu.org/licenses/>.
+
+
+try {
+ // View setup
+ require (realpath ('../view-setup.php'));
+
+ // Default blocklist array
+ $blocklist = array ();
+
+ // Blocklist JSON file location
+ $blocklist_file = $hashover->setup->getAbsolutePath ('config/blocklist.json');
+
+ // Check if the form has been submitted
+ if (!empty ($_POST['addresses']) and is_array ($_POST['addresses'])) {
+ // If so, run through submitted addresses
+ foreach ($_POST['addresses'] as $address) {
+ // Add each non-empty address value to the blocklist array
+ if (!empty ($address)) {
+ $blocklist[] = $address;
+ }
+ }
+
+ // Save the JSON data to the blocklist file
+ if ($hashover->setup->verifyAdmin ($hashover->login->password)
+ and $data_files->saveJSON ($blocklist_file, $blocklist))
+ {
+ // Redirect with success indicator
+ header ('Location: index.php?status=success');
+ } else {
+ // Redirect with failure indicator
+ header ('Location: index.php?status=failure');
+ }
+
+ // Exit after redirect
+ exit;
+ }
+
+ // Otherwise, load and parse blocklist file
+ $json = $data_files->readJSON ($blocklist_file);
+
+ // Check for JSON parse error
+ if (is_array ($json)) {
+ $blocklist = $json;
+ }
+
+ // IP Address inputs
+ $inputs = new HTMLTag ('span');
+
+ // Create IP address inputs
+ for ($i = 0, $il = max (3, count ($blocklist)); $i < $il; $i++) {
+ // Use IP address from file or blank
+ $address = !empty ($blocklist[$i]) ? $blocklist[$i] : '';
+
+ // Create input tag
+ $input = new HTMLTag ('input', array (
+ 'class' => 'addresses',
+ 'type' => 'text',
+ 'name' => 'addresses[]',
+ 'value' => $address,
+ 'size' => '15',
+ 'maxlength' => '15',
+ 'placeholder' => '127.0.0.1',
+ 'title' => $hashover->locale->text['blocklist-ip-tip']
+ ), false, true);
+
+ // Add input to inputs container
+ $inputs->appendChild ($input);
+ }
+
+ // Template data
+ $template = array (
+ 'title' => $hashover->locale->text['blocklist-title'],
+ 'logout' => $logout->asHTML ("\t\t\t"),
+ 'sub-title' => $hashover->locale->text['blocklist-sub'],
+ 'message' => $form_message,
+ 'inputs' => $inputs->getInnerHTML ("\t\t\t\t"),
+ 'save-button' => $hashover->locale->text['save']
+ );
+
+ // Load and parse HTML template
+ echo $hashover->templater->parseTemplate ('blocklist.html', $template);
+
+} catch (\Exception $error) {
+ $misc = new Misc ('php');
+ $message = $error->getMessage ();
+ $misc->displayError ($message);
+}
diff --git a/bootstrap/comments/admin/views/documentation/documentation.html b/bootstrap/comments/admin/views/documentation/documentation.html
new file mode 100644
index 0000000..be82e62
--- /dev/null
+++ b/bootstrap/comments/admin/views/documentation/documentation.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+
+<html lang="en" dir="ltr">
+ <head>
+ <title>HashOver - {hashover:title}</title>
+
+ <link type="text/css" href="../../../themes/default/general.css" rel="stylesheet">
+ <link type="text/css" href="../../../themes/default/special.css" rel="stylesheet">
+
+ <meta http-equiv="Content-type" content="text/html; charset=utf-8">
+ <meta http-equiv="Content-Language" content="EN">
+
+ <link type="image/x-icon" href="../../../images/favicon.png" rel="shortcut icon">
+ <link type="image/x-icon" href="../../../images/favicon.png" rel="icon">
+ <link href="https://www.gnu.org/licenses/agpl" rel="copyright">
+ </head>
+
+ <body>
+ <h1 class="underlined">
+ {hashover:title}
+
+ {hashover:logout}
+ </h1>
+
+ <p class="muted-text">{hashover:sub-title}</p>
+ </body>
+</html>
diff --git a/bootstrap/comments/admin/views/documentation/index.php b/bootstrap/comments/admin/views/documentation/index.php
new file mode 100644
index 0000000..74a62be
--- /dev/null
+++ b/bootstrap/comments/admin/views/documentation/index.php
@@ -0,0 +1,38 @@
+<?php namespace HashOver;
+
+// Copyright (C) 2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// HashOver is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// HashOver is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with HashOver. If not, see <http://www.gnu.org/licenses/>.
+
+
+try {
+ // View setup
+ require (realpath ('../view-setup.php'));
+
+ // Template data
+ $template = array (
+ 'title' => $hashover->locale->text['documentation'],
+ 'logout' => $logout->asHTML ("\t\t\t"),
+ 'sub-title' => $hashover->locale->text['coming-soon']
+ );
+
+ // Load and parse HTML template
+ echo $hashover->templater->parseTemplate ('documentation.html', $template);
+
+} catch (\Exception $error) {
+ $misc = new Misc ('php');
+ $message = $error->getMessage ();
+ $misc->displayError ($message);
+}
diff --git a/bootstrap/comments/admin/views/example/example.html b/bootstrap/comments/admin/views/example/example.html
new file mode 100644
index 0000000..1e4464c
--- /dev/null
+++ b/bootstrap/comments/admin/views/example/example.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+
+<html lang="en" dir="ltr">
+ <head>
+ <title>HashOver - {hashover:title}</title>
+
+ <link type="text/css" href="../../../themes/default/general.css" rel="stylesheet">
+ <link type="text/css" href="../../../themes/default/special.css" rel="stylesheet">
+ <link type="text/css" href="example.css" rel="stylesheet">
+
+ <script type="text/javascript" src="example.js"></script>
+
+ <meta http-equiv="Content-type" content="text/html; charset=utf-8">
+ <meta http-equiv="Content-Language" content="EN">
+
+ <link type="image/x-icon" href="../../../images/favicon.png" rel="shortcut icon">
+ <link type="image/x-icon" href="../../../images/favicon.png" rel="icon">
+ <link href="https://www.gnu.org/licenses/agpl" rel="copyright">
+ </head>
+
+ <body>
+ <h1 class="underlined">
+ {hashover:title}
+
+ {hashover:logout}
+ </h1>
+
+ <p class="muted-text">{hashover:sub-title}</p>
+ </body>
+</html>
diff --git a/bootstrap/comments/admin/views/example/index.php b/bootstrap/comments/admin/views/example/index.php
new file mode 100644
index 0000000..7848daf
--- /dev/null
+++ b/bootstrap/comments/admin/views/example/index.php
@@ -0,0 +1,38 @@
+<?php namespace HashOver;
+
+// Copyright (C) 2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// HashOver is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// HashOver is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with HashOver. If not, see <http://www.gnu.org/licenses/>.
+
+
+try {
+ // View setup
+ require (realpath ('../view-setup.php'));
+
+ // Template data
+ $template = array (
+ 'title' => $hashover->locale->text['example'],
+ 'logout' => $logout->asHTML ("\t\t\t"),
+ 'sub-title' => $hashover->locale->text['example']
+ );
+
+ // Load and parse HTML template
+ echo $hashover->templater->parseTemplate ('example.html', $template);
+
+} catch (\Exception $error) {
+ $misc = new Misc ('php');
+ $message = $error->getMessage ();
+ $misc->displayError ($message);
+}
diff --git a/bootstrap/comments/admin/views/login/index.php b/bootstrap/comments/admin/views/login/index.php
new file mode 100644
index 0000000..1913ece
--- /dev/null
+++ b/bootstrap/comments/admin/views/login/index.php
@@ -0,0 +1,92 @@
+<?php namespace HashOver;
+
+// Copyright (C) 2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// HashOver is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// HashOver is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with HashOver. If not, see <http://www.gnu.org/licenses/>.
+
+
+// Redirects the user back to where they came from
+function redirect ($url = '')
+{
+ // Check if we're redirecting to a specific URL
+ if (!empty ($url)) {
+ // If so, use it
+ header ('Location: ' . $url);
+ } else {
+ // If not, check if there is a redirect specified
+ if (!empty ($_GET['redirect'])) {
+ // If so, use it
+ header ('Location: ' . $_GET['redirect']);
+ } else {
+ // If not, redirect to moderation
+ header ('Location: ../moderation/');
+ }
+ }
+
+ // Exit after redirect
+ exit;
+}
+
+try {
+ // View setup
+ require (realpath ('../view-setup.php'));
+
+ // Check if the user submitted login information
+ if (!empty ($_POST['name']) and !empty ($_POST['password'])) {
+ // If so, attempt to log them in
+ $hashover->login->setLogin ();
+
+ // Check if the user is not admin
+ if ($hashover->setup->adminLogin ($hashover->login->loginHash) === false) {
+ // If so, logout
+ $hashover->login->clearLogin ();
+
+ // Sleep 5 seconds
+ sleep (5);
+ }
+
+ // And redirect user to desired view
+ redirect ();
+ }
+
+ // Check if we're logging out
+ if (isset ($_GET['logout'])) {
+ // If so, attempt to log the user out
+ $hashover->login->clearLogin ();
+
+ // And redirect user to main admin page
+ redirect ($hashover->setup->getHttpPath ('admin'));
+ }
+
+ // Template data
+ $template = array (
+ 'title' => $hashover->locale->text['login'],
+ 'logout' => $logout->asHTML ("\t\t\t"),
+ 'sub-title' => $hashover->locale->text['admin-required'],
+ 'name' => $hashover->locale->text['name'],
+ 'password' => $hashover->locale->text['password'],
+ 'email' => $hashover->locale->optionalize ('email'),
+ 'website' => $hashover->locale->optionalize ('website'),
+ 'login' => $hashover->locale->text['login']
+ );
+
+ // Load and parse HTML template
+ echo $hashover->templater->parseTemplate ('login.html', $template);
+
+} catch (\Exception $error) {
+ $misc = new Misc ('php');
+ $message = $error->getMessage ();
+ $misc->displayError ($message);
+}
diff --git a/bootstrap/comments/admin/views/login/login.css b/bootstrap/comments/admin/views/login/login.css
new file mode 100644
index 0000000..37e0547
--- /dev/null
+++ b/bootstrap/comments/admin/views/login/login.css
@@ -0,0 +1,27 @@
+body {
+ background-image: url('../../../images/white-noise.png');
+}
+
+#login {
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ padding: 15px;
+ margin: -133px 0px 0px -150px;
+ width: 300px;
+ border: 2px solid #CCCCCC;
+ background-color: #FFFFFF;
+ overflow: hidden;
+}
+
+#login input {
+ width: 100%;
+}
+
+input[name="name"], input[name="password"] {
+ font-weight: bold;
+}
+
+#login.red {
+ border-color: #FF0000;
+}
diff --git a/bootstrap/comments/admin/views/login/login.html b/bootstrap/comments/admin/views/login/login.html
new file mode 100644
index 0000000..47dbd29
--- /dev/null
+++ b/bootstrap/comments/admin/views/login/login.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+
+<html lang="en" dir="ltr">
+ <head>
+ <title>HashOver - {hashover:title}</title>
+
+ <link type="text/css" href="../../../themes/default/general.css" rel="stylesheet">
+ <link type="text/css" href="../../../themes/default/special.css" rel="stylesheet">
+ <link type="text/css" href="login.css" rel="stylesheet">
+
+ <script type="text/javascript" src="login.js"></script>
+
+ <meta http-equiv="Content-type" content="text/html; charset=utf-8">
+ <meta http-equiv="Content-Language" content="EN">
+
+ <link type="image/x-icon" href="../../../images/favicon.png" rel="shortcut icon">
+ <link type="image/x-icon" href="../../../images/favicon.png" rel="icon">
+ <link href="https://www.gnu.org/licenses/agpl" rel="copyright">
+ </head>
+
+ <body>
+ <div id="login" class="red animate-all">
+ <h1 class="underlined">{hashover:title}</h1>
+
+ <form method="post">
+ <div><input type="text" name="name" placeholder="{hashover:name}"></div>
+ <div><input type="password" name="password" placeholder="{hashover:password}"></div>
+ <div><input type="text" name="email" placeholder="{hashover:email}"></div>
+ <div><input type="text" name="website" placeholder="{hashover:website}"></div>
+
+ <p>
+ <input type="submit" value="{hashover:login}">
+ </p>
+ </form>
+ </div>
+ </body>
+</html>
diff --git a/bootstrap/comments/admin/views/login/login.js b/bootstrap/comments/admin/views/login/login.js
new file mode 100644
index 0000000..a94955c
--- /dev/null
+++ b/bootstrap/comments/admin/views/login/login.js
@@ -0,0 +1,10 @@
+// Wait for the page HTML to be parsed
+document.addEventListener ('DOMContentLoaded', function () {
+ // Get login dialog
+ var login = document.getElementById ('login');
+
+ // Remove red border class
+ setTimeout (function () {
+ login.className = login.className.replace (/red ?/, '');
+ }, 1000);
+}, false);
diff --git a/bootstrap/comments/admin/views/moderation/index.php b/bootstrap/comments/admin/views/moderation/index.php
new file mode 100644
index 0000000..2252389
--- /dev/null
+++ b/bootstrap/comments/admin/views/moderation/index.php
@@ -0,0 +1,88 @@
+<?php namespace HashOver;
+
+// Copyright (C) 2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// HashOver is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// HashOver is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with HashOver. If not, see <http://www.gnu.org/licenses/>.
+
+
+try {
+ // View setup
+ require (realpath ('../view-setup.php'));
+
+ // Create comment thread table
+ $table = new HTMLTag ('table', array (
+ 'id' => 'threads',
+ 'class' => 'striped-rows-odd',
+ 'cellspacing' => '0',
+ 'cellpadding' => '4'
+ ));
+
+ // Get comment threads
+ $threads = $hashover->thread->queryThreads ();
+
+ // Run through comment threads
+ foreach ($threads as $thread) {
+ // Create table row and cell
+ $tr = new HTMLTag ('tr');
+ $td = new HTMLTag ('td');
+
+ // Read and parse JSON metadata file
+ $data = $hashover->thread->data->readMeta ('page-info', $thread);
+
+ // Check if metadata was read successfully
+ if ($data === false or empty ($data['url']) or empty ($data['title'])) {
+ continue;
+ }
+
+ // Create thread hyperlink
+ $thread_link = new HTMLTag ('a', array (
+ 'href' => 'threads.php?' . implode ('&', array (
+ 'thread=' . urlencode ($thread),
+ 'title=' . urlencode ($data['title']),
+ 'url=' . urlencode ($data['url'])
+ )),
+
+ 'innerHTML' => $data['title']
+ ));
+
+ // Append thread hyperlink to cell
+ $td->appendChild ($thread_link);
+
+ // Append page URL to row
+ $td->appendChild (new HTMLTag ('p', new HTMLTag ('small', $data['url'])));
+
+ // Append cell to row
+ $tr->appendChild ($td);
+
+ // Append row to table
+ $table->appendChild ($tr);
+ }
+
+ // Template data
+ $template = array (
+ 'title' => $hashover->locale->text['moderation'],
+ 'logout' => $logout->asHTML ("\t\t\t"),
+ 'sub-title' => $hashover->locale->text['moderation-sub'],
+ 'threads' => $table->asHTML ("\t\t")
+ );
+
+ // Load and parse HTML template
+ echo $hashover->templater->parseTemplate ('moderation.html', $template);
+
+} catch (\Exception $error) {
+ $misc = new Misc ('php');
+ $message = $error->getMessage ();
+ $misc->displayError ($message);
+}
diff --git a/bootstrap/comments/admin/views/moderation/moderation.css b/bootstrap/comments/admin/views/moderation/moderation.css
new file mode 100644
index 0000000..ec6c389
--- /dev/null
+++ b/bootstrap/comments/admin/views/moderation/moderation.css
@@ -0,0 +1,3 @@
+#threads small {
+ color: #5AB3FA;
+}
diff --git a/bootstrap/comments/admin/views/moderation/moderation.html b/bootstrap/comments/admin/views/moderation/moderation.html
new file mode 100644
index 0000000..a9d9df7
--- /dev/null
+++ b/bootstrap/comments/admin/views/moderation/moderation.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+
+<html lang="en" dir="ltr">
+ <head>
+ <title>HashOver - {hashover:title}</title>
+
+ <link type="text/css" href="../../../themes/default/general.css" rel="stylesheet">
+ <link type="text/css" href="../../../themes/default/special.css" rel="stylesheet">
+ <link type="text/css" href="moderation.css" rel="stylesheet">
+
+ <meta http-equiv="Content-type" content="text/html; charset=utf-8">
+ <meta http-equiv="Content-Language" content="EN">
+
+ <link type="image/x-icon" href="../../../images/favicon.png" rel="shortcut icon">
+ <link type="image/x-icon" href="../../../images/favicon.png" rel="icon">
+ <link href="https://www.gnu.org/licenses/agpl" rel="copyright">
+ </head>
+
+ <body>
+ <h1 class="underlined">
+ {hashover:title}
+
+ {hashover:logout}
+ </h1>
+
+ <p class="muted-text">{hashover:sub-title}</p>
+
+ {hashover:threads}
+ </body>
+</html>
diff --git a/bootstrap/comments/admin/views/moderation/threads.css b/bootstrap/comments/admin/views/moderation/threads.css
new file mode 100644
index 0000000..48258da
--- /dev/null
+++ b/bootstrap/comments/admin/views/moderation/threads.css
@@ -0,0 +1,27 @@
+.special a,
+.special a:link {
+ display: inline-block;
+ border-bottom: 0.1em solid transparent;
+ margin-bottom: -0.1em;
+}
+
+.special a:hover {
+ border-bottom-color: #266394;
+}
+
+#loading {
+ display: block ! important;
+ position: absolute;
+ top: 50%;
+ left: 0px;
+ width: 100%;
+ margin-top: -15px;
+}
+
+#loading img {
+ vertical-align: top;
+}
+
+.hashover > div {
+ display: none;
+}
diff --git a/bootstrap/comments/admin/views/moderation/threads.html b/bootstrap/comments/admin/views/moderation/threads.html
new file mode 100644
index 0000000..68c57a5
--- /dev/null
+++ b/bootstrap/comments/admin/views/moderation/threads.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+
+<html lang="en" dir="ltr">
+ <head>
+ <title>HashOver - {hashover:title}</title>
+
+ <link type="text/css" href="../../../themes/default/general.css" rel="stylesheet">
+ <link type="text/css" href="threads.css" rel="stylesheet">
+
+ <meta http-equiv="Content-type" content="text/html; charset=utf-8">
+ <meta http-equiv="Content-Language" content="EN">
+
+ <link type="image/x-icon" href="../../../images/favicon.png" rel="shortcut icon">
+ <link type="image/x-icon" href="../../../images/favicon.png" rel="icon">
+ <link href="https://www.gnu.org/licenses/agpl" rel="copyright">
+ </head>
+
+ <body>
+ <h1 class="underlined special">
+ &#8592; <a href="../moderation/">{hashover:title}</a>
+
+ {hashover:logout}
+ </h1>
+
+ <div id="hashover">
+ <center id="loading">
+ <img src="../../../images/loading-ltr.gif" alt="Loading..." width="90" height="30">
+ </center>
+ </div>
+
+ <script type="text/javascript" src="../../../comments.php?nodefault"></script>
+ <script type="text/javascript" src="threads.js"></script>
+ </body>
+</html>
diff --git a/bootstrap/comments/admin/views/moderation/threads.js b/bootstrap/comments/admin/views/moderation/threads.js
new file mode 100644
index 0000000..851cad3
--- /dev/null
+++ b/bootstrap/comments/admin/views/moderation/threads.js
@@ -0,0 +1,21 @@
+var fullQueries = window.location.search.substr (1);
+var queries = fullQueries.split ('&');
+var options = {};
+
+// Parse URL queries as HashOver options
+for (var i = 0, il = queries.length; i < il; i++) {
+ var queryParts = queries[i].split ('=');
+ var queryName = queryParts[0];
+ var queryValue = queryParts[1];
+
+ if (queryName && queryValue) {
+ queryValue = queryValue.replace (/\+/g, '%20');
+ queryValue = decodeURIComponent (queryValue);
+
+ // Set option
+ options[queryName] = queryValue;
+ }
+}
+
+// Instantiate HashOver
+var hashover = new HashOver (options);
diff --git a/bootstrap/comments/admin/views/moderation/threads.php b/bootstrap/comments/admin/views/moderation/threads.php
new file mode 100644
index 0000000..b22c692
--- /dev/null
+++ b/bootstrap/comments/admin/views/moderation/threads.php
@@ -0,0 +1,33 @@
+<?php namespace HashOver;
+
+// Copyright (C) 2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// HashOver is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// HashOver is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with HashOver. If not, see <http://www.gnu.org/licenses/>.
+
+
+try {
+ // View setup
+ require (realpath ('../view-setup.php'));
+
+ // Load and parse HTML template
+ echo $hashover->templater->parseTemplate ('threads.html', array (
+ 'title' => $hashover->locale->text['back'],
+ 'logout' => $logout->asHTML ("\t\t\t")
+ ));
+} catch (\Exception $error) {
+ $misc = new Misc ('php');
+ $message = $error->getMessage ();
+ $misc->displayError ($message);
+}
diff --git a/bootstrap/comments/admin/views/settings/index.php b/bootstrap/comments/admin/views/settings/index.php
new file mode 100644
index 0000000..37a72b3
--- /dev/null
+++ b/bootstrap/comments/admin/views/settings/index.php
@@ -0,0 +1,515 @@
+<?php namespace HashOver;
+
+// Copyright (C) 2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// HashOver is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// HashOver is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with HashOver. If not, see <http://www.gnu.org/licenses/>.
+
+
+// Generates an array of various settings information
+function ui_array (Setup $setup)
+{
+ // Theme names
+ $themes = array ();
+
+ // Themes directory
+ $themes_directory = $setup->getAbsolutePath ('themes');
+
+ // Get each theme directory name
+ foreach (glob ($themes_directory . '/*', GLOB_ONLYDIR) as $directory) {
+ $theme = basename ($directory);
+ $themes[$theme] = $theme;
+ }
+
+ // Return array of settings allowed to be changed
+ return array (
+ 'language' => array (
+ 'type' => 'select',
+ 'value' => $setup->language,
+
+ 'options' => array (
+ 'auto' => 'auto',
+ 'en' => 'English',
+ 'da' => 'Danish',
+ 'el' => 'Greek',
+ 'de' => 'German',
+ 'es' => 'Spanish',
+ 'fa' => 'Persian',
+ 'fr' => 'French',
+ 'jp' => 'Japanese',
+ 'ko' => 'Korean',
+ 'lt' => 'Lithuanian',
+ 'nl' => 'Dutch',
+ 'pl' => 'Polish',
+ 'pt-br' => 'Brazilian Portuguese',
+ 'ro' => 'Romanian',
+ 'tr' => 'Turkish',
+ 'zh-cn' => 'Simplified Chinese'
+ )
+ ),
+ 'theme' => array (
+ 'type' => 'select',
+ 'value' => $setup->theme,
+ 'options' => $themes
+ ),
+ 'uses-moderation' => array (
+ 'type' => 'checkbox',
+ 'value' => $setup->usesModeration
+ ),
+ 'pends-user-edits' => array (
+ 'type' => 'checkbox',
+ 'value' => $setup->pendsUserEdits
+ ),
+ 'data-format' => array (
+ 'type' => 'select',
+ 'value' => $setup->dataFormat,
+
+ 'options' => array (
+ 'xml' => 'XML',
+ 'json' => 'JSON',
+ 'sql' => 'SQL'
+ )
+ ),
+ 'default-name' => array (
+ 'type' => 'text',
+ 'value' => $setup->defaultName
+ ),
+ 'allows-images' => array (
+ 'type' => 'checkbox',
+ 'value' => $setup->allowsImages
+ ),
+ 'allows-login' => array (
+ 'type' => 'checkbox',
+ 'value' => $setup->allowsLogin
+ ),
+ 'allows-likes' => array (
+ 'type' => 'checkbox',
+ 'value' => $setup->allowsLikes
+ ),
+ 'allows-dislikes' => array (
+ 'type' => 'checkbox',
+ 'value' => $setup->allowsDislikes
+ ),
+ 'uses-ajax' => array (
+ 'type' => 'checkbox',
+ 'value' => $setup->usesAjax
+ ),
+ 'collapses-interface' => array (
+ 'type' => 'checkbox',
+ 'value' => $setup->collapsesInterface
+ ),
+ 'collapses-comments' => array (
+ 'type' => 'checkbox',
+ 'value' => $setup->collapsesComments
+ ),
+ 'collapse-limit' => array (
+ 'type' => 'number',
+ 'value' => $setup->collapseLimit
+ ),
+ 'reply-mode' => array (
+ 'type' => 'select',
+ 'value' => $setup->replyMode,
+
+ 'options' => array (
+ 'thread' => 'Threaded',
+ 'stream' => 'Stream'
+ )
+ ),
+ 'stream-depth' => array (
+ 'type' => 'number',
+ 'value' => $setup->streamDepth
+ ),
+ 'popularity-threshold' => array (
+ 'type' => 'number',
+ 'value' => $setup->popularityThreshold
+ ),
+ 'popularity-limit' => array (
+ 'type' => 'number',
+ 'value' => $setup->popularityLimit
+ ),
+ 'uses-markdown' => array (
+ 'type' => 'checkbox',
+ 'value' => $setup->usesMarkdown
+ ),
+ 'server-timezone' => array (
+ 'type' => 'text',
+ 'value' => $setup->serverTimezone
+ ),
+ 'uses-user-timezone' => array (
+ 'type' => 'checkbox',
+ 'value' => $setup->usesUserTimezone
+ ),
+ 'uses-short-dates' => array (
+ 'type' => 'checkbox',
+ 'value' => $setup->usesShortDates
+ ),
+ 'time-format' => array (
+ 'documentation' => 'http://php.net/manual/en/function.date.php',
+ 'type' => 'text',
+ 'value' => $setup->timeFormat
+ ),
+ 'date-format' => array (
+ 'documentation' => 'http://php.net/manual/en/function.date.php',
+ 'type' => 'text',
+ 'value' => $setup->dateFormat
+ ),
+ 'displays-title' => array (
+ 'type' => 'checkbox',
+ 'value' => $setup->displaysTitle
+ ),
+ 'form-position' => array (
+ 'type' => 'select',
+ 'value' => $setup->formPosition,
+
+ 'options' => array (
+ 'top' => 'Top',
+ 'bottom' => 'Bottom'
+ )
+ ),
+ 'uses-auto-login' => array (
+ 'type' => 'checkbox',
+ 'value' => $setup->usesAutoLogin
+ ),
+ 'shows-reply-count' => array (
+ 'type' => 'checkbox',
+ 'value' => $setup->showsReplyCount
+ ),
+ 'count-includes-deleted' => array (
+ 'type' => 'checkbox',
+ 'value' => $setup->countIncludesDeleted
+ ),
+ 'icon-mode' => array (
+ 'type' => 'select',
+ 'value' => $setup->iconMode,
+
+ 'options' => array (
+ 'image' => 'Image',
+ 'count' => 'Count',
+ 'none' => 'None'
+ )
+ ),
+ 'icon-size' => array (
+ 'type' => 'number',
+ 'value' => $setup->iconSize
+ ),
+ 'image-format' => array (
+ 'type' => 'select',
+ 'value' => $setup->imageFormat,
+
+ 'options' => array (
+ 'png' => 'PNG',
+ 'svg' => 'SVG'
+ )
+ ),
+ 'uses-labels' => array (
+ 'type' => 'checkbox',
+ 'value' => $setup->usesLabels
+ ),
+ 'uses-cancel-buttons' => array (
+ 'type' => 'checkbox',
+ 'value' => $setup->usesCancelButtons
+ ),
+ 'appends-css' => array (
+ 'type' => 'checkbox',
+ 'value' => $setup->appendsCss
+ ),
+ 'appends-rss' => array (
+ 'type' => 'checkbox',
+ 'value' => $setup->appendsRss
+ ),
+ 'login-method' => array (
+ 'type' => 'select',
+ 'value' => $setup->loginMethod,
+ 'options' => array ('defaultLogin' => 'Default Login')
+ ),
+ 'sets-cookies' => array (
+ 'type' => 'checkbox',
+ 'value' => $setup->setsCookies
+ ),
+ 'secure-cookies' => array (
+ 'type' => 'checkbox',
+ 'value' => $setup->secureCookies
+ ),
+ 'stores-ip-address' => array (
+ 'type' => 'checkbox',
+ 'value' => $setup->storesIpAddress
+ ),
+ 'subscribes-user' => array (
+ 'type' => 'checkbox',
+ 'value' => $setup->subscribesUser
+ ),
+ 'allows-user-replies' => array (
+ 'type' => 'checkbox',
+ 'value' => $setup->allowsUserReplies
+ ),
+ 'noreply-email' => array (
+ 'type' => 'text',
+ 'value' => $setup->noreplyEmail
+ ),
+ 'spam-batabase' => array (
+ 'type' => 'select',
+ 'value' => $setup->spamDatabase,
+
+ 'options' => array (
+ 'remote' => 'StopForumSpam.com',
+ 'local' => 'Local CSV file'
+ )
+ ),
+ 'spam-check-modes' => array (
+ 'type' => 'select',
+ 'value' => $setup->spamCheckModes,
+
+ 'options' => array (
+ 'json' => 'JSON',
+ 'php' => 'PHP',
+ 'both' => 'Both'
+ )
+ ),
+ 'gravatar-force' => array (
+ 'type' => 'checkbox',
+ 'value' => $setup->gravatarForce
+ ),
+ 'gravatar-default' => array (
+ 'type' => 'select',
+ 'value' => $setup->gravatarDefault,
+
+ 'options' => array (
+ 'custom' => 'Custom',
+ 'identicon' => 'Identicon',
+ 'monsterid' => 'Monsterid',
+ 'wavatar' => 'Wavatar',
+ 'retro' => 'Retro'
+ )
+ ),
+ 'minifies-javascript' => array (
+ 'type' => 'checkbox',
+ 'value' => $setup->minifiesJavascript
+ ),
+ 'minify-level' => array (
+ 'type' => 'select',
+ 'cast' => 'number',
+ 'value' => $setup->minifyLevel,
+
+ 'options' => array (
+ 1 => 'Basic (removes code comments)',
+ 2 => 'Low (removes whitespace + Basic)',
+ 3 => 'Medium (removes newlines + Low)',
+ 4 => 'High (removes extra bits + Medium)'
+ )
+ ),
+ 'allow-local-metadata' => array (
+ 'type' => 'checkbox',
+ 'value' => $setup->allowLocalMetadata
+ )
+ );
+}
+
+try {
+ // View setup
+ require (realpath ('../view-setup.php'));
+
+ // Get array of UI elements to create
+ $ui = ui_array ($hashover->setup);
+
+ // Check if the form has been submitted
+ if (isset ($_POST['save'])) {
+ // Settings JSON file path
+ $settings_file = $hashover->setup->getAbsolutePath ('config/settings.json');
+
+ // Read JSON settings file
+ $json = $data_files->readJSON ($settings_file);
+
+ // Existing JSON settings or an empty array
+ $settings = ($json !== false) ? $json : array ();
+
+ // Run through configurable settings
+ foreach ($ui as $name => $setting) {
+ // Use specified type or optional cast
+ $type = !empty ($setting['cast']) ? 'cast' : 'type';
+
+ switch ($setting[$type]) {
+ // Set value to boolean based on POST data
+ case 'checkbox': {
+ $settings[$name] = isset ($_POST[$name]);
+ break;
+ }
+
+ // Cast number values to integers
+ case 'number': {
+ $settings[$name] = (int)($_POST[$name]);
+ break;
+ }
+
+ // All other values are strings
+ default: {
+ $settings[$name] = (string)($_POST[$name]);
+ break;
+ }
+ }
+ }
+
+ // Save the settings to the JSON settings file
+ if ($hashover->setup->verifyAdmin ($hashover->login->password)
+ and $data_files->saveJSON ($settings_file, $settings))
+ {
+ // Redirect with success indicator
+ header ('Location: index.php?status=success');
+ } else {
+ // Redirect with failure indicator
+ header ('Location: index.php?status=failure');
+ }
+
+ // Exit after redirect
+ exit;
+ }
+
+ // Otherwise, create settings table
+ $table = new HTMLTag ('table', array (
+ 'id' => 'settings',
+ 'class' => 'p-spaced',
+ 'cellspacing' => '0',
+ 'cellpadding' => '4'
+ ));
+
+ // Create settings table
+ foreach ($ui as $name => $setting) {
+ // Create table row
+ $tr = new HTMLTag ('tr');
+
+ // Create setting description cell
+ $description = new HTMLTag ('td');
+
+ // Create description label
+ $label = new HTMLTag ('label', array (
+ 'for' => $name,
+ 'innerHTML' => $hashover->locale->text['setting-' . $name]
+ ), false);
+
+ // Check for documentation URL
+ if (!empty ($setting['documentation'])) {
+ // Create documentation link
+ $docs = new HTMLTag ('a', array (
+ 'href' => $setting['documentation'],
+ 'target' => '_blank',
+ 'innerHTML' => mb_strtolower ($hashover->locale->text['documentation'])
+ ), false);
+
+ // Append documentation in parentheses
+ $label->appendInnerHTML ('(' . $docs->asHTML () . ')');
+ }
+
+ // Append label to description cell
+ $description->appendChild ($label);
+
+ // Append description cell to settings table row
+ $tr->appendChild ($description);
+
+ // Create setting value cell
+ $field = new HTMLTag ('td');
+
+ switch ($setting['type']) {
+ case 'checkbox': {
+ // Create checkbox for enabling/disabling the setting
+ $element = new HTMLTag ('input', array (
+ 'id' => $name,
+ 'type' => 'checkbox',
+ 'name' => $name
+ ), false, true);
+
+ // Set check based on current setting
+ if ($setting['value'] !== false) {
+ $element->createAttribute ('checked', 'true');
+ }
+
+ break;
+ }
+
+ // Create text/number box for entering the setting value
+ case 'number' : case 'text': {
+ $element = new HTMLTag ('input', array (
+ 'id' => $name,
+ 'type' => $setting['type'],
+ 'name' => $name,
+ 'value' => $setting['value'],
+ 'size' => ($setting['type'] === 'text') ? '25' : '10'
+ ), false, true);
+
+ break;
+ }
+
+ // Create dropdown menu for selecting the setting value
+ case 'select': {
+ // Create wrapper element for dropdown menu
+ $element = new HTMLTag ('span', array (
+ 'class' => 'select-wrapper'
+ ));
+
+ // Create dropdown menu
+ $select = new HTMLTag ('select', array (
+ 'id' => $name,
+ 'name' => $name,
+ 'size' => 1
+ ));
+
+ foreach ($setting['options'] as $value => $text) {
+ // Create setting option
+ $option = new HTMLTag ('option', array (
+ 'value' => $value,
+ 'innerHTML' => $text
+ ), false);
+
+ // Select proper option
+ if ($value === $setting['value']) {
+ $option->createAttribute ('selected', 'true');
+ }
+
+ // Append option to menu
+ $select->appendChild ($option);
+ }
+
+ // Append dropdown menu to wrapper element
+ $element->appendChild ($select);
+
+ break;
+ }
+ }
+
+ // Append the setting value element to the setting value cell
+ $field->appendChild ($element);
+
+ // Append the setting value cell to the settings table row
+ $tr->appendChild ($field);
+
+ // Add row to settings table
+ $table->appendChild ($tr);
+ }
+
+ // Template data
+ $template = array (
+ 'title' => $hashover->locale->text['settings'],
+ 'logout' => $logout->asHTML ("\t\t\t"),
+ 'sub-title' => $hashover->locale->text['settings-sub'],
+ 'message' => $form_message,
+ 'settings' => $table->asHTML ("\t\t\t"),
+ 'save-button' => $hashover->locale->text['save']
+ );
+
+ // Load and parse HTML template
+ echo $hashover->templater->parseTemplate ('settings.html', $template);
+
+} catch (\Exception $error) {
+ $misc = new Misc ('php');
+ $message = $error->getMessage ();
+ $misc->displayError ($message);
+}
diff --git a/bootstrap/comments/admin/views/settings/settings.css b/bootstrap/comments/admin/views/settings/settings.css
new file mode 100644
index 0000000..1e1f1da
--- /dev/null
+++ b/bootstrap/comments/admin/views/settings/settings.css
@@ -0,0 +1,15 @@
+#settings {
+ width: 100%;
+}
+
+#settings tr:nth-child(odd) {
+ background-color: #F5F5F5;
+}
+
+input, textarea, select, .select-wrapper {
+ margin: 0px;
+}
+
+table tr {
+ height: 35px;
+}
diff --git a/bootstrap/comments/admin/views/settings/settings.html b/bootstrap/comments/admin/views/settings/settings.html
new file mode 100644
index 0000000..bf4330a
--- /dev/null
+++ b/bootstrap/comments/admin/views/settings/settings.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+
+<html lang="en" dir="ltr">
+ <head>
+ <title>HashOver - {hashover:title}</title>
+
+ <link type="text/css" href="../../../themes/default/general.css" rel="stylesheet">
+ <link type="text/css" href="../../../themes/default/special.css" rel="stylesheet">
+ <link type="text/css" href="settings.css" rel="stylesheet">
+
+ <script type="text/javascript" src="../shared/update-elements.js"></script>
+
+ <meta http-equiv="Content-type" content="text/html; charset=utf-8">
+ <meta http-equiv="Content-Language" content="EN">
+
+ <link type="image/x-icon" href="../../../images/favicon.png" rel="shortcut icon">
+ <link type="image/x-icon" href="../../../images/favicon.png" rel="icon">
+ <link href="https://www.gnu.org/licenses/agpl" rel="copyright">
+ </head>
+
+ <body>
+ <h1 class="underlined">
+ <span id="title">{hashover:title}</span>
+
+ {hashover:logout}
+ </h1>
+
+ <div class="p-spaced muted-text">
+ {hashover:sub-title}
+ {hashover:message}
+ </div>
+
+ <form method="post">
+ {hashover:settings}
+
+ <p>
+ <input id="save-button" type="submit" name="save" value="{hashover:save-button}">
+ </p>
+ </form>
+ </body>
+</html>
diff --git a/bootstrap/comments/admin/views/shared/update-elements.js b/bootstrap/comments/admin/views/shared/update-elements.js
new file mode 100644
index 0000000..a6d2d74
--- /dev/null
+++ b/bootstrap/comments/admin/views/shared/update-elements.js
@@ -0,0 +1,12 @@
+// Wait for the page HTML to be parsed
+document.addEventListener ('DOMContentLoaded', function () {
+ // Get message element
+ var message = document.getElementById ('message');
+
+ // Hide message after 5 seconds
+ if (message !== null) {
+ setTimeout (function () {
+ message.className = message.className.replace ('success', 'hide');
+ }, 1000 * 5);
+ }
+}, false);
diff --git a/bootstrap/comments/admin/views/updates/index.php b/bootstrap/comments/admin/views/updates/index.php
new file mode 100644
index 0000000..660abf1
--- /dev/null
+++ b/bootstrap/comments/admin/views/updates/index.php
@@ -0,0 +1,38 @@
+<?php namespace HashOver;
+
+// Copyright (C) 2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// HashOver is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// HashOver is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with HashOver. If not, see <http://www.gnu.org/licenses/>.
+
+
+try {
+ // View setup
+ require (realpath ('../view-setup.php'));
+
+ // Template data
+ $template = array (
+ 'title' => $hashover->locale->text['check-for-updates'],
+ 'logout' => $logout->asHTML ("\t\t\t"),
+ 'sub-title' => $hashover->locale->text['coming-soon']
+ );
+
+ // Load and parse HTML template
+ echo $hashover->templater->parseTemplate ('updates.html', $template);
+
+} catch (\Exception $error) {
+ $misc = new Misc ('php');
+ $message = $error->getMessage ();
+ $misc->displayError ($message);
+}
diff --git a/bootstrap/comments/admin/views/updates/updates.html b/bootstrap/comments/admin/views/updates/updates.html
new file mode 100644
index 0000000..be82e62
--- /dev/null
+++ b/bootstrap/comments/admin/views/updates/updates.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+
+<html lang="en" dir="ltr">
+ <head>
+ <title>HashOver - {hashover:title}</title>
+
+ <link type="text/css" href="../../../themes/default/general.css" rel="stylesheet">
+ <link type="text/css" href="../../../themes/default/special.css" rel="stylesheet">
+
+ <meta http-equiv="Content-type" content="text/html; charset=utf-8">
+ <meta http-equiv="Content-Language" content="EN">
+
+ <link type="image/x-icon" href="../../../images/favicon.png" rel="shortcut icon">
+ <link type="image/x-icon" href="../../../images/favicon.png" rel="icon">
+ <link href="https://www.gnu.org/licenses/agpl" rel="copyright">
+ </head>
+
+ <body>
+ <h1 class="underlined">
+ {hashover:title}
+
+ {hashover:logout}
+ </h1>
+
+ <p class="muted-text">{hashover:sub-title}</p>
+ </body>
+</html>
diff --git a/bootstrap/comments/admin/views/url-queries/index.php b/bootstrap/comments/admin/views/url-queries/index.php
new file mode 100644
index 0000000..461f62a
--- /dev/null
+++ b/bootstrap/comments/admin/views/url-queries/index.php
@@ -0,0 +1,134 @@
+<?php namespace HashOver;
+
+// Copyright (C) 2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// HashOver is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// HashOver is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with HashOver. If not, see <http://www.gnu.org/licenses/>.
+
+
+try {
+ // View setup
+ require (realpath ('../view-setup.php'));
+
+ // Default URL Query Pair array
+ $ignored_queries = array ();
+
+ // Ignored URL queries JSON file location
+ $ignored_queries_file = $hashover->setup->getAbsolutePath ('config/ignored-queries.json');
+
+ // Check if the form has been submitted
+ if (!empty ($_POST['names']) and is_array ($_POST['names'])
+ and !empty ($_POST['values']) and is_array ($_POST['values']))
+ {
+ // If so, run through submitted queries
+ for ($i = 0, $il = count ($_POST['names']); $i < $il; $i++) {
+ // Add each non-empty query to the pair array
+ if (!empty ($_POST['names'][$i])) {
+ // Start the query pair with the query name
+ $query_pair = $_POST['names'][$i];
+
+ // Check if the query has a value
+ if (!empty ($_POST['values'][$i])) {
+ // If so, add it to the pair
+ $query_pair .= '=' . $_POST['values'][$i];
+ }
+
+ // Add the query pair to the URL Query Pair array
+ $ignored_queries[] = $query_pair;
+ }
+ }
+
+ // Save the JSON data to the URL Query Pairs file
+ if ($hashover->setup->verifyAdmin ($hashover->login->password)
+ and $data_files->saveJSON ($ignored_queries_file, $ignored_queries))
+ {
+ // Redirect with success indicator
+ header ('Location: index.php?status=success');
+ } else {
+ // Redirect with failure indicator
+ header ('Location: index.php?status=failure');
+ }
+
+ // Exit after redirect
+ exit;
+ }
+
+ // Otherwise, load and parse URL Query Pairs file
+ $json = $data_files->readJSON ($ignored_queries_file);
+
+ // Check for JSON parse error
+ if (is_array ($json)) {
+ $ignored_queries = $json;
+ }
+
+ // URL Query Pair inputs
+ $inputs = new HTMLTag ('span');
+
+ // Create URL Query Pair inputs
+ for ($i = 0, $il = max (3, count ($ignored_queries)); $i < $il; $i++) {
+ // Use URL query pairs from file or blank
+ $query = !empty ($ignored_queries[$i]) ? $ignored_queries[$i] : '';
+
+ // Split query pair into name and value
+ $query_parts = explode ('=', $query);
+ $query_name = $query_parts[0];
+ $query_value = !empty ($query_parts[1]) ? $query_parts[1] : '';
+
+ // Create input tag
+ $input = new HTMLTag ('div', array (
+ 'children' => array (
+ new HTMLTag ('input', array (
+ 'class' => 'name',
+ 'type' => 'text',
+ 'name' => 'names[]',
+ 'value' => $query_name,
+ 'size' => '15',
+ 'placeholder' => $hashover->locale->text['name'],
+ 'title' => $hashover->locale->text['url-queries-name-tip']
+ ), false, true),
+
+ new HTMLTag ('input', array (
+ 'class' => 'value',
+ 'type' => 'text',
+ 'name' => 'values[]',
+ 'value' => $query_value,
+ 'size' => '25',
+ 'placeholder' => $hashover->locale->text['value'],
+ 'title' => $hashover->locale->text['url-queries-value-tip']
+ ), false, true)
+ )
+ ));
+
+ // Add input to inputs container
+ $inputs->appendChild ($input);
+ }
+
+ // Template data
+ $template = array (
+ 'title' => $hashover->locale->text['url-queries-title'],
+ 'logout' => $logout->asHTML ("\t\t\t"),
+ 'sub-title' => $hashover->locale->text['url-queries-sub'],
+ 'message' => $form_message,
+ 'inputs' => $inputs->getInnerHTML ("\t\t\t\t"),
+ 'save-button' => $hashover->locale->text['save']
+ );
+
+ // Load and parse HTML template
+ echo $hashover->templater->parseTemplate ('url-queries.html', $template);
+
+} catch (\Exception $error) {
+ $misc = new Misc ('php');
+ $message = $error->getMessage ();
+ $misc->displayError ($message);
+}
diff --git a/bootstrap/comments/admin/views/url-queries/url-queries.html b/bootstrap/comments/admin/views/url-queries/url-queries.html
new file mode 100644
index 0000000..fe350ce
--- /dev/null
+++ b/bootstrap/comments/admin/views/url-queries/url-queries.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+
+<html lang="en" dir="ltr">
+ <head>
+ <title>HashOver - {hashover:title}</title>
+
+ <link type="text/css" href="../../../themes/default/general.css" rel="stylesheet">
+ <link type="text/css" href="../../../themes/default/special.css" rel="stylesheet">
+
+ <script type="text/javascript" src="../shared/update-elements.js"></script>
+ <script type="text/javascript" src="url-queries.js"></script>
+
+ <meta http-equiv="Content-type" content="text/html; charset=utf-8">
+ <meta http-equiv="Content-Language" content="EN">
+
+ <link type="image/x-icon" href="../../../images/favicon.png" rel="shortcut icon">
+ <link type="image/x-icon" href="../../../images/favicon.png" rel="icon">
+ <link href="https://www.gnu.org/licenses/agpl" rel="copyright">
+ </head>
+
+ <body>
+ <h1 class="underlined">
+ <span id="title">{hashover:title}</span>
+
+ {hashover:logout}
+ </h1>
+
+ <div class="p-spaced muted-text">
+ {hashover:sub-title}
+ {hashover:message}
+ </div>
+
+ <form method="post">
+ <div id="queries-list" class="p-spaced">
+ {hashover:inputs}
+ </div>
+
+ <p>
+ <input id="save-button" type="submit" value="{hashover:save-button}">
+ <input id="new-button" type="button" value="+">
+ </p>
+ </form>
+ </body>
+</html>
diff --git a/bootstrap/comments/admin/views/url-queries/url-queries.js b/bootstrap/comments/admin/views/url-queries/url-queries.js
new file mode 100644
index 0000000..6267691
--- /dev/null
+++ b/bootstrap/comments/admin/views/url-queries/url-queries.js
@@ -0,0 +1,36 @@
+// Wait for the page HTML to be parsed
+document.addEventListener ('DOMContentLoaded', function () {
+ // Get the "New Query Pair" and "Save" buttons
+ var newButton = document.getElementById ('new-button');
+ var queriesList = document.getElementById ('queries-list');
+ var saveButton = document.getElementById ('save-button');
+
+ newButton.onclick = function ()
+ {
+ // Create inputs and indentation
+ var div = document.createElement ('div');
+ var names = document.getElementsByClassName ('name');
+ var values = document.getElementsByClassName ('value');
+ var indentation = document.createTextNode ('\n\t\t\t\t\t');
+
+ // Clone the first name and value fields
+ var nameInput = names[0].cloneNode (true);
+ var valueInput = values[0].cloneNode (true);
+
+ // Remove their values
+ nameInput.value = '';
+ valueInput.value = '';
+
+ // Append indentation and input to URL Query Pair list
+ div.appendChild (nameInput);
+ div.appendChild (indentation);
+ div.appendChild (valueInput);
+ queriesList.appendChild (div);
+ };
+
+ // Disable the "Save" button when clicked
+ saveButton.onclick = function ()
+ {
+ this.disabled = true;
+ };
+}, false);
diff --git a/bootstrap/comments/admin/views/view-setup.php b/bootstrap/comments/admin/views/view-setup.php
new file mode 100644
index 0000000..adcf93d
--- /dev/null
+++ b/bootstrap/comments/admin/views/view-setup.php
@@ -0,0 +1,107 @@
+<?php namespace HashOver;
+
+// Copyright (C) 2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// HashOver is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// HashOver is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with HashOver. If not, see <http://www.gnu.org/licenses/>.
+
+
+// Do some standard HashOver setup work
+require (realpath ('../../../backend/standard-setup.php'));
+
+// Autoload class files
+spl_autoload_register (function ($uri) {
+ $uri = str_replace ('\\', '/', strtolower ($uri));
+ $class_name = basename ($uri);
+
+ if (!@include (realpath ('../../../backend/classes/' . $class_name . '.php'))) {
+ echo '"' . $class_name . '.php" file could not be included!';
+ exit;
+ }
+});
+
+// Instantiate HashOver class
+$hashover = new \HashOver ();
+$hashover->initiate ();
+$hashover->finalize ();
+
+// Instantiate FileWriter class
+$data_files = new DataFiles ($hashover->setup);
+
+// Exit if the user isn't logged in as admin
+if ($hashover->login->userIsAdmin !== true) {
+ $uri = $_SERVER['REQUEST_URI'];
+ $uri_parts = explode ('?', $uri);
+
+ if (basename ($uri_parts[0]) !== 'login') {
+ header ('Location: ../login/?redirect=' . urlencode ($uri));
+ exit;
+ }
+}
+
+// Create logout hyperlink
+$logout = new HTMLTag ('span', array (
+ 'class' => 'right',
+
+ 'children' => array (
+ new HTMLTag ('a', array (
+ 'href' => '../login/?logout=true',
+ 'target' => '_parent',
+ 'innerHTML' => $hashover->locale->text['logout']
+ ))
+ )
+));
+
+// Check if the form has been submitted
+if (!empty ($_GET['status'])) {
+ // Check if the form submission was successful
+ if ($_GET['status'] === 'success') {
+ // If so, create message element for success message
+ $message = new HTMLTag ('div', array (
+ 'id' => 'message',
+ 'class' => 'success',
+
+ 'children' => array (
+ new HTMLTag ('p', array (
+ 'innerHTML' => $hashover->locale->text['successful-save']
+ ), false)
+ )
+ ));
+ } else {
+ // If so, create message element for error message
+ $message = new HTMLTag ('div', array (
+ 'id' => 'message',
+ 'class' => 'error',
+
+ 'children' => array (
+ // Main error message
+ new HTMLTag ('p', array (
+ 'innerHTML' => $hashover->locale->text['failed-to-save']
+ ), false),
+
+ // File permissions explanation
+ new HTMLTag ('p', array (
+ 'innerHTML' => $hashover->locale->permissionsInfo ('config')
+ ), false)
+ )
+ ));
+ }
+
+ // Set message as HTML
+ $form_message = $message->asHTML ("\t\t");
+
+} else {
+ // If not, set the message as an empty string
+ $form_message = '';
+}
diff --git a/bootstrap/comments/api/backend/count-link-ajax.php b/bootstrap/comments/api/backend/count-link-ajax.php
new file mode 100644
index 0000000..bf53312
--- /dev/null
+++ b/bootstrap/comments/api/backend/count-link-ajax.php
@@ -0,0 +1,76 @@
+<?php namespace HashOver;
+
+// Copyright (C) 2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// HashOver is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// HashOver is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with HashOver. If not, see <http://www.gnu.org/licenses/>.
+
+
+// Change to the HashOver directory
+chdir (realpath ('../../'));
+
+// Check if request is for JSONP
+if (isset ($_GET['jsonp'])) {
+ // If so, setup HashOver for JavaScript
+ require ('backend/javascript-setup.php');
+} else {
+ // If not, setup HashOver for JSON
+ require ('backend/json-setup.php');
+}
+
+try {
+ // Instantiate HashOver class
+ $hashover = new \HashOver ('json', 'api');
+ $hashover->setup->setPageURL ('request');
+ $hashover->initiate ();
+ $hashover->finalize ();
+
+ // Throw exception if the "Latest Comments" API is disabled
+ if ($hashover->setup->apiStatus ('count-link') === 'disabled') {
+ throw new \Exception ('This API is not enabled.');
+ }
+
+ // Count response array
+ $data = array (
+ 'primary-count' => $hashover->thread->primaryCount - 1,
+ 'total-count' => $hashover->thread->totalCount - 1
+ );
+
+ // Check if there are any comments
+ if ($hashover->thread->totalCount > 1) {
+ // If so, set the count link text to the comment count
+ $data['link-text'] = $hashover->getCommentCount ();
+ } else {
+ // If not, set the count link text to "Post Comment"
+ $data['link-text'] = $hashover->locale->text['post-button'];
+ }
+
+ // Generate statistics
+ $hashover->statistics->executionEnd ();
+
+ // HashOver statistics
+ $data['statistics'] = array (
+ 'execution-time' => $hashover->statistics->executionTime,
+ 'script-memory' => $hashover->statistics->scriptMemory,
+ 'system-memory' => $hashover->statistics->systemMemory
+ );
+
+ // Encode JSON data
+ echo $hashover->misc->jsonData ($data);
+
+} catch (\Exception $error) {
+ $misc = new Misc ('json');
+ $message = $error->getMessage ();
+ $misc->displayError ($message);
+}
diff --git a/bootstrap/comments/api/backend/latest-ajax.php b/bootstrap/comments/api/backend/latest-ajax.php
new file mode 100644
index 0000000..964fe34
--- /dev/null
+++ b/bootstrap/comments/api/backend/latest-ajax.php
@@ -0,0 +1,207 @@
+<?php namespace HashOver;
+
+// Copyright (C) 2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// HashOver is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// HashOver is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with HashOver. If not, see <http://www.gnu.org/licenses/>.
+
+
+// Change to the HashOver directory
+chdir (realpath ('../../'));
+
+// Check if request is for JSONP
+if (isset ($_GET['jsonp'])) {
+ // If so, setup HashOver for JavaScript
+ require ('backend/javascript-setup.php');
+} else {
+ // If not, setup HashOver for JSON
+ require ('backend/json-setup.php');
+}
+
+try {
+ // Instantiate HashOver class
+ $hashover = new \HashOver ('json', 'api');
+ $hashover->setup->setThreadName ('request');
+ $hashover->initiate ();
+ $hashover->finalize ();
+
+ // Throw exception if the "Latest Comments" API is disabled
+ if ($hashover->setup->apiStatus ('latest') === 'disabled') {
+ throw new \Exception ('This API is not enabled.');
+ }
+
+ // Comments and statistics response array
+ $data = array ();
+
+ // Add locales to data
+ $data['locale'] = array (
+ 'date-time' => $hashover->locale->text['date-time'],
+ 'dislike' => $hashover->locale->text['dislike'],
+ 'external-image-tip' => $hashover->locale->text['external-image-tip'],
+ 'like' => $hashover->locale->text['like'],
+ 'today' => $hashover->locale->text['date-today'],
+ 'commenter-tip' => $hashover->locale->text['commenter-tip'],
+ 'subscribed-tip' => $hashover->locale->text['subscribed-tip'],
+ 'unsubscribed-tip' => $hashover->locale->text['unsubscribed-tip'],
+ 'replies' => $hashover->locale->text['replies'],
+ 'reply' => $hashover->locale->text['reply'],
+ 'loading' => $hashover->locale->text['loading'],
+ 'click-to-close' => $hashover->locale->text['click-to-close'],
+ 'day-names' => $hashover->locale->text['date-day-names'],
+ 'month-names' => $hashover->locale->text['date-month-names']
+ );
+
+ // Add setup information to data
+ $data['setup'] = array (
+ 'server-eol' => PHP_EOL,
+ 'default-name' => $hashover->setup->defaultName,
+ 'user-is-logged-in' => $hashover->login->userIsLoggedIn,
+ 'time-format' => $hashover->setup->timeFormat,
+ 'image-extensions' => $hashover->setup->imageTypes,
+ 'image-placeholder' => $hashover->setup->getImagePath ('place-holder'),
+ 'theme-css' => $hashover->setup->getThemePath ('latest.css'),
+ 'device-type' => ($hashover->setup->isMobile === true) ? 'mobile' : 'desktop',
+ 'uses-user-timezone' => $hashover->setup->usesUserTimezone,
+ 'uses-short-dates' => $hashover->setup->usesShortDates,
+ 'allows-images' => $hashover->setup->allowsImages,
+ 'uses-markdown' => $hashover->setup->usesMarkdown
+ );
+
+ // Add UI HTML to data
+ $data['ui'] = array (
+ 'user-avatar' => $hashover->ui->userAvatar (),
+ 'name-link' => $hashover->ui->nameElement ('a'),
+ 'name-span' => $hashover->ui->nameElement ('span'),
+ 'thread-link' => $hashover->ui->threadLink (),
+ 'reply-link' => $hashover->ui->formLink ('{{href}}', 'reply'),
+ 'like-count' => $hashover->ui->likeCount ('likes'),
+ 'dislike-count' => $hashover->ui->likeCount ('dislikes'),
+ 'name-wrapper' => $hashover->ui->nameWrapper (),
+ 'date-link' => $hashover->ui->dateLink (),
+ 'comment-wrapper' => $hashover->ui->commentWrapper (),
+ 'theme' => $hashover->templater->parseTheme ('latest.html')
+ );
+
+ // Attempt to get comment thread from GET/POST data
+ $get_thread = $hashover->setup->getRequest ('thread', 'auto');
+
+ // Check if we're getting metadata for a specific thread
+ if ($get_thread !== 'auto') {
+ // If so, attempt to read thread-specific latest comments metadata
+ $latest = $hashover->thread->data->readMeta ('latest-comments', $get_thread);
+ } else {
+ // If not, attempt to read global latest comments metadata
+ $latest = $hashover->thread->data->readMeta ('latest-comments', 'auto', true);
+ }
+
+ // Check if the latest comments read successfully
+ if ($latest !== false) {
+ // If so, reduce number of latest comments to configured limit
+ $latest = array_slice ($latest, 0, $hashover->setup->latestMax);
+ } else {
+ // If not, set to empty array
+ $latest = array ();
+ }
+
+ // Latest comments
+ $comments = array ();
+
+ // Run through the latest comments
+ foreach ($latest as $item) {
+ // Get comment key
+ $key = basename ($item);
+ $key_parts = explode ('-', $key);
+
+ // Decide proper thread
+ $thread = ($get_thread === 'auto') ? dirname ($item) : $get_thread;
+
+ // Attempt to read page information metadata
+ $page_info = $hashover->thread->data->readMeta ('page-info', $thread);
+
+ // Attempt to read comment
+ $raw = $hashover->thread->data->read ($key, $thread);
+
+ // Skip failed or unapproved comments or missing metadata
+ if ((!empty ($raw['status']) and $raw['status'] !== 'approved')
+ or ($raw and $page_info) === false)
+ {
+ continue;
+ }
+
+ // Parse comment
+ $comment = $hashover->commentParser->parse ($raw, $key, $key_parts);
+
+ // Merge comment with page metadata
+ $comment = array_merge ($page_info, $comment);
+
+ // Trim comment body to configurable length
+ if ($hashover->setup->latestTrimWidth > 0) {
+ // Instantiate WriteComments
+ //
+ // TODO: Split WriteComments into multiple classes so we
+ // can instantiate only the functionality that we need
+ //
+ $write_comments = new WriteComments (
+ $hashover->setup,
+ $hashover->thread
+ );
+
+ // Shorthands
+ $trim_length = $hashover->setup->latestTrimWidth;
+ $close_tags = $write_comments->closeTags;
+
+ // Add <code> to list of tags to close
+ $write_comments->closeTags[] = 'code';
+
+ // Trim the comment to configurable length
+ $body = rtrim (mb_strimwidth ($comment['body'], 0, $trim_length, '...'));
+
+ // Close any tags that may have had their endings trimmed off
+ $body = $write_comments->tagCloser ($close_tags, $body);
+
+ // Escape any HTML tags that may have been trimmed in half
+ $body = $write_comments->htmlSelectiveEscape ($body);
+
+ // Update the comment
+ $comment['body'] = $body;
+ }
+
+ // Add comment to response array
+ $comments[] = $comment;
+ }
+
+ // HashOver instance information
+ $data['instance'] = array (
+ 'comments' => array ('primary' => $comments),
+ 'total-count' => count ($comments)
+ );
+
+ // Generate statistics
+ $hashover->statistics->executionEnd ();
+
+ // HashOver statistics
+ $data['statistics'] = array (
+ 'execution-time' => $hashover->statistics->executionTime,
+ 'script-memory' => $hashover->statistics->scriptMemory,
+ 'system-memory' => $hashover->statistics->systemMemory
+ );
+
+ // Encode JSON data
+ echo $hashover->misc->jsonData ($data);
+
+} catch (\Exception $error) {
+ $misc = new Misc ('json');
+ $message = $error->getMessage ();
+ $misc->displayError ($message);
+}
diff --git a/bootstrap/comments/api/count-link.php b/bootstrap/comments/api/count-link.php
new file mode 100644
index 0000000..f3da768
--- /dev/null
+++ b/bootstrap/comments/api/count-link.php
@@ -0,0 +1,95 @@
+<?php namespace HashOver;
+
+// Copyright (C) 2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// HashOver is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// HashOver is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with HashOver. If not, see <http://www.gnu.org/licenses/>.
+
+
+// Change to the HashOver directory
+chdir (realpath ('../'));
+
+// Setup HashOver for JavaScript
+require ('backend/javascript-setup.php');
+
+try {
+ // Instantiate general setup class
+ $setup = new Setup (array (
+ 'mode' => 'javascript',
+ 'context' => 'api'
+ ));
+
+ // Throw exception if the "Latest Comments" API is disabled
+ if ($setup->apiStatus ('count-link') === 'disabled') {
+ throw new \Exception ('This API is not enabled.');
+ }
+
+ // Instantiate HashOver statistics class
+ $statistics = new Statistics ('javascript');
+
+ // Start execution timer
+ $statistics->executionStart ();
+
+ // Instantiate JavaScript build class
+ $javascript = new JavaScriptBuild ('api/frontends/count-link');
+
+ // Register initial constructor
+ $javascript->registerFile ('constructor.js');
+
+ // Register comment count AJAX request getter method
+ $javascript->registerFile ('getcommentcount.js');
+
+ // Change to standard frontend directory
+ $javascript->changeDirectory ('frontend');
+
+ // Register HashOver script tag getter method
+ $javascript->registerFile ('script.js');
+
+ // Register backend path setter
+ $javascript->registerFile ('backendpath.js');
+
+ // Register HashOver ready state detection method
+ $javascript->registerFile ('onready.js');
+
+ // Register element creation methods
+ $javascript->registerFile ('elements.js');
+
+ // Register AJAX-related methods
+ $javascript->registerFile ('ajax.js');
+
+ // Change back to count link frontend directory
+ $javascript->changeDirectory ('api/frontends/count-link');
+
+ // Register automatic instantiation code
+ $javascript->registerFile ('instantiate.js', array (
+ 'include' => !isset ($_GET['nodefault'])
+ ));
+
+ // JavaScript build process output
+ $output = $javascript->build (
+ $setup->minifiesJavascript,
+ $setup->minifyLevel
+ );
+
+ // Display JavaScript build process output
+ echo $output, PHP_EOL;
+
+ // Display statistics
+ echo $statistics->executionEnd ();
+
+} catch (\Exception $error) {
+ $misc = new Misc ('javascript');
+ $message = $error->getMessage ();
+ $misc->displayError ($message);
+}
diff --git a/bootstrap/comments/api/frontends/count-link/constructor.js b/bootstrap/comments/api/frontends/count-link/constructor.js
new file mode 100644
index 0000000..7473788
--- /dev/null
+++ b/bootstrap/comments/api/frontends/count-link/constructor.js
@@ -0,0 +1,43 @@
+// @licstart The following is the entire license notice for the
+// JavaScript code in this page.
+//
+// Copyright (C) 2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// HashOver is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// HashOver is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with HashOver. If not, see <http://www.gnu.org/licenses/>.
+//
+// @licend The above is the entire license notice for the
+// JavaScript code in this page.
+
+"use strict";
+
+// Count link API frontend constructor (constructor.js)
+function HashOverCountLink ()
+{
+ // Get count link class elements
+ var countLinks = document.getElementsByClassName ('hashover-count-link');
+
+ // Run through the count links
+ for (var i = 0, il = countLinks.length; i < il; i++) {
+ var link = countLinks[i];
+
+ // Instantiate new count link object
+ this.getCommentCount (link, {
+ url: link.href
+ });
+ }
+};
+
+// Constructor to add HashOver methods to
+var HashOverConstructor = HashOverCountLink;
diff --git a/bootstrap/comments/api/frontends/count-link/getcommentcount.js b/bootstrap/comments/api/frontends/count-link/getcommentcount.js
new file mode 100644
index 0000000..536824c
--- /dev/null
+++ b/bootstrap/comments/api/frontends/count-link/getcommentcount.js
@@ -0,0 +1,19 @@
+// Send AJAX request to backend for a comment count (getcommentcount.js)
+HashOverCountLink.prototype.getCommentCount = function (link, options)
+{
+ // Reference to this HashOver object
+ var hashover = this;
+
+ // Get backend queries
+ var queries = ['url=' + options.url];
+
+ // Backend request path
+ var requestPath = '/countlinkajax';
+
+ // Handle backend request
+ this.ajax ('POST', requestPath, queries, function (json) {
+ if (json['link-text'] !== undefined) {
+ link.textContent = json['link-text'];
+ }
+ }, true);
+};
diff --git a/bootstrap/comments/api/frontends/count-link/instantiate.js b/bootstrap/comments/api/frontends/count-link/instantiate.js
new file mode 100644
index 0000000..9329ca6
--- /dev/null
+++ b/bootstrap/comments/api/frontends/count-link/instantiate.js
@@ -0,0 +1,4 @@
+// Instantiate after the DOM is parsed (instantiate.js)
+HashOverCountLink.onReady (function () {
+ window.hashoverCountLink = new HashOverCountLink ();
+});
diff --git a/bootstrap/comments/api/frontends/latest/addcontrols.js b/bootstrap/comments/api/frontends/latest/addcontrols.js
new file mode 100644
index 0000000..b5642f1
--- /dev/null
+++ b/bootstrap/comments/api/frontends/latest/addcontrols.js
@@ -0,0 +1,22 @@
+// Add various events to various elements in each comment (addcontrols.js)
+HashOverLatest.prototype.addControls = function (json, popular)
+{
+ // Reference to this object
+ var hashover = this;
+
+ // Get permalink from JSON object
+ var permalink = json.permalink;
+
+ // Set onclick functions for external images
+ if (this.setup['allows-images'] !== false) {
+ // Get embedded image elements
+ var embeddedImgs = document.getElementsByClassName ('hashover-embedded-image');
+
+ for (var i = 0, il = embeddedImgs.length; i < il; i++) {
+ embeddedImgs[i].onclick = function ()
+ {
+ hashover.openEmbeddedImage (this);
+ };
+ }
+ }
+};
diff --git a/bootstrap/comments/api/frontends/latest/addratings.js b/bootstrap/comments/api/frontends/latest/addratings.js
new file mode 100644
index 0000000..b4d7c8e
--- /dev/null
+++ b/bootstrap/comments/api/frontends/latest/addratings.js
@@ -0,0 +1,22 @@
+// Add Like/Dislike link and count to template (addratings.js)
+HashOverLatest.prototype.comments.addRatings = function (comment, template, action, commentKey)
+{
+ // Reference to the parent object
+ var hashover = this.parent;
+
+ // Check if the comment has been likes/dislikes
+ if (comment[action + 's'] !== undefined) {
+ // Add likes/dislikes to HTML template
+ template[action + 's'] = comment[action + 's'];
+
+ // Get "X Like/Dislike(s)" locale
+ var plural = (comment[action + 's'] === 1 ? 0 : 1);
+ var count = comment[action + 's'] + ' ' + hashover.locale[action][plural];
+ }
+
+ // Add like count to HTML template
+ template[action + '-count'] = hashover.strings.parseTemplate (hashover.ui[action + '-count'], {
+ permalink: commentKey,
+ text: count || ''
+ });
+};
diff --git a/bootstrap/comments/api/frontends/latest/constructor.js b/bootstrap/comments/api/frontends/latest/constructor.js
new file mode 100644
index 0000000..32c1880
--- /dev/null
+++ b/bootstrap/comments/api/frontends/latest/constructor.js
@@ -0,0 +1,79 @@
+// @licstart The following is the entire license notice for the
+// JavaScript code in this page.
+//
+// Copyright (C) 2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// HashOver is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// HashOver is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with HashOver. If not, see <http://www.gnu.org/licenses/>.
+//
+// @licend The above is the entire license notice for the
+// JavaScript code in this page.
+
+"use strict";
+
+// Latest comments API frontend constructor (constructor.js)
+function HashOverLatest (options)
+{
+ // Reference to this HashOver object
+ var hashover = this;
+
+ // Backend queries
+ if (options && options.thread !== undefined) {
+ var queries = ['thread=' + encodeURIComponent (options.thread)];
+ } else {
+ var queries = [];
+ }
+
+ // Backend request path
+ var requestPath = '/latestajax';
+
+ // Handle backend request
+ this.ajax ('POST', requestPath, queries, function (json) {
+ // Handle error messages
+ if (json.message !== undefined) {
+ hashover.displayError (json, 'hashover-widget');
+ return;
+ }
+
+ // Locales from HashOver backend
+ HashOverLatest.prototype.locale = json.locale;
+
+ // Setup information from HashOver back-end
+ HashOverLatest.prototype.setup = json.setup;
+
+ // UI HTML from HashOver back-end
+ HashOverLatest.prototype.ui = json.ui;
+
+ // Thread information from HashOver back-end
+ hashover.instance = json.instance;
+
+ // Backend execution time and memory usage statistics
+ hashover.statistics = json.statistics;
+
+ // Initiate HashOver latest comments
+ hashover.init ();
+ }, true);
+
+ // Add parent proterty to all prototype objects
+ for (var name in this) {
+ var value = this[name];
+
+ if (value && value.constructor === Object) {
+ value.parent = this;
+ }
+ }
+};
+
+// Constructor to add HashOver methods to
+var HashOverConstructor = HashOverLatest;
diff --git a/bootstrap/comments/api/frontends/latest/init.js b/bootstrap/comments/api/frontends/latest/init.js
new file mode 100644
index 0000000..77185cd
--- /dev/null
+++ b/bootstrap/comments/api/frontends/latest/init.js
@@ -0,0 +1,37 @@
+// HashOver latest comments UI initialization process (init.js)
+HashOverLatest.prototype.init = function ()
+{
+ // Shorthand
+ var comments = this.instance.comments.primary;
+
+ // Initial comments HTML
+ var html = '';
+
+ // Append theme CSS if enabled
+ this.optionalMethod ('appendCSS', [ 'hashover-widget' ]);
+
+ // Add main HashOver element to this HashOver instance
+ this.instance['main-element'] = this.getMainElement ('hashover-widget');
+
+ // Templatify UI HTML strings
+ for (var element in this.ui) {
+ this.ui[element] = this.strings.templatify (this.ui[element]);
+ }
+
+ // Parse every comment
+ for (var i = 0, il = comments.length; i < il; i++) {
+ html += this.comments.parse (comments[i]);
+ }
+
+ // Add comments to element's innerHTML
+ if ('insertAdjacentHTML' in this.instance['main-element']) {
+ this.instance['main-element'].insertAdjacentHTML ('beforeend', html);
+ } else {
+ this.instance['main-element'].innerHTML = html;
+ }
+
+ // Add control events
+ for (var i = 0, il = comments.length; i < il; i++) {
+ this.addControls (comments[i]);
+ }
+};
diff --git a/bootstrap/comments/api/frontends/latest/instantiate.js b/bootstrap/comments/api/frontends/latest/instantiate.js
new file mode 100644
index 0000000..82ff971
--- /dev/null
+++ b/bootstrap/comments/api/frontends/latest/instantiate.js
@@ -0,0 +1,4 @@
+// Instantiate after the DOM is parsed
+HashOverLatest.onReady (function () {
+ window.hashoverLatest = new HashOverLatest ();
+});
diff --git a/bootstrap/comments/api/json.php b/bootstrap/comments/api/json.php
new file mode 100644
index 0000000..2904960
--- /dev/null
+++ b/bootstrap/comments/api/json.php
@@ -0,0 +1,76 @@
+<?php namespace HashOver;
+
+// Copyright (C) 2010-2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// HashOver is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// HashOver is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with HashOver. If not, see <http://www.gnu.org/licenses/>.
+
+
+// Change to the HashOver directory
+chdir (realpath ('../'));
+
+// Setup HashOver for JSON
+require ('backend/json-setup.php');
+
+try {
+ // Instantiate HashOver class
+ $hashover = new \HashOver ('json', 'api');
+
+ // Display error if the API is disabled
+ if ($hashover->setup->apiStatus ('json') === 'disabled') {
+ throw new \Exception ('<b>HashOver</b>: This API is not enabled.');
+ }
+
+ // Configure HashOver and load comments
+ $hashover->setup->setPageURL ('request');
+ $hashover->setup->collapsesComments = false;
+ $hashover->initiate ();
+
+ // Comments and statistics response array
+ $data = array ();
+
+ // Setup where to start reading comments
+ $start = $hashover->setup->getRequest ('start', 0);
+
+ // Check for comments
+ if ($hashover->thread->totalCount > 1) {
+ // Parse comments; TODO: Use starting point
+ $hashover->parsePrimary ();
+ $hashover->parsePopular ();
+
+ // Display as JSON data
+ $data['comments'] = $hashover->comments;
+ } else {
+ // Return no comments message
+ $data = array ('No comments.');
+ }
+
+ // Generate statistics
+ $hashover->statistics->executionEnd ();
+
+ // HashOver statistics
+ $data['statistics'] = array (
+ 'execution-time' => $hashover->statistics->executionTime,
+ 'script-memory' => $hashover->statistics->scriptMemory,
+ 'system-memory' => $hashover->statistics->systemMemory
+ );
+
+ // Return JSON or JSONP function call
+ echo $hashover->misc->jsonData ($data);
+
+} catch (\Exception $error) {
+ $misc = new Misc ('json');
+ $message = $error->getMessage ();
+ $misc->displayError ($message);
+}
diff --git a/bootstrap/comments/api/latest.php b/bootstrap/comments/api/latest.php
new file mode 100644
index 0000000..0582260
--- /dev/null
+++ b/bootstrap/comments/api/latest.php
@@ -0,0 +1,154 @@
+<?php namespace HashOver;
+
+// Copyright (C) 2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// HashOver is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// HashOver is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with HashOver. If not, see <http://www.gnu.org/licenses/>.
+
+
+// Change to the HashOver directory
+chdir (realpath ('../'));
+
+// Setup HashOver for JavaScript
+require ('backend/javascript-setup.php');
+
+try {
+ // Instantiate general setup class
+ $setup = new Setup (array (
+ 'mode' => 'javascript',
+ 'context' => 'api'
+ ));
+
+ // Throw exception if the "Latest Comments" API is disabled
+ if ($setup->apiStatus ('latest') === 'disabled') {
+ throw new \Exception ('This API is not enabled.');
+ }
+
+ // Instantiate HashOver statistics class
+ $statistics = new Statistics ('javascript');
+
+ // Start execution timer
+ $statistics->executionStart ();
+
+ // Instantiate JavaScript build class
+ $javascript = new JavaScriptBuild ('api/frontends/latest');
+
+ // Register initial constructor
+ $javascript->registerFile ('constructor.js');
+
+ // Change to standard frontend directory
+ $javascript->changeDirectory ('frontend');
+
+ // Register HashOver script tag getter method
+ $javascript->registerFile ('script.js');
+
+ // Register backend path setter
+ $javascript->registerFile ('backendpath.js');
+
+ // Register HashOver ready state detection method
+ $javascript->registerFile ('onready.js');
+
+ // Register element creation methods
+ $javascript->registerFile ('elements.js');
+
+ // Register main HashOver element getter method
+ $javascript->registerFile ('getmainelement.js');
+
+ // Register error message handler method
+ $javascript->registerFile ('displayerror.js');
+
+ // Register AJAX-related methods
+ $javascript->registerFile ('ajax.js');
+
+ // Register pre-compiled regular expressions
+ $javascript->registerFile ('regex.js');
+
+ // Register end-of-line trimmer method
+ $javascript->registerFile ('eoltrim.js');
+
+ // Register parent permalink getter method
+ $javascript->registerFile ('permalinks.js');
+
+ // Register markdown methods
+ $javascript->registerFile ('markdown.js', array (
+ 'include' => $setup->usesMarkdown
+ ));
+
+ // Register date/time methods
+ $javascript->registerFile ('datetime.js', array (
+ 'include' => $setup->usesUserTimezone
+ ));
+
+ // Register search and replace methods
+ $javascript->registerFile ('strings.js');
+
+ // Register optional method handler method
+ $javascript->registerFile ('optionalmethod.js');
+
+ // Register comment parsing methods
+ $javascript->registerFile ('comments.js');
+
+ // Register embedded image method
+ $javascript->registerFile ('embedimage.js', array (
+ 'include' => $setup->allowsImages,
+
+ 'dependencies' => array (
+ 'openembeddedimage.js'
+ )
+ ));
+
+ // Register classList polyfill methods
+ $javascript->registerFile ('classes.js');
+
+ // Register theme stylesheet appender method
+ $javascript->registerFile ('appendcss.js', array (
+ 'include' => $setup->appendsCss
+ ));
+
+ // Change back to latest frontend directory
+ $javascript->changeDirectory ('api/frontends/latest');
+
+ // Register Like/Dislike methods
+ $javascript->registerFile ('addratings.js', array (
+ 'include' => ($setup->allowsLikes or $setup->allowsDislikes)
+ ));
+
+ // Register control event handler attacher method
+ $javascript->registerFile ('addcontrols.js');
+
+ // Register initialization method
+ $javascript->registerFile ('init.js');
+
+ // Register automatic instantiation code
+ $javascript->registerFile ('instantiate.js', array (
+ 'include' => !isset ($_GET['nodefault'])
+ ));
+
+ // JavaScript build process output
+ $output = $javascript->build (
+ $setup->minifiesJavascript,
+ $setup->minifyLevel
+ );
+
+ // Display JavaScript build process output
+ echo $output, PHP_EOL;
+
+ // Display statistics
+ echo $statistics->executionEnd ();
+
+} catch (\Exception $error) {
+ $misc = new Misc ('javascript');
+ $message = $error->getMessage ();
+ $misc->displayError ($message);
+}
diff --git a/bootstrap/comments/api/rss.php b/bootstrap/comments/api/rss.php
new file mode 100644
index 0000000..d1cf020
--- /dev/null
+++ b/bootstrap/comments/api/rss.php
@@ -0,0 +1,327 @@
+<?php namespace HashOver;
+
+// Copyright (C) 2010-2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// HashOver is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// HashOver is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with HashOver. If not, see <http://www.gnu.org/licenses/>.
+
+
+// Tell browser this is XML/RSS
+header ('Content-Type: application/xml; charset=utf-8');
+
+// Change to the HashOver directory
+chdir (realpath ('../'));
+
+// Do some standard HashOver setup work
+require ('backend/nocache-headers.php');
+require ('backend/standard-setup.php');
+
+// Autoload class files
+spl_autoload_register (function ($uri) {
+ $uri = str_replace ('\\', '/', strtolower ($uri));
+ $class_name = basename ($uri);
+ $error = '"' . $class_name . '.php" file could not be included!';
+
+ if (!@include ('backend/classes/' . $class_name . '.php')) {
+ echo '<?xml version="1.0" encoding="UTF-8"?>', PHP_EOL;
+ echo '<error>', $error, '</error>';
+ exit;
+ }
+});
+
+function create_rss (&$hashover)
+{
+ // Shorter variable name
+ $thread = $hashover->setup->threadName;
+
+ // Attempt to read page information metadata
+ $metadata = $hashover->thread->data->readMeta ('page-info', $thread);
+
+ // Check if metadata read successfully
+ if ($metadata !== false) {
+ // If so, set page URL blank if it's missing from the metadata
+ if (!isset ($metadata['url'])) {
+ $metadata['url'] = '';
+ }
+
+ // And set page title to "Untitled" if it's missing from the metadata
+ if (!isset ($metadata['title'])) {
+ $metadata['title'] = $hashover->locale->text['untitled'];
+ }
+ } else {
+ // If not, set default metadata information
+ $metadata = array (
+ 'url' => '',
+ 'title' => $hashover->locale->text['untitled']
+ );
+ }
+
+ // Create new DOM document.
+ $xml = new \DOMDocument ('1.0', 'UTF-8');
+ $xml->preserveWhiteSpace = false;
+ $xml->formatOutput = true;
+
+ // Create main RSS element
+ $rss = $xml->createElement ('rss');
+ $rss->setAttribute ('version', '2.0');
+ $rss->setAttribute ('xmlns:dc', 'http://purl.org/dc/elements/1.1/');
+ $rss->setAttribute ('xmlns:content', 'http://purl.org/rss/1.0/modules/content/');
+ $rss->setAttribute ('xmlns:atom', 'http://www.w3.org/2005/Atom');
+
+ // Display error if the API is disabled
+ if ($hashover->setup->apiStatus ('rss') === 'disabled') {
+ $title = $xml->createElement ('title');
+ $title_value = $xml->createTextNode ('HashOver: RSS API is not enabled.');
+ $title->appendChild ($title_value);
+ $rss->appendChild ($title);
+
+ $description = $xml->createElement ('description');
+ $description_value = $xml->createTextNode ('Error!');
+ $description->appendChild ($description_value);
+ $rss->appendChild ($description);
+
+ // Add main RSS element to XML
+ $xml->appendChild ($rss);
+
+ // Return RSS XML
+ exit (str_replace (' ', "\t", $xml->saveXML ()));
+ }
+
+ // Create channel element
+ $channel = $xml->createElement ('channel');
+
+ // Create channel title element
+ $title = $xml->createElement ('title');
+ $title_value = $xml->createTextNode (html_entity_decode ($metadata['title'], ENT_COMPAT, 'UTF-8'));
+ $title->appendChild ($title_value);
+
+ // Add channel title to channel element
+ $channel->appendChild ($title);
+
+ // Create channel link element
+ $link = $xml->createElement ('link');
+ $link_value = $xml->createTextNode (html_entity_decode ($metadata['url'], ENT_COMPAT, 'UTF-8'));
+ $link->appendChild ($link_value);
+
+ // Add channel link to channel element
+ $channel->appendChild ($link);
+
+ // Create channel description element
+ $description = $xml->createElement ('description');
+ $count_plural = ($hashover->thread->totalCount !== 1);
+ $showing_comments_locale = $hashover->locale->text['showing-comments'][$count_plural];
+ $count_locale = sprintf ($showing_comments_locale, $hashover->thread->totalCount - 1);
+ $description_value = $xml->createTextNode ($count_locale);
+ $description->appendChild ($description_value);
+
+ // Add channel description to channel element
+ $channel->appendChild ($description);
+
+ // Create channel atom link element
+ $atom_link = $xml->createElement ('atom:link');
+ $atom_link->setAttribute ('href', 'http://' . $hashover->setup->domain . $_SERVER['PHP_SELF'] . '?url=' . $metadata['url']);
+ $atom_link->setAttribute ('rel', 'self');
+
+ // Add channel atom link to channel element
+ $channel->appendChild ($atom_link);
+
+ // Create channel language element
+ $language = $xml->createElement ('language');
+ $language_value = $xml->createTextNode ('en-us');
+ $language->appendChild ($language_value);
+
+ // Add channel language to channel element
+ $channel->appendChild ($language);
+
+ // Create channel ttl element
+ $ttl = $xml->createElement ('ttl');
+ $ttl_value = $xml->createTextNode ('40');
+ $ttl->appendChild ($ttl_value);
+
+ // Add channel ttl to channel element
+ $channel->appendChild ($ttl);
+
+ // Add channel element to main RSS element
+ $rss->appendChild ($channel);
+
+ // Parse comments
+ function parse_comments (&$metadata, &$comment, &$rss, &$xml, &$hashover)
+ {
+ // Skip deleted/unmoderated comments
+ if (isset ($comment['notice'])) {
+ return;
+ }
+
+ // Encode HTML entities
+ $comment['body'] = htmlentities ($comment['body'], ENT_COMPAT, 'UTF-8', true);
+
+ // Decode HTML entities
+ $comment['body'] = html_entity_decode ($comment['body'], ENT_COMPAT, 'UTF-8');
+
+ // Remove [img] tags
+ $comment['body'] = preg_replace ('/\[(img|\/img)\]/iS', '', $comment['body']);
+
+ // Parse comment as markdown
+ $comment['body'] = $hashover->markdown->parseMarkdown ($comment['body']);
+
+ // Convert <code> tags to <pre> tags
+ $comment['body'] = preg_replace ('/(<|<\/)code>/iS', '\\1pre>', $comment['body']);
+
+ // Get name from comment or use configured default
+ $name = !empty ($comment['name']) ? $comment['name'] : $hashover->setup->defaultName;
+
+ // Create item element
+ $item = $xml->createElement ('item');
+
+ // Generate comment summary item title
+ $title = $name . ' : ';
+ $single_comment = str_replace (PHP_EOL, ' ', strip_tags ($comment['body']));
+
+ if (mb_strlen ($single_comment) > 40) {
+ $title .= mb_substr ($single_comment, 0, 40) . '...';
+ } else {
+ $title .= $single_comment;
+ }
+
+ // Create item title element
+ $item_title = $xml->createElement ('title');
+ $item_title_value = $xml->createTextNode (html_entity_decode ($title, ENT_COMPAT, 'UTF-8'));
+ $item_title->appendChild ($item_title_value);
+
+ // Add item title element to item element
+ $item->appendChild ($item_title);
+
+ // Create item name element
+ $item_name = $xml->createElement ('name');
+ $item_name_value = $xml->createTextNode (html_entity_decode ($name, ENT_COMPAT, 'UTF-8'));
+ $item_name->appendChild ($item_name_value);
+
+ // Add item name element to item element
+ $item->appendChild ($item_name);
+
+ // Add HTML anchor tag to URLs (hyperlinks)
+ $comment['body'] = preg_replace ('/((ftp|http|https):\/\/[a-z0-9-@:%_\+.~#?&\/=]+) {0,}/iS', '<a href="\\1" target="_blank">\\1</a>', $comment['body']);
+
+ // Replace newlines with break tags
+ $comment['body'] = str_replace (PHP_EOL, '<br>', $comment['body']);
+
+ // Create item description element
+ $item_description = $xml->createElement ('description');
+ $item_description_value = $xml->createTextNode ($comment['body']);
+ $item_description->appendChild ($item_description_value);
+
+ // Add item description element to item element
+ $item->appendChild ($item_description);
+
+ // Create item avatar element
+ $item_avatar = $xml->createElement ('avatar');
+ $web_root = 'http://' . $hashover->setup->domain . $hashover->setup->httpRoot;
+ $item_avatar_value = $xml->createTextNode ($web_root . $comment['avatar']);
+ $item_avatar->appendChild ($item_avatar_value);
+
+ // Add item avatar element to item element
+ $item->appendChild ($item_avatar);
+
+ if (!empty ($comment['likes'])) {
+ // Create item likes element
+ $item_likes = $xml->createElement ('likes');
+ $item_likes_value = $xml->createTextNode ($comment['likes']);
+ $item_likes->appendChild ($item_likes_value);
+
+ // Add item likes element to item element
+ $item->appendChild ($item_likes);
+ }
+
+ if ($hashover->setup->allowsDislikes === true) {
+ if (!empty ($comment['dislikes'])) {
+ // Create item dislikes element
+ $item_dislikes = $xml->createElement ('dislikes');
+ $item_dislikes_value = $xml->createTextNode ($comment['dislikes']);
+ $item_dislikes->appendChild ($item_dislikes_value);
+
+ // Add item dislikes element to item element
+ $item->appendChild ($item_dislikes);
+ }
+ }
+
+ // Create item publication date element
+ $item_pubDate = $xml->createElement ('pubDate');
+ $item_pubDate_value = $xml->createTextNode (date ('D, d M Y H:i:s O', $comment['sort-date']));
+ $item_pubDate->appendChild ($item_pubDate_value);
+
+ // Add item pubDate element to item element
+ $item->appendChild ($item_pubDate);
+
+ // URL to comment for item guide and link elements
+ $item_permalink_url = $metadata['url'] . '#' . $comment['permalink'];
+
+ // Create item guide element
+ $item_guid = $xml->createElement ('guid');
+ $item_guid_value = $xml->createTextNode ($item_permalink_url);
+ $item_guid->appendChild ($item_guid_value);
+
+ // Add item guide element to item element
+ $item->appendChild ($item_guid);
+
+ // Create item link element
+ $item_link = $xml->createElement ('link');
+ $item_link_value = $xml->createTextNode ($item_permalink_url);
+ $item_link->appendChild ($item_link_value);
+
+ // Add item link element to item element
+ $item->appendChild ($item_link);
+
+ // Add item element to main RSS element
+ $rss->appendChild ($item);
+
+ // Recursively parse replies
+ if (!empty ($comment['replies'])) {
+ foreach ($comment['replies'] as $reply) {
+ parse_comments ($metadata, $reply, $rss, $xml, $hashover);
+ }
+ }
+ }
+
+ // Add item element to main RSS element
+ foreach ($hashover->comments['primary'] as $comment) {
+ parse_comments ($metadata, $comment, $rss, $xml, $hashover);
+ }
+
+ // Add main RSS element to XML
+ $xml->appendChild ($rss);
+
+ // Return RSS XML
+ echo preg_replace_callback ('/^(\s+)/m', function ($spaces) {
+ return str_repeat ("\t", strlen ($spaces[1]) / 2);
+ }, $xml->saveXML ());
+
+ // Return statistics
+ echo $hashover->statistics->executionEnd ();
+}
+
+try {
+ // Instantiate HashOver class
+ $hashover = new \HashOver ('php', 'api');
+ $hashover->setup->setPageURL ('request');
+ $hashover->setup->collapsesComments = false;
+ $hashover->initiate ();
+ $hashover->parsePrimary ();
+
+ // Create RSS feed
+ create_rss ($hashover);
+
+} catch (\Exception $error) {
+ $misc = new Misc ('rss');
+ $misc->displayError ($error->getMessage ());
+}
diff --git a/bootstrap/comments/backend/classes/avatars.php b/bootstrap/comments/backend/classes/avatars.php
new file mode 100644
index 0000000..89fc96b
--- /dev/null
+++ b/bootstrap/comments/backend/classes/avatars.php
@@ -0,0 +1,90 @@
+<?php namespace HashOver;
+
+// Copyright (C) 2015-2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// HashOver is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// HashOver is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with HashOver. If not, see <http://www.gnu.org/licenses/>.
+
+
+class Avatars
+{
+ public $setup;
+ public $isHTTPS = false;
+ public $http;
+ public $subdomain;
+ public $iconSize;
+ public $avatar;
+ public $fallback;
+
+ public function __construct (Setup $setup)
+ {
+ $this->setup = $setup;
+ $this->isHTTPS = $setup->isHTTPS ();
+
+ // Get icon size from settings
+ $this->iconSize = ($setup->isMobile === true) ? 256 : $setup->iconSize;
+
+ // Default avatar
+ $avatar = $setup->httpImages . '/avatar';
+ $extension = ($setup->isMobile === true) ? 'svg' : 'png';
+ $this->avatar = $avatar . '.' . $extension;
+
+ // Use HTTPS if this file is requested with HTTPS
+ $this->http = ($this->isHTTPS ? 'https' : 'http') . '://';
+ $this->subdomain = $this->isHTTPS ? 'secure' : 'www';
+
+ // If set to custom, direct 404s to local avatar image
+ if ($setup->gravatarDefault === 'custom') {
+ $fallback = $avatar . '.png';
+
+ // Check if HashOver is being remotely accessed
+ if ($setup->remoteAccess === false) {
+ // If so, make avatar path absolute
+ $fallback = $setup->absolutePath . $fallback;
+ }
+
+ // URL encode fallback URL
+ $this->fallback = urlencode ($fallback);
+ } else {
+ // If not direct to a themed default
+ $this->fallback = $setup->gravatarDefault;
+ }
+
+ // Gravatar URL
+ $this->gravatar = $this->http . $this->subdomain;
+ $this->gravatar .= '.gravatar.com/avatar/';
+ }
+
+ // Attempt to get Gravatar avatar image
+ public function getGravatar ($hash)
+ {
+ // If no hash is given, return the default avatar
+ if (empty ($hash)) {
+ return $this->avatar;
+ }
+
+ // Gravatar URL
+ $gravatar = $this->gravatar . $hash . '.png?r=pg';
+ $gravatar .= '&amp;s=' . $this->iconSize;
+ $gravatar .= '&amp;d=' . $this->fallback;
+
+ // Force Gravatar default avatar if enabled
+ if ($this->setup->gravatarForce === true) {
+ $gravatar .= '&amp;f=y';
+ }
+
+ // Redirect Gravatar image
+ return $gravatar;
+ }
+}
diff --git a/bootstrap/comments/backend/classes/commentfiles.php b/bootstrap/comments/backend/classes/commentfiles.php
new file mode 100644
index 0000000..187a724
--- /dev/null
+++ b/bootstrap/comments/backend/classes/commentfiles.php
@@ -0,0 +1,106 @@
+<?php namespace HashOver;
+
+// Copyright (C) 2010-2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// HashOver is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// HashOver is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with HashOver. If not, see <http://www.gnu.org/licenses/>.
+
+
+// Read and count comments
+class CommentFiles extends DataFiles
+{
+ public $setup;
+ public $storageMode;
+
+ public function __construct (Setup $setup)
+ {
+ parent::__construct ($setup);
+
+ $this->setup = $setup;
+ $this->storageMode = 'flat-file';
+ }
+
+ // Returns a comment file path for a given file and thread
+ public function getCommentPath ($file, $extension, $thread = 'auto')
+ {
+ $default = $this->setup->threadDirectory;
+ $path = $this->setup->pagesDirectory;
+ $thread = ($thread !== 'auto') ? $path . '/' . $thread : $default;
+ $path = $thread . '/' . $file . '.' . $extension;
+
+ return $path;
+ }
+
+ // Read directory contents, put filenames in array, count files
+ public function loadFiles ($extension, array $files = array (), $auto = true)
+ {
+ if ($auto === true) {
+ $pattern = $this->setup->threadDirectory . '/*.' . $extension;
+ $files = glob ($pattern, GLOB_NOSORT);
+ }
+
+ if (!empty ($files)) {
+ $comments = array ();
+
+ foreach ($files as $file) {
+ $key = basename ($file, '.' . $extension);
+ $comments[$key] =(string) $key;
+ }
+
+ return $comments;
+ }
+
+ return false;
+ }
+
+ // Check if comment thread directory exists
+ public function checkThread ()
+ {
+ // Attempt to create the directory
+ if (!file_exists ($this->setup->threadDirectory)
+ and !@mkdir ($this->setup->threadDirectory, 0755, true)
+ and !@chmod ($this->setup->threadDirectory, 0755))
+ {
+ throw new \Exception (sprintf (
+ 'Failed to create comment thread directory at "%s"',
+ $this->setup->threadDirectory
+ ));
+ }
+
+ // If yes, check if it is or can be made to be writable
+ if (!is_writable ($this->setup->threadDirectory)
+ and !@chmod ($this->setup->threadDirectory, 0755))
+ {
+ throw new \Exception (sprintf (
+ 'Comment thread directory at "%s" is not writable.',
+ $this->setup->threadDirectory
+ ));
+ }
+
+ return true;
+ }
+
+ // Queries a list of comment threads
+ public function queryThreads ()
+ {
+ $pages = $this->setup->pagesDirectory;
+ $directories = glob ($pages . '/*', GLOB_ONLYDIR);
+
+ foreach ($directories as &$directory) {
+ $directory = basename ($directory);
+ }
+
+ return $directories;
+ }
+}
diff --git a/bootstrap/comments/backend/classes/commentparser.php b/bootstrap/comments/backend/classes/commentparser.php
new file mode 100644
index 0000000..fc6a6b0
--- /dev/null
+++ b/bootstrap/comments/backend/classes/commentparser.php
@@ -0,0 +1,227 @@
+<?php namespace HashOver;
+
+// Copyright (C) 2010-2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// HashOver is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// HashOver is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with HashOver. If not, see <http://www.gnu.org/licenses/>.
+
+
+// Parse comments and create deleted comment note
+class CommentParser
+{
+ public $setup;
+ public $login;
+ public $locale;
+ public $avatars;
+ public $cookies;
+
+ protected $dateIntervalLocales;
+ protected $todayLocale;
+ protected $currentDateTime;
+
+ public function __construct (Setup $setup)
+ {
+ $this->setup = $setup;
+ $this->login = new Login ($setup);
+ $this->locale = new Locale ($setup);
+ $this->avatars = new Avatars ($setup);
+ $this->cookies = new Cookies ($setup);
+ $this->currentDateTime = new \DateTime (date('Y-m-d'));
+
+ $this->dateIntervalLocales = array (
+ 'y' => $this->locale->text['date-years'],
+ 'm' => $this->locale->text['date-months'],
+ 'd' => $this->locale->text['date-days']
+ );
+
+ $this->todayLocale = $this->locale->text['date-today'];
+ $this->dateTimeLocale = $this->locale->text['date-time'];
+ }
+
+ // Parse comment files
+ public function parse (array $comment, $key, $key_parts, $popular = false)
+ {
+ $micro_date = 0;
+ $output = array ();
+
+ // Get micro time of comment post date
+ if (!empty ($comment['date'])) {
+ $micro_date = strtotime ($comment['date']);
+ }
+
+ // Generate permalink
+ if (count ($key_parts) > 1) {
+ $output['permalink'] = 'c' . str_replace ('-', 'r', $key);
+ } else {
+ $output['permalink'] = 'c' . $key;
+ }
+
+ // Append "-pop" to end of permalink if popular comment
+ if ($popular === true) {
+ $output['permalink'] .= '-pop';
+ }
+
+ // Check if short dates are enabled
+ if ($this->setup->usesShortDates === true) {
+ // If so, get DateTime from comment
+ $comment_datetime = new \DateTime (date ('Y-m-d', $micro_date));
+
+ // Get the difference between today's date and the comment post date
+ $interval = $comment_datetime->diff ($this->currentDateTime);
+
+ // And attempt to get a day, month, or year interval
+ foreach ($this->dateIntervalLocales as $i => $date_locale) {
+ if ($interval->$i > 0) {
+ $date_plural = ($interval->$i !== 1);
+ $comment_date = sprintf ($date_locale[$date_plural], $interval->$i);
+ break;
+ }
+ }
+
+ // Otherwise, use today locale
+ if (empty ($comment_date)) {
+ $comment_time = date ($this->setup->timeFormat, $micro_date);
+ $comment_date = sprintf ($this->todayLocale, $comment_time);
+ }
+ } else {
+ // If not, use configurable date and time locale
+ $comment_date = date ($this->dateTimeLocale, $micro_date);
+ }
+
+ // Add name to output
+ if (!empty ($comment['name'])) {
+ $output['name'] = $comment['name'];
+ }
+
+ // Get avatar icons
+ if ($this->setup->iconMode !== 'none') {
+ if ($this->setup->iconMode === 'image') {
+ // Get MD5 hash for Gravatar
+ $hash = !empty ($comment['email_hash']) ? $comment['email_hash'] : '';
+ $output['avatar'] = $this->avatars->getGravatar ($hash);
+ } else {
+ $output['avatar'] = end ($key_parts);
+ }
+ }
+
+ // Add website URL to output
+ if (!empty ($comment['website'])) {
+ $output['website'] = $comment['website'];
+ }
+
+ // Output whether commenter receives notifications
+ if (!empty ($comment['email']) and !empty ($comment['notifications'])) {
+ if ($comment['notifications'] === 'yes') {
+ $output['subscribed'] = true;
+ }
+ }
+
+ // Add number of likes to output
+ if (!empty ($comment['likes'])) {
+ $output['likes'] =(int) $comment['likes'];
+ }
+
+ // If enabled, add number of dislikes to output
+ if ($this->setup->allowsDislikes === true) {
+ if (!empty ($comment['dislikes'])) {
+ $output['dislikes'] =(int) $comment['dislikes'];
+ }
+ }
+
+ // Check if the user is logged in
+ if ($this->login->userIsLoggedIn === true and !empty ($comment['login_id'])) {
+ // If so, check this comment belongs to logged in user
+ if ($this->login->loginHash === $comment['login_id']) {
+ $output['user-owned'] = true;
+
+ // Check if the comment is editable
+ if (!empty ($comment['password'])) {
+ $output['editable'] = true;
+ }
+ }
+ }
+
+ // Admin is allowed to edit every comment
+ if ($this->login->userIsAdmin === true) {
+ $output['editable'] = true;
+ }
+
+ // Create like cookie hash
+ $like_hash = md5 ($this->setup->domain . $this->setup->threadName . '/' . $key);
+
+ // Get like cookie
+ $like_cookie = $this->cookies->getValue ($like_hash);
+
+ // Check if comment has been liked or disliked
+ if ($like_cookie !== null) {
+ switch ($like_cookie) {
+ case 'liked': {
+ $output['liked'] = true;
+ break;
+ }
+
+ case 'disliked': {
+ if ($this->setup->allowsDislikes === true) {
+ $output['disliked'] = true;
+ }
+
+ break;
+ }
+ }
+ }
+
+ // Add comment date to output
+ $output['date'] =(string) $comment_date;
+
+ if (!empty ($comment['status'])) {
+ $status = $comment['status'];
+
+ // Check if comment has a status other than approved
+ if ($status !== 'approved') {
+ // If so, add comment status to output
+ $output['status'] =(string) $status;
+
+ // And add status text to output
+ $output['status-text'] = mb_strtolower ($this->locale->text[$status . '-name']);
+ }
+ }
+
+ // Add comment date as Unix timestamp to output
+ $output['sort-date'] =(int) $micro_date;
+
+ // Add comment body to output
+ $output['body'] =(string) $comment['body'];
+
+ return $output;
+ }
+
+ // Function for adding notices to output
+ public function notice ($type, $key, &$last_date)
+ {
+ $output = array ();
+ $output['title'] = $this->locale->text[$type . '-name'];
+ $last_date++;
+
+ if ($this->setup->iconMode !== 'none') {
+ $output['avatar'] = $this->setup->getImagePath ($type . '-icon');
+ }
+
+ $output['permalink'] = 'c' . str_replace ('-', 'r', $key);
+ $output['notice'] = $this->locale->text[$type . '-note'];
+ $output['notice-class'] = 'hashover-' . $type;
+ $output['sort-date'] =(int) $last_date;
+
+ return $output;
+ }
+}
diff --git a/bootstrap/comments/backend/classes/commentsui.php b/bootstrap/comments/backend/classes/commentsui.php
new file mode 100644
index 0000000..9acd447
--- /dev/null
+++ b/bootstrap/comments/backend/classes/commentsui.php
@@ -0,0 +1,634 @@
+<?php namespace HashOver;
+
+// Copyright (C) 2015-2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// HashOver is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// HashOver is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with HashOver. If not, see <http://www.gnu.org/licenses/>.
+
+
+class CommentsUI extends FormUI
+{
+ public function __construct (Setup $setup, array $counts)
+ {
+ parent::__construct ($setup, $counts);
+ }
+
+ // Creates a wrapper element for each comment
+ public function commentWrapper ($permalink = '{{permalink}}')
+ {
+ $comment_wrapper = new HTMLTag ('div', array (
+ 'id' => $permalink,
+ 'class' => 'hashover-comment'
+ ), false);
+
+ if ($this->mode !== 'php') {
+ $comment_wrapper->appendAttribute ('class', '{{class}}', false);
+ $comment_wrapper->innerHTML ('{{html}}');
+
+ return $comment_wrapper->asHTML ();
+ }
+
+ return $comment_wrapper;
+ }
+
+ // Creates wrapper element to name element
+ public function nameWrapper ($class = '{{class}}', $link = '{{link}}')
+ {
+ $name_wrapper = new HTMLTag ('span', array (
+ 'class' => 'hashover-comment-name ' . $class,
+ 'innerHTML' => $link
+ ), false);
+
+ return $name_wrapper->asHTML ();
+ }
+
+ // Creates name hyperlink/span element
+ public function nameElement ($element, $permalink = '{{permalink}}', $name = '{{name}}', $href = '{{href}}')
+ {
+ // Decide what kind of element to create
+ switch ($element) {
+ case 'a': {
+ // A hyperlink pointing to the user's input URL
+ $name_link = new HTMLTag ('a', array (
+ 'href' => $href,
+ 'id' => 'hashover-name-' . $permalink,
+ 'rel' => 'noopener noreferrer',
+ 'target' => '_blank',
+ 'title' => $name,
+ 'innerHTML' => $name
+ ), false);
+
+ break;
+ }
+
+ case 'span': {
+ // A plain wrapper element
+ $name_link = new HTMLTag ('span', array (
+ 'id' => 'hashover-name-' . $permalink,
+ 'innerHTML' => $name
+ ), false);
+
+ break;
+ }
+ }
+
+ return $name_link->asHTML ();
+ }
+
+ // Creates "Top of Thread" hyperlink element
+ public function parentThreadLink ($parent = '{{parent}}', $permalink = '{{permalink}}', $name = '{{name}}')
+ {
+ // Get locale string
+ $thread_locale = $this->locale->text['thread'];
+
+ // Inject OP's name into the locale
+ $inner_html = sprintf ($thread_locale, $name);
+
+ // Create hyperlink element
+ $thread_link = new HTMLTag ('a', array (
+ 'href' => '#' . $parent,
+ 'id' => 'hashover-thread-link-' . $permalink,
+ 'class' => 'hashover-thread-link',
+ 'title' => $this->locale->text['thread-tip'],
+ 'innerHTML' => $inner_html
+ ), false);
+
+ return $thread_link->asHTML ();
+ }
+
+ // Creates hyperlink with URL queries to link reference
+ protected function queryLink ($href = '', array $queries = array ())
+ {
+ // Create hyperlink with relative local or absolute remote path
+ $link = new HTMLTag ('a', array (
+ 'href' => !empty ($href) ? $href : $this->setup->filePath
+ ), false);
+
+ // Merge given URL queries with existing page URL queries
+ $queries = array_merge ($this->setup->URLQueryList, $queries);
+
+ // Add URL queries to link path
+ if (!empty ($queries)) {
+ $link->appendAttribute ('href', '?' . implode ('&', $queries), false);
+ }
+
+ return $link;
+ }
+
+ // Creates date/permalink hyperlink element
+ public function dateLink ($href = '{{href}}', $permalink = '{{permalink}}', $date = '{{date}}')
+ {
+ // Create hyperlink element
+ $date_link = $this->queryLink ($href);
+
+ // Append more attributes
+ $date_link->appendAttributes (array (
+ 'href' => '#' . $permalink,
+ 'class' => 'hashover-date-permalink',
+ 'title' => 'Permalink',
+ 'innerHTML' => $date
+ ), false);
+
+ return $date_link->asHTML ();
+ }
+
+ // Creates element to hold a count of likes/dislikes each comment has
+ public function likeCount ($type, $permalink = '{{permalink}}', $text = '{{text}}')
+ {
+ // CSS class
+ $class = 'hashover-' . $type;
+
+ // Create element
+ $count = new HTMLTag ('span', array (
+ 'id' => $class . '-' . $permalink,
+ 'class' => $class,
+ 'innerHTML' => $text
+ ), false);
+
+ return $count->asHTML ();
+ }
+
+ // Creates "Like"/"Dislike" hyperlink element
+ public function likeLink ($type, $permalink = '{{permalink}}', $class = '{{class}}', $title = '{{title}}', $text = '{{text}}')
+ {
+ // Create hyperlink element
+ $link = new HTMLTag ('a', array (
+ 'href' => '#',
+ 'id' => 'hashover-' . $type . '-' . $permalink,
+ 'class' => $class,
+ 'title' => $title,
+ 'innerHTML' => $text
+ ), false);
+
+ return $link->asHTML ();
+ }
+
+ // Creates a form control hyperlink element
+ public function formLink ($href, $type, $permalink = '{{permalink}}', $class = '{{class}}', $title = '{{title}}')
+ {
+ $form = 'hashover-' . $type;
+ $link = $this->queryLink ($href, array ($form . '=' . $permalink));
+ $title_locale = ($type === 'reply') ? 'reply-to-comment' : 'edit-your-comment';
+
+ // Create more attributes
+ $link->createAttributes (array (
+ 'id' => $form. '-link-' . $permalink,
+ 'class' => 'hashover-comment-' . $type,
+ 'title' => $this->locale->text[$title_locale]
+ ));
+
+ // Append href attribute
+ $link->appendAttribute ('href', '#' . $form . '-' . $permalink, false);
+
+ // Append attributes
+ if ($type === 'reply') {
+ $link->appendAttributes (array (
+ 'class' => $class,
+ 'title' => '- ' . $title
+ ));
+ }
+
+ // Add link text
+ $link->innerHTML ($this->locale->text[$type]);
+
+ return $link->asHTML ();
+ }
+
+ // Creates "Cancel" hyperlink element
+ public function cancelLink ($permalink, $for, $class = '')
+ {
+ $cancel_link = $this->queryLink ($this->setup->filePath);
+ $cancel_locale = $this->locale->text['cancel'];
+
+ // Append href attribute
+ $cancel_link->appendAttribute ('href', '#' . $permalink, false);
+
+ // Create more attributes
+ $cancel_link->createAttributes (array (
+ 'class' => 'hashover-comment-' . $for,
+ 'title' => $cancel_locale
+ ));
+
+ // Append optional class
+ if (!empty ($class)) {
+ $cancel_link->appendAttribute ('class', $class);
+ }
+
+ // Add "Cancel" hyperlink text
+ $cancel_link->innerHTML ($cancel_locale);
+
+ return $cancel_link->asHTML ();
+ }
+
+ public function userAvatar ($src = '{{src}}', $href = '{{href}}', $text = '{{text}}')
+ {
+ // If avatars set to images
+ if ($this->setup->iconMode !== 'none') {
+ // Create wrapper element for avatar image
+ $avatar_wrapper = new HTMLTag ('span', array (
+ 'class' => 'hashover-avatar'
+ ), false);
+
+ if ($this->setup->iconMode !== 'count') {
+ // Create avatar image element
+ $comments_avatar = new HTMLTag ('div', array (
+ 'style' => 'background-image: url(\'' . $src . '\');'
+ ), false);
+ } else {
+ // Avatars set to count
+ // Create element displaying comment number user will be
+ $comments_avatar = new HTMLTag ('a', array (
+ 'href' => '#' . $href,
+ 'title' => 'Permalink',
+ 'innerHTML' => $text
+ ), false);
+ }
+
+ // Add comments avatar to avatar image wrapper element
+ $avatar_wrapper->appendChild ($comments_avatar);
+
+ return $avatar_wrapper->asHTML ();
+ }
+
+ return '';
+ }
+
+ public function cancelButton ($type, $permalink)
+ {
+ $cancel_button = $this->queryLink ($this->setup->filePath);
+ $class = 'hashover-' . $type . '-cancel';
+ $cancel_locale = $this->locale->text['cancel'];
+
+ // Add ID attribute with JavaScript variable single quote break out
+ if (!empty ($permalink)) {
+ $cancel_button->createAttribute ('id', $class . '-' . $permalink);
+ }
+
+ // Append href attribute
+ $cancel_button->appendAttribute ('href', '#hashover-' . $permalink, false);
+
+ // Create more attributes
+ $cancel_button->createAttributes (array (
+ 'class' => 'hashover-submit ' . $class,
+ 'title' => $cancel_locale,
+ 'innerHTML' => $cancel_locale
+ ));
+
+ return $cancel_button;
+ }
+
+ public function replyForm ($permalink = '{{permalink}}', $file = '{{file}}', $subscribed = true)
+ {
+ // Create HashOver reply form
+ $reply_form = new HTMLTag ('div', array (
+ 'class' => 'hashover-balloon'
+ ));
+
+ // If avatars are enabled
+ if ($this->setup->iconMode !== 'none') {
+ // Create avatar element for HashOver reply form
+ $reply_avatar = new HTMLTag ('div', array (
+ 'class' => 'hashover-avatar-image'
+ ));
+
+ // Add count element to avatar element
+ $reply_avatar->appendChild ($this->avatar ('+'));
+
+ // Add avatar element to inputs wrapper element
+ $reply_form->appendChild ($reply_avatar);
+ }
+
+ // Display default login inputs when logged out
+ if ($this->login->userIsLoggedIn === false) {
+ $reply_login_inputs = $this->loginInputs ($permalink);
+ $reply_form->appendChild ($reply_login_inputs);
+ }
+
+ // Create label element for comment textarea
+ if ($this->setup->usesLabels === true) {
+ $reply_comment_label = new HTMLTag ('label', array (
+ 'for' => 'hashover-reply-comment-' . $permalink,
+ 'class' => 'hashover-comment-label',
+ 'innerHTML' => $this->locale->text['reply-to-comment']
+ ), false);
+
+ // Add comment label to form element
+ $reply_form->appendChild ($reply_comment_label);
+ }
+
+ // Reply form locale
+ $reply_form_placeholder = $this->locale->text['reply-form'];
+
+ // Create reply textarea element and add it to form element
+ $this->commentForm ($reply_form, 'reply', $reply_form_placeholder, '', $permalink);
+
+ // Add page info fields to reply form
+ $this->pageInfoFields ($reply_form);
+
+ // Create hidden reply to input element
+ if (!empty ($file)) {
+ $reply_to_input = new HTMLTag ('input', array (
+ 'type' => 'hidden',
+ 'name' => 'reply-to',
+ 'value' => $file
+ ), false, true);
+
+ // Add hidden reply to input element to form element
+ $reply_form->appendChild ($reply_to_input);
+ }
+
+ // Create reply form footer element
+ $reply_form_footer = new HTMLTag ('div', array (
+ 'class' => 'hashover-form-footer'
+ ));
+
+ // Create wrapper for form links
+ $reply_form_links_wrapper = new HTMLTag ('span', array (
+ 'class' => 'hashover-form-links'
+ ));
+
+ // Add checkbox label element to reply form footer element
+ if ($this->setup->fieldOptions['email'] !== false) {
+ if ($this->login->userIsLoggedIn === false or !empty ($this->login->email)) {
+ $subscribe_label = $this->subscribeLabel ($permalink, 'reply', $subscribed);
+ $reply_form_links_wrapper->appendChild ($subscribe_label);
+ }
+ }
+
+ // Create and add accepted HTML revealer hyperlink
+ if ($this->mode !== 'php') {
+ $reply_form_links_wrapper->appendChild ($this->acceptedFormatting ('reply', $permalink));
+ }
+
+ // Add reply form links wrapper to reply form footer element
+ $reply_form_footer->appendChild ($reply_form_links_wrapper);
+
+ // Create wrapper for form buttons
+ $reply_form_buttons_wrapper = new HTMLTag ('span', array (
+ 'class' => 'hashover-form-buttons'
+ ));
+
+ // Create "Cancel" link element
+ if ($this->setup->usesCancelButtons === true) {
+ // Add "Cancel" link element to reply form footer element
+ $reply_cancel_button = $this->cancelButton ('reply', $permalink);
+ $reply_form_buttons_wrapper->appendChild ($reply_cancel_button);
+ }
+
+ // Create "Post Comment" button element
+ $reply_post_button = new HTMLTag ('input', array (), false, true);
+
+ // Add ID attribute with JavaScript variable single quote break out
+ if (!empty ($permalink)) {
+ $reply_post_button->createAttribute ('id', 'hashover-reply-post-' . $permalink);
+ }
+
+ // Post reply locale
+ $post_reply = $this->locale->text['post-reply'];
+
+ // Continue with other attributes
+ $reply_post_button->createAttributes (array (
+ 'class' => 'hashover-submit hashover-reply-post',
+ 'type' => 'submit',
+ 'name' => 'post',
+ 'value' => $post_reply,
+ 'title' => $post_reply
+ ));
+
+ // Add "Post Comment" element to reply form footer element
+ $reply_form_buttons_wrapper->appendChild ($reply_post_button);
+
+ // Add reply form buttons wrapper to reply form footer element
+ $reply_form_footer->appendChild ($reply_form_buttons_wrapper);
+
+ // Add reply form footer to reply form element
+ $reply_form->appendChild ($reply_form_footer);
+
+ return $reply_form->asHTML ();
+ }
+
+ public function editForm ($permalink = '{{permalink}}', $file = '{{file}}', $name = '{{name}}', $website = '{{website}}', $body = '{{body}}', $status = '', $subscribed = true)
+ {
+ // "Edit Comment" locale string
+ $edit_comment = $this->locale->text['edit-comment'];
+
+ // "Save Edit" locale string
+ $save_edit = $this->locale->text['save'];
+
+ // "Cancel" locale string
+ $cancel_edit = $this->locale->text['cancel'];
+
+ // "Delete" locale string
+ if ($this->login->userIsAdmin === true) {
+ $delete_comment = $this->locale->text['permanently-delete'];
+ } else {
+ $delete_comment = $this->locale->text['delete'];
+ }
+
+ // Create wrapper element
+ $edit_form = new HTMLTag ('div');
+
+ // Create edit form title element
+ $edit_form_title = new HTMLTag ('div', array (
+ 'class' => 'hashover-title hashover-dashed-title',
+ 'innerHTML' => $edit_comment
+ ), false);
+
+ if ($this->login->userIsAdmin === true) {
+ // Create status dropdown wrapper element
+ $edit_status_wrapper = new HTMLTag ('span', array (
+ 'class' => 'hashover-edit-status',
+ 'innerHTML' => $this->locale->text['status']
+ ), false);
+
+ // Create select wrapper element
+ $edit_status_select_wrapper = new HTMLTag ('span', array (
+ 'class' => 'hashover-select-wrapper'
+ ), false);
+
+ // Status dropdown menu options
+ $status_options = array (
+ 'approved' => $this->locale->text['status-approved'],
+ 'pending' => $this->locale->text['status-pending'],
+ 'deleted' => $this->locale->text['status-deleted']
+ );
+
+ // Create status dropdown menu element
+ $edit_status_dropdown = new HTMLTag ('select', array (
+ 'id' => 'hashover-edit-status-' . $permalink,
+ 'name' => 'status',
+ 'size' => '1'
+ ));
+
+ foreach ($status_options as $value => $inner_html) {
+ // Create status dropdown menu option element
+ $edit_status_option = new HTMLTag ('option', array (
+ 'value' => $value,
+ 'innerHTML' => $inner_html
+ ));
+
+ // Set option as selected if it matches the comment status given
+ if ($value === $status) {
+ $edit_status_option->createAttribute ('selected', 'true');
+ }
+
+ // Add option element to status dropdown menu
+ $edit_status_dropdown->appendChild ($edit_status_option);
+ }
+
+ // Add status dropdown menu to select wrapper element
+ $edit_status_select_wrapper->appendChild ($edit_status_dropdown);
+
+ // Add select wrapper to status dropdown wrapper element
+ $edit_status_wrapper->appendChild ($edit_status_select_wrapper);
+
+ // Add status dropdown wrapper to edit form title element
+ $edit_form_title->appendChild ($edit_status_wrapper);
+ }
+
+ // Append edit form title to edit form wrapper
+ $edit_form->appendChild ($edit_form_title);
+
+ // Append default login inputs
+ $edit_login_inputs = $this->loginInputs ($permalink, true, $name, $website);
+ $edit_form->appendChild ($edit_login_inputs);
+
+ // Create label element for comment textarea
+ if ($this->setup->usesLabels === true) {
+ $edit_comment_label = new HTMLTag ('label', array (
+ 'for' => 'hashover-edit-comment-' . $permalink,
+ 'class' => 'hashover-comment-label',
+ 'innerHTML' => $this->locale->text['edit-your-comment']
+ ), false);
+
+ // Add comment label to form element
+ $edit_form->appendChild ($edit_comment_label);
+ }
+
+ // Comment form placeholder text
+ $edit_placeholder = $this->locale->text['comment-form'];
+
+ // Create edit textarea element and add it to form element
+ $this->commentForm ($edit_form, 'edit', $edit_placeholder, $body, $permalink);
+
+ // Add page info fields to edit form
+ $this->pageInfoFields ($edit_form);
+
+ // Create hidden comment file input element
+ $edit_file_input = new HTMLTag ('input', array (
+ 'type' => 'hidden',
+ 'name' => 'file',
+ 'value' => $file
+ ), false, true);
+
+ // Add hidden page title input element to form element
+ $edit_form->appendChild ($edit_file_input);
+
+ // Create wrapper element for edit form buttons
+ $edit_form_footer = new HTMLTag ('div', array (
+ 'class' => 'hashover-form-footer'
+ ));
+
+ // Create wrapper for form links
+ $edit_form_links_wrapper = new HTMLTag ('span', array (
+ 'class' => 'hashover-form-links'
+ ));
+
+ // Add checkbox label element to edit form buttons wrapper element
+ if ($this->setup->fieldOptions['email'] !== false) {
+ $subscribe_label = $this->subscribeLabel ($permalink, 'edit', $subscribed);
+ $edit_form_links_wrapper->appendChild ($subscribe_label);
+ }
+
+ // Create and add accepted HTML revealer hyperlink
+ if ($this->mode !== 'php') {
+ $edit_form_links_wrapper->appendChild ($this->acceptedFormatting ('edit', $permalink));
+ }
+
+ // Add edit form links wrapper to edit form footer element
+ $edit_form_footer->appendChild ($edit_form_links_wrapper);
+
+ // Create wrapper for form buttons
+ $edit_form_buttons_wrapper = new HTMLTag ('span', array (
+ 'class' => 'hashover-form-buttons'
+ ));
+
+ // Create "Cancel" link element
+ if ($this->setup->usesCancelButtons === true) {
+ // Add "Cancel" hyperlink element to edit form footer element
+ $edit_cancel_button = $this->cancelButton ('edit', $permalink);
+ $edit_form_buttons_wrapper->appendChild ($edit_cancel_button);
+ }
+
+ // Create "Post Comment" button element
+ $save_edit_button = new HTMLTag ('input', array (), false, true);
+
+ // Add ID attribute with JavaScript variable single quote break out
+ if (!empty ($permalink)) {
+ $save_edit_button->createAttribute ('id', 'hashover-edit-post-' . $permalink);
+ }
+
+ // Continue with other attributes
+ $save_edit_button->createAttributes (array (
+ 'class' => 'hashover-submit hashover-edit-post',
+ 'type' => 'submit',
+ 'name' => 'edit',
+ 'value' => $save_edit,
+ 'title' => $save_edit
+ ));
+
+ // Add "Save Edit" element to edit form footer element
+ $edit_form_buttons_wrapper->appendChild ($save_edit_button);
+
+ // Create "Delete" button element
+ $delete_button = new HTMLTag ('input', array (), false, true);
+
+ // Add ID attribute with JavaScript variable single quote break out
+ if (!empty ($permalink)) {
+ $delete_button->createAttribute ('id', 'hashover-edit-delete-' . $permalink);
+ }
+
+ // Continue with other attributes
+ $delete_button->createAttributes (array (
+ 'class' => 'hashover-submit hashover-edit-delete',
+ 'type' => 'submit',
+ 'name' => 'delete',
+ 'value' => $delete_comment,
+ 'title' => $delete_comment
+ ));
+
+ // Add "Delete" element to edit form footer element
+ $edit_form_buttons_wrapper->appendChild ($delete_button);
+
+ // Add edit form buttons wrapper to edit form footer element
+ $edit_form_footer->appendChild ($edit_form_buttons_wrapper);
+
+ // Add form buttons to edit form element
+ $edit_form->appendChild ($edit_form_footer);
+
+ return $edit_form->innerHTML;
+ }
+
+ // Creates thread hyperlink element
+ public function threadLink ($url = '{{url}}', $title = '{{title}}')
+ {
+ // Create hyperlink element
+ $thread_link = new HTMLTag ('a', array (
+ 'href' => $url,
+ 'innerHTML' => $title
+ ), false);
+
+ return $thread_link->asHTML ();
+ }
+}
diff --git a/bootstrap/comments/backend/classes/cookies.php b/bootstrap/comments/backend/classes/cookies.php
new file mode 100644
index 0000000..ec57d05
--- /dev/null
+++ b/bootstrap/comments/backend/classes/cookies.php
@@ -0,0 +1,128 @@
+<?php namespace HashOver;
+
+// Copyright (C) 2010-2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// HashOver is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// HashOver is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with HashOver. If not, see <http://www.gnu.org/licenses/>.
+
+
+class Cookies
+{
+ public $setup;
+ public $secure = false;
+
+ public function __construct (Setup $setup)
+ {
+ $this->setup = $setup;
+ $this->domain = $setup->domain;
+
+ // Remove port from domain
+ if (mb_strpos ($this->domain, ':') !== false) {
+ $this->domain = mb_substr ($this->domain, 0, strrpos ($this->domain, ':'));
+ }
+
+ // Transmit cookies over HTTPS if set so in Settings
+ if ($setup->secureCookies === true) {
+ $this->secure = !empty ($_SERVER['HTTPS']) ? true : false;
+ }
+ }
+
+ // Set a cookie with expiration date
+ public function set ($name, $value = '', $date = '')
+ {
+ $name = 'hashover-' . $name;
+
+ // Use specific expiration date or the one in Settings
+ $date = !empty ($date) ? $date : $this->setup->cookieExpiration;
+
+ // Set the cookie if cookies are enabled
+ if ($this->setup->setsCookies !== false) {
+ setcookie ($name, $value, $date, '/', $this->domain, $this->secure, true);
+ }
+ }
+
+ // Set cookies for remembering state login actions
+ public function setFailedOn ($input, $reply_to, $replied = true)
+ {
+ // Set success status cookie
+ $this->set ('failed-on', $input);
+
+ // Set reply cookie
+ if ($replied === true and !empty ($reply_to)) {
+ $this->set ('replied', $reply_to);
+ }
+
+ // Set comment text cookie
+ $comment = $this->setup->getRequest ('comment');
+
+ // Check if comment is set
+ if ($comment !== false) {
+ $this->set ('comment', $comment);
+ }
+ }
+
+ // Get cookie value
+ public function getValue ($name, $trim = false)
+ {
+ $name = 'hashover-' . $name;
+
+ // Check if it exists
+ if (!empty ($_COOKIE[$name])) {
+ $value = $_COOKIE[$name];
+
+ // Strip escaping backslashes from cookie value
+ if (get_magic_quotes_gpc ()) {
+ $value = stripslashes ($value);
+ }
+
+ // Return trimmed value if told to
+ if ($trim === true) {
+ $value = trim ($value, " \r\n\t");
+ }
+
+ return $value;
+ }
+
+ // If not set return null
+ return null;
+ }
+
+ // Expire a cookie
+ public function expireCookie ($cookie)
+ {
+ // Set its expiration date to 1 if it exists
+ if ($this->getValue ($cookie) !== null) {
+ $this->set ($cookie, '', 1);
+ }
+ }
+
+ // Expire HashOver's default cookies
+ public function clear ()
+ {
+ // Expire message cookie
+ $this->expireCookie ('message');
+
+ // Expire error cookie
+ $this->expireCookie ('error');
+
+ // Expire comment failure cookie
+ $this->expireCookie ('failed-on');
+
+ // Expire reply failure cookie
+ $this->expireCookie ('replied');
+
+ // Expire comment text cookie
+ $this->expireCookie ('comment');
+ }
+}
diff --git a/bootstrap/comments/backend/classes/database.php b/bootstrap/comments/backend/classes/database.php
new file mode 100644
index 0000000..70984db
--- /dev/null
+++ b/bootstrap/comments/backend/classes/database.php
@@ -0,0 +1,395 @@
+<?php namespace HashOver;
+
+// Copyright (C) 2010-2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// HashOver is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// HashOver is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with HashOver. If not, see <http://www.gnu.org/licenses/>.
+
+
+// Database! Database! Just living in the database! Wow! Wow!
+class Database
+{
+ public $setup;
+ public $storageMode;
+ public $database;
+
+ public function __construct (Setup $setup)
+ {
+ $this->setup = $setup;
+ $this->storageMode = $setup->databaseType;
+
+ try {
+ if ($setup->databaseType === 'sqlite') {
+ $sqlite_file = $setup->commentsDirectory . '/' . $setup->databaseName . '.sqlite';
+ $this->database = new \PDO ('sqlite:' . $sqlite_file);
+ } else {
+ $sql_connection = implode (';', array (
+ 'host=' . $setup->databaseHost,
+ 'dbname=' . $setup->databaseName,
+ 'charset=' . $setup->databaseCharset
+ ));
+
+ $this->database = new \PDO (
+ $setup->databaseType . ':' . $sql_connection,
+ $setup->databaseUser,
+ $setup->databasePassword
+ );
+ }
+ } catch (\PDOException $error) {
+ throw new \Exception ($error->getMessage ());
+ }
+ }
+
+ public function getCommentThread ($thread)
+ {
+ return ($thread !== 'auto') ? $thread : $this->setup->threadName;
+ }
+
+ // Gets the appropriate metadata table name
+ protected function getMetaTable ($name, $thread, $global)
+ {
+ // Check if we're getting metadata for a specific thread
+ if ($global !== true) {
+ // If so, use the thread's table
+ if ($thread === 'auto') {
+ $table = $this->setup->threadName . '/metadata';
+ } else {
+ $table = $thread . '/metadata';
+ }
+ } else {
+ // If not, use the global metadata table
+ $table = 'hashover-metadata';
+ }
+
+ // Final table name
+ $table .= '/' . $name;
+
+ return $table;
+ }
+
+ // Read and return specific metadata from JSON file
+ public function readMeta ($name, $thread = 'auto', $global = false)
+ {
+ // Metadata table
+ $metadata_table = $this->getMetaTable ($name, $thread, $global);
+
+ // Query statement array
+ $statement = 'SELECT * FROM `' . $metadata_table . '`';
+
+ // Query statement
+ $results = $this->database->query ($statement);
+
+ // Return threads if successful
+ if ($results !== false) {
+ $fetch_all = $results->fetchAll (\PDO::FETCH_ASSOC);
+
+ if (isset ($fetch_all[0]['items'])) {
+ $columns = array ();
+
+ foreach ($fetch_all as $column) {
+ foreach ($column as $key => $value) {
+ $columns[] = $value;
+ }
+ }
+
+ return $columns;
+ }
+
+ return $fetch_all[0];
+ }
+
+ return false;
+ }
+
+ // Create table creation statement from array
+ protected function creationStatement (array $columns)
+ {
+ $statement = array ();
+
+ // Check if the array is associative
+ if (array_keys ($columns) !== range (0, count ($columns) - 1)) {
+ // If so, create a statement using specific columns
+ foreach ($columns as $name => $value) {
+ $type = is_numeric ($value) ? 'INTEGER' : 'TEXT';
+ $statement[] = sprintf ('`%s` %s', $name, $type);
+ }
+ } else {
+ // If not, create statement using generic "items" column
+ $statement[] = '`items` TEXT';
+ }
+
+ return $statement;
+ }
+
+ // Prepare and execute an SQL statement
+ protected function executeStatement ($statement, $data = null)
+ {
+ try {
+ // Prepare statement
+ $prepare = $this->database->prepare ($statement);
+
+ // Attempt to execute statement
+ if ($prepare !== false) {
+ return $prepare->execute ($data);
+ }
+
+ return $prepare;
+
+ } catch (\PDOException $error) {
+ throw new \Exception ($error->getMessage ());
+ }
+ }
+
+ // Create comment table if it doesn't exist
+ protected function createTable ($name, array $columns)
+ {
+ // Statement for creating initial comment thread table
+ $statement = 'CREATE TABLE IF NOT EXISTS `' . $name . '` ';
+ $statement .= '(' . implode (', ', $columns) . ')';
+
+ // Execute statement
+ $created = $this->executeStatement ($statement);
+
+ // Throw exception on failure
+ if ($created === false) {
+ throw new \Exception (sprintf (
+ 'Failed to create table "%s"',
+ $this->setup->threadName
+ ));
+ }
+
+ return true;
+ }
+
+ // Create table query statement from array
+ protected function queryStatement (array $columns)
+ {
+ $statement = array ();
+
+ foreach ($columns as $name => $value) {
+ $statement[] = ':' . $name;
+ }
+
+ return $statement;
+ }
+
+ // Delete all rows from a given table
+ protected function deleteAllRows ($table)
+ {
+ // Deletion statement
+ $statement = 'DELETE FROM `' . $table . '`';
+
+ // Execute statement
+ $deleted = $this->executeStatement ($statement);
+
+ // Throw exception on failure
+ if ($deleted === false) {
+ throw new \Exception ('Failed to delete existing metadata!');
+ }
+ }
+
+ // Save metadata to specific metadata JSON file
+ public function saveMeta ($name, array $data, $thread = 'auto', $global = false)
+ {
+ // Metadata table
+ $metadata_table = $this->getMetaTable ($name, $thread, $global);
+
+ // Create metadata table creation statement
+ $creation_statement = $this->creationStatement ($data);
+
+ // Attempt to create metadata table
+ $this->createTable ($metadata_table, $creation_statement);
+
+ // Delete existing data from database
+ $this->deleteAllRows ($metadata_table);
+
+ // Check if the array is associative
+ if (array_keys ($data) !== range (0, count ($data) - 1)) {
+ // If so, create metadata columns insertion statement array
+ $columns = $this->queryStatement ($data);
+
+ // Insert data into specific columns
+ $save = 'INSERT INTO `' . $metadata_table . '` ';
+ $save .= 'VALUES (' . implode (', ', $columns) . ')';
+
+ // Execute statement
+ $saved = $this->executeStatement ($save, $data);
+ } else {
+ // If not, insert each item individually
+ $save = 'INSERT INTO `' . $metadata_table . '` ';
+ $save .= 'VALUES (:items)';
+
+ // Insert each item individually
+ for ($i = 0, $il = count ($data); $i < $il; $i++) {
+ // Execute statement
+ $saved = $this->executeStatement ($save, array (
+ 'items' => $data[$i]
+ ));
+
+ // Stop on any failures
+ if ($saved === false) {
+ break;
+ }
+ }
+ }
+
+ // Throw exception on failure
+ if ($saved === false) {
+ throw new \Exception ('Failed to save metadata!');
+ }
+ }
+
+ public function write ($action, $thread, array $array, $alt = false)
+ {
+ $thread = $this->getCommentThread ($thread);
+
+ switch ($action) {
+ // Action for posting a comment
+ case 'insert': {
+ $columns = implode (', ', array (
+ ':id',
+ ':body',
+ ':status',
+ ':date',
+ ':name',
+ ':password',
+ ':login_id',
+ ':email',
+ ':encryption',
+ ':email_hash',
+ ':notifications',
+ ':website',
+ ':ipaddr',
+ ':likes',
+ ':dislikes'
+ ));
+
+ $query = 'INSERT INTO `' . $thread . '` ';
+ $query .= 'VALUES (' . $columns . ')';
+
+ break;
+ }
+
+ // Action for editing a comment
+ case 'update': {
+ $columns = implode (', ', array (
+ 'body=:body',
+ 'status=:status',
+ 'name=:name',
+ 'password=:password',
+ 'email=:email',
+ 'encryption=:encryption',
+ 'email_hash=:email_hash',
+ 'notifications=:notifications',
+ 'website=:website',
+ 'likes=:likes',
+ 'dislikes=:dislikes'
+ ));
+
+ $query = 'UPDATE `' . $thread . '` ';
+ $query .= 'SET ' . $columns . ' ';
+ $query .= 'WHERE id=:id';
+
+ break;
+ }
+
+ // Action for deleting a comment
+ case 'delete': {
+ // Check if we're actually deleting the comment
+ if ($alt === true) {
+ // If so, use delete statement
+ $query = 'DELETE FROM `' . $thread . '` ';
+ $query .= 'WHERE id=:id';
+ } else {
+ // If not, use status update statement
+ $query = 'UPDATE `' . $thread . '` ';
+ $query .= 'SET status=:status ';
+ $query .= 'WHERE id=:id';
+ }
+
+ break;
+ }
+ }
+
+ // Execute statement
+ $queried = $this->executeStatement ($query, $array);
+
+ // Throw exception on failure
+ if ($queried === false) {
+ throw new \Exception ('Failed to write to database!');
+ }
+
+ return true;
+ }
+
+ // Create comment thread if it doesn't exist
+ public function checkThread ()
+ {
+ $thread = $this->setup->threadName;
+
+ return $this->createTable ($thread, array (
+ '`id` TEXT',
+ '`body` TEXT',
+ '`status` TEXT',
+ '`date` TEXT',
+ '`name` TEXT',
+ '`password` TEXT',
+ '`login_id` TEXT',
+ '`email` TEXT',
+ '`encryption` TEXT',
+ '`email_hash` TEXT',
+ '`notifications` TEXT',
+ '`website` TEXT',
+ '`ipaddr` TEXT',
+ '`likes` INTEGER',
+ '`dislikes` INTEGER'
+ ));
+ }
+
+ // Queries a list of comment threads
+ public function queryThreads ()
+ {
+ // Database name
+ $name = $this->setup->databaseName;
+
+ // Check if database type if SQLite
+ if ($this->setup->databaseType === 'sqlite') {
+ // If so, use SQLite statement
+ $statement = 'SELECT * FROM sqlite_master ';
+ $statement .= 'WHERE type=\'table\'';
+ } else {
+ // If not, use MySQL statement
+ $statement = 'SELECT * FROM INFORMATION_SCHEMA.TABLES ';
+ $statement .= 'WHERE TABLE_TYPE=\'BASE TABLE\' ';
+ $statement .= 'AND TABLE_SCHEMA=\'' . $name . '\'';
+ }
+
+ // Query statement
+ $results = $this->database->query ($statement);
+
+ // Return threads if successful
+ if ($results !== false) {
+ $fetch_all = $results->fetchAll (\PDO::FETCH_ASSOC);
+ $threads = array ();
+
+ foreach ($fetch_all as $table) {
+ $threads[] = $table['name'];
+ }
+
+ return $threads;
+ }
+
+ return false;
+ }
+}
diff --git a/bootstrap/comments/backend/classes/datafiles.php b/bootstrap/comments/backend/classes/datafiles.php
new file mode 100644
index 0000000..7e2eb1e
--- /dev/null
+++ b/bootstrap/comments/backend/classes/datafiles.php
@@ -0,0 +1,176 @@
+<?php namespace HashOver;
+
+// Copyright (C) 2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// HashOver is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// HashOver is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with HashOver. If not, see <http://www.gnu.org/licenses/>.
+
+
+class DataFiles
+{
+ public $setup;
+
+ public function __construct (Setup $setup)
+ {
+ $this->setup = $setup;
+ }
+
+ public function readJSON ($file)
+ {
+ // Read JSON comment file
+ $data = @file_get_contents ($file);
+
+ // Parse JSON comment file
+ $json = @json_decode ($data, true);
+
+ // Check for JSON parse error
+ if ($json !== null) {
+ return $json;
+ }
+
+ return false;
+ }
+
+ public function saveJSON ($file, array $contents = array ())
+ {
+ // Check if we have pretty print support
+ if (defined ('JSON_PRETTY_PRINT')) {
+ // If so, encode comment to JSON with pretty print
+ $json = json_encode ($contents, JSON_PRETTY_PRINT);
+
+ // And convert spaces indentation to tabs
+ $json = str_replace (' ', "\t", $json);
+ } else {
+ // If not, encode comment to JSON normally
+ $json = json_encode ($contents);
+ }
+
+ // Convert line endings to OS specific style
+ $json = $this->osLineEndings ($json);
+
+ // Save the JSON data to the comment file
+ if (@file_put_contents ($file, $json)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ // Gets the appropriate thread directory path
+ protected function getMetaRoot ($thread)
+ {
+ // Using the automatically setup thread if told to
+ if ($thread === 'auto') {
+ return $this->setup->threadDirectory;
+ }
+
+ // Otherwise construct thread directory path
+ return $this->setup->pagesDirectory . '/' . $thread;
+ }
+
+ // Gets the appropriate metadata directory path
+ protected function getMetaDirectory ($thread, $global)
+ {
+ // Check if we're getting metadata for a specific thread
+ if ($global !== true) {
+ // If so, use the thread's path
+ $directory = $this->getMetaRoot ($thread) . '/metadata';
+ } else {
+ // If not, use the global metadata path
+ $directory = $this->setup->commentsDirectory . '/metadata';
+ }
+
+ // Return metadata directory path
+ return $directory;
+ }
+
+ // Gets the appropriate metadata file path
+ protected function getMetaPath ($name, $thread, $global)
+ {
+ // Metadata directory path
+ $directory = $this->getMetaDirectory ($thread, $global);
+
+ // Metadata file path
+ $path = $directory . '/' . $name . '.json';
+
+ // Return metadata file path
+ return $path;
+ }
+
+ // Creates metadata directory if it doesn't exist
+ protected function setupMeta ($thread, $global)
+ {
+ // Metadata directory path
+ $metadata = $this->getMetaDirectory ($thread, $global);
+
+ // Check if metadata root directory exists
+ if (file_exists ($this->getMetaRoot ($thread))) {
+ // If so, attempt to create metadata directory
+ if (file_exists ($metadata) or @mkdir ($metadata, 0755, true)) {
+ // If successful, set permissions to 0755 again
+ @chmod ($metadata, 0755);
+ return true;
+ }
+
+ // Otherwise throw exception
+ throw new \Exception (sprintf (
+ 'Failed to create metadata directory at: "%s"!',
+ $metadata
+ ));
+ }
+ }
+
+ // Read and return specific metadata from JSON file
+ public function readMeta ($name, $thread = 'auto', $global = false)
+ {
+ // Metadata JSON file path
+ $metadata_path = $this->getMetaPath ($name, $thread, $global);
+
+ // Return metadata if read successfully
+ return $this->readJSON ($metadata_path);
+ }
+
+ // Save metadata to specific metadata JSON file
+ public function saveMeta ($name, array $data, $thread = 'auto', $global = false)
+ {
+ // Metadata JSON file path
+ $metadata_path = $this->getMetaPath ($name, $thread, $global);
+
+ // Create metadata directory if it doesn't exist
+ $this->setupMeta ($thread, $global);
+
+ // Check if metadata root directory exists
+ if (file_exists ($this->getMetaRoot ($thread))) {
+ // If so, attempt to save data to metadata JSON file
+ if ($this->saveJSON ($metadata_path, $data) === false) {
+ // Throw exception on failure
+ throw new \Exception ('Failed to save metadata!');
+ }
+ }
+
+ return true;
+ }
+
+ // Convert a string to OS-specific line endings
+ public function osLineEndings ($string)
+ {
+ // Different line ending styles
+ $eol_styles = array ("\r\n", "\r", "\n");
+
+ // Actual consersion
+ $string = str_replace ($eol_styles, PHP_EOL, $string);
+
+ return $string;
+ }
+}
diff --git a/bootstrap/comments/backend/classes/defaultlogin.php b/bootstrap/comments/backend/classes/defaultlogin.php
new file mode 100644
index 0000000..0eda0ea
--- /dev/null
+++ b/bootstrap/comments/backend/classes/defaultlogin.php
@@ -0,0 +1,110 @@
+<?php namespace HashOver;
+
+// Copyright (C) 2015-2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// HashOver is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// HashOver is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with HashOver. If not, see <http://www.gnu.org/licenses/>.
+
+
+class DefaultLogin
+{
+ public $setup;
+ public $encryption;
+ public $cookies;
+ public $locale;
+ public $enabled = true;
+ public $name;
+ public $password;
+ public $loginHash;
+ public $email;
+ public $website;
+
+ public function __construct (Setup $setup, Cookies $cookies, Locale $locale)
+ {
+ $this->setup = $setup;
+ $this->encryption = $setup->encryption;
+ $this->cookies = $cookies;
+ $this->locale = $locale;
+
+ // Disable login if cookies are disabled
+ if ($setup->setsCookies === false) {
+ $this->enabled = false;
+ $setup->allowsLogin = false;
+ $setup->syncSettings ();
+ }
+ }
+
+ // Set login credentials
+ public function setCredentials ()
+ {
+ // Set login cookies
+ $this->cookies->set ('name', $this->name);
+ $this->cookies->set ('password', $this->password);
+ $this->cookies->set ('website', $this->website);
+
+ // Check if an email was given
+ if (!empty ($this->email)) {
+ // If so, generate encrypted string / decryption keys from e-mail
+ $email = $this->encryption->encrypt ($this->email);
+
+ // And set e-mail and encryption cookies
+ $this->cookies->set ('email', $email['encrypted']);
+ $this->cookies->set ('encryption', $email['keys']);
+ } else {
+ // If not, expire e-mail and encryption cookies
+ $this->cookies->expireCookie ('email');
+ $this->cookies->expireCookie ('encryption');
+ }
+ }
+
+ // Get login credentials
+ public function getCredentials ()
+ {
+ // Get user name via cookie
+ $this->name = $this->cookies->getValue ('name', true);
+
+ // Get user password via cookie
+ $this->password = $this->cookies->getValue ('password', true);
+
+ // Decrypt email cookie
+ $encrypted_email = $this->cookies->getValue ('email', true);
+ $encryption = $this->cookies->getValue ('encryption', true);
+ $email = $this->encryption->decrypt ($encrypted_email, $encryption);
+
+ // Validate e-mail address
+ if (filter_var ($email, FILTER_VALIDATE_EMAIL)) {
+ $this->email = trim ($email, " \r\n\t");
+ }
+
+ // Get user website via cookie
+ $this->website = $this->cookies->getValue ('website', true);
+
+ // Get login hash via cookie
+ $this->loginHash = $this->cookies->getValue ('login', true);
+ }
+
+ // Main login method
+ public function setLogin ()
+ {
+ // Set login cookie
+ $this->cookies->set ('login', $this->loginHash);
+ }
+
+ // Main logout method
+ public function clearLogin ()
+ {
+ // Expire login cookie
+ $this->cookies->expireCookie ('login');
+ }
+}
diff --git a/bootstrap/comments/backend/classes/encryption.php b/bootstrap/comments/backend/classes/encryption.php
new file mode 100644
index 0000000..2e21f48
--- /dev/null
+++ b/bootstrap/comments/backend/classes/encryption.php
@@ -0,0 +1,191 @@
+<?php namespace HashOver;
+
+// Copyright (C) 2010-2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// HashOver is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// HashOver is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with HashOver. If not, see <http://www.gnu.org/licenses/>.
+
+
+// Encryption methods
+class Encryption
+{
+ protected $prefix;
+ protected $cost = '$10$';
+ protected $encryptionHash;
+ protected $ivSize;
+ protected $cipher = 'aes-128-cbc';
+ protected $options;
+ protected $alphabet = 'aAbBcCdDeEfFgGhHiIjJkKlLmM.nNoOpPqQrRsStTuUvVwWxXyYzZ/0123456789';
+
+ public function __construct ($encryption_key)
+ {
+ // Throw exception if encryption key isn't at least 8 characters long
+ if (mb_strlen ($encryption_key, '8bit') < 8) {
+ throw new Exception ('Encryption key must by at least 8 characters long.');
+ }
+
+ // Blowfish prefix
+ $this->prefix = (version_compare (PHP_VERSION, '5.3.7') < 0) ? '$2a' : '$2y';
+
+ // OpenSSL raw output/options
+ $this->options = defined ('OPENSSL_RAW_DATA') ? OPENSSL_RAW_DATA : true;
+
+ // SHA-256 hash array
+ $this->encryptionHash = str_split (hash ('sha256', $encryption_key));
+
+ // OpenSSL cipher IV
+ $this->ivSize = openssl_cipher_iv_length ($this->cipher);
+ }
+
+ // Creates Blowfish hash for passwords
+ public function createHash ($string)
+ {
+ // Alphanumeric character array
+ $alphabet = str_split ($this->alphabet);
+
+ // Shuffle alphanumeric character array as to randomize it
+ shuffle ($alphabet);
+
+ // Initial salt
+ $salt = '';
+
+ // Generate random 20 character alphanumeric string
+ foreach (array_rand ($alphabet, 20) as $character) {
+ $salt .= $alphabet[$character];
+ }
+
+ // Blowfish hash
+ $hash = crypt ($string, $this->prefix . $this->cost . $salt . '$$');
+
+ // Return hashed string
+ return $hash;
+ }
+
+ // Creates Blowfish hash with salt from supplied hash
+ public function verifyHash ($string, $compare)
+ {
+ // Split string by dollar sign
+ $parts = explode ('$', $compare);
+
+ // Hash salt
+ $salt = !empty ($parts[3]) ? $parts[3] : '';
+
+ // Encryption string as Blowfish hash
+ $hash = crypt ($string, $this->prefix . $this->cost . $salt . '$$');
+
+ // Returns true if both match
+ return ($hash === $compare);
+ }
+
+ // Generates a random encryption key
+ public function createKey ($string)
+ {
+ // Shuffle alphanumeric character array as to randomize it
+ shuffle ($string);
+
+ // Initial array keys
+ $keys = array ();
+
+ // Initial key
+ $key = '';
+
+ // Generate random string from encryption key SHA-256 hash
+ for ($k = 0; $k < 16; $k++) {
+ // Add encryption key character index to keys array
+ $keys[] = array_search ($string[$k], $this->encryptionHash);
+
+ // Add encryption key character to key
+ $key .= $string[$k];
+ }
+
+ // Return random string and list of encryption hash array keys
+ return array (
+ 'key' => $key,
+ 'keys' => join (',', $keys)
+ );
+ }
+
+ // OpenSSL encrypt with random key from SHA-256 hash for e-mails
+ public function encrypt ($string)
+ {
+ // Get a random encryption key
+ $key_pair = $this->createKey ($this->encryptionHash);
+
+ // Get pseudo-random bytes for OpenSSL IV
+ $iv = openssl_random_pseudo_bytes ($this->ivSize);
+
+ // OpenSSL encrypt using random encryption key
+ $ciphertext = openssl_encrypt (
+ $string,
+ $this->cipher,
+ $key_pair['key'],
+ $this->options,
+ $iv
+ );
+
+ // Return encrypted value and list of encryption hash array keys
+ return array (
+ 'encrypted' => base64_encode ($iv . $ciphertext),
+ 'keys' => $key_pair['keys']
+ );
+ }
+
+ // Decrypt OpenSSL encrypted string
+ public function decrypt ($string, $keys)
+ {
+ // Return false if string or keys is empty
+ if (empty ($string) or empty ($keys)) {
+ return false;
+ }
+
+ // Initial key
+ $key = '';
+
+ // Split keys string into array
+ $keys = explode (',', $keys);
+
+ // Retrieve random key from array
+ foreach ($keys as $value) {
+ // Cast key to integer
+ $hash_key = (int)($value);
+
+ // Give up if any array value isn't valid
+ if (!isset ($this->encryptionHash[$hash_key])) {
+ return '';
+ }
+
+ // Add character to decryption key
+ $key .= $this->encryptionHash[$hash_key];
+ }
+
+ // Decode base64 encoded string
+ $decode = base64_decode ($string, true);
+
+ // Setup OpenSSL IV
+ $iv = mb_substr ($decode, 0, $this->ivSize, '8bit');
+
+ // Setup OpenSSL cipher text
+ $full_length = mb_strlen ($decode, '8bit');
+ $deciphertext = mb_substr ($decode, $this->ivSize, $full_length, '8bit');
+
+ // Return OpenSSL decrypted string
+ return openssl_decrypt (
+ $deciphertext,
+ $this->cipher,
+ $key,
+ $this->options,
+ $iv
+ );
+ }
+}
diff --git a/bootstrap/comments/backend/classes/formui.php b/bootstrap/comments/backend/classes/formui.php
new file mode 100644
index 0000000..eacb8e4
--- /dev/null
+++ b/bootstrap/comments/backend/classes/formui.php
@@ -0,0 +1,1093 @@
+<?php namespace HashOver;
+
+// Copyright (C) 2015-2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// HashOver is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// HashOver is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with HashOver. If not, see <http://www.gnu.org/licenses/>.
+
+
+class FormUI
+{
+ public $setup;
+ public $mode;
+ public $locale;
+ public $avatars;
+ public $misc;
+ public $cookies;
+ public $login;
+ public $commentCounts;
+ public $pageTitle;
+ public $pageURL;
+ public $postCommentOn;
+ public $popularComments;
+ public $comments;
+
+ protected $emphasizedField;
+ protected $defaultLoginInputs;
+
+ public function __construct (Setup $setup, array $counts)
+ {
+ $this->setup = $setup;
+ $this->mode = $setup->usage['mode'];
+ $this->locale = new Locale ($setup);
+ $this->login = new Login ($setup);
+ $this->avatars = new Avatars ($setup);
+ $this->misc = new Misc ($this->mode);
+ $this->cookies = new Cookies ($setup);
+ $this->commentCounts = $counts;
+ $this->pageTitle = $this->setup->pageTitle;
+ $this->pageURL = $this->setup->pageURL;
+
+ // Attempt to get form field submission failed on
+ $failedField = $this->cookies->getValue ('failed-on');
+
+ // Set the field to emphasize after a failed post
+ if ($failedField !== null) {
+ $this->emphasizedField = $failedField;
+ }
+
+ // "Post a comment" locale strings
+ // $post_comment_on = $this->locale->text['post-comment-on'];
+ // $this->postCommentOn = $post_comment_on[0];
+
+ // Add optional "on <page title>" to "Post a comment" title
+ if ($this->setup->displaysTitle !== false and !empty ($this->pageTitle)) {
+ $this->postCommentOn = sprintf ($post_comment_on[1], $this->pageTitle);
+ }
+
+ // Create default login inputs elements
+ $this->defaultLoginInputs = $this->loginInputs ();
+ }
+
+ // Re-encode a URL
+ protected function safeURLEncode ($url)
+ {
+ return urlencode (urldecode ($url));
+ }
+
+ // Creates input elements for user login information
+ protected function loginInputs ($permalink = '', $edit_form = false, $name = '', $website = '')
+ {
+ $permalink = !empty ($permalink) ? '-' . $permalink : '';
+
+ // Login input attribute information
+ $login_input_attributes = array (
+ 'name' => array (
+ 'wrapper-class' => 'hashover-name-input',
+ 'label-class' => 'hashover-name-label',
+ 'placeholder' => $this->locale->text['name'],
+ 'input-id' => 'hashover-main-name' . $permalink,
+ 'input-type' => 'text',
+ 'input-name' => 'name',
+ 'input-title' => $this->locale->text['name-tip'],
+ 'input-value' => $this->misc->makeXSSsafe ($this->login->name)
+ ),
+
+ 'password' => array (
+ 'wrapper-class' => 'hashover-password-input',
+ 'label-class' => 'hashover-password-label',
+ 'placeholder' => $this->locale->text['password'],
+ 'input-id' => 'hashover-main-password' . $permalink,
+ 'input-type' => 'password',
+ 'input-name' => 'password',
+ 'input-title' => $this->locale->text['password-tip'],
+ 'input-value' => ''
+ ),
+
+ 'email' => array (
+ 'wrapper-class' => 'hashover-email-input',
+ 'label-class' => 'hashover-email-label',
+ 'placeholder' => $this->locale->text['email'],
+ 'input-id' => 'hashover-main-email' . $permalink,
+ 'input-type' => 'email',
+ 'input-name' => 'email',
+ 'input-title' => $this->locale->text['email-tip'],
+ 'input-value' => $this->misc->makeXSSsafe ($this->login->email)
+ ),
+
+ 'website' => array (
+ 'wrapper-class' => 'hashover-website-input',
+ 'label-class' => 'hashover-website-label',
+ 'placeholder' => $this->locale->text['website'],
+ 'input-id' => 'hashover-main-website' . $permalink,
+ 'input-type' => 'url',
+ 'input-name' => 'website',
+ 'input-title' => $this->locale->text['website-tip'],
+ 'input-value' => $this->misc->makeXSSsafe ($this->login->website)
+ )
+ );
+
+ // Change input values to specified values
+ if ($edit_form === true) {
+ $login_input_attributes['name']['input-value'] = $name;
+ $login_input_attributes['password']['placeholder'] = $this->locale->text['confirm-password'];
+ $login_input_attributes['password']['input-title'] = $this->locale->text['confirm-password'];
+ $login_input_attributes['website']['input-value'] = $website;
+ }
+
+ // Create wrapper element for styling login inputs
+ $login_inputs = new HTMLTag ('div', array (
+ 'class' => 'hashover-inputs'
+ ));
+
+ // Create and append login input elements to main form inputs wrapper element
+ foreach ($login_input_attributes as $field => $attributes) {
+ // Skip disabled input tags
+ if ($this->setup->fieldOptions[$field] === false) {
+ continue;
+ }
+
+ // Create cell element for inputs
+ $input_cell = new HTMLTag ('div', array (
+ 'class' => 'hashover-input-cell'
+ ));
+
+ if ($this->setup->usesLabels === true) {
+ // Create label element for input
+ $label = new HTMLTag ('label', array (
+ 'for' => $attributes['input-id'],
+ 'class' => $attributes['label-class'],
+ 'innerHTML' => $attributes['placeholder']
+ ), false);
+
+ // Add label to cell element
+ $input_cell->appendChild ($label);
+ }
+
+ // Create wrapper element for input
+ $input_wrapper = new HTMLTag ('div', array (
+ 'class' => $attributes['wrapper-class']
+ ));
+
+ // Add a class for indicating a required field
+ if ($this->setup->fieldOptions[$field] === 'required') {
+ $input_wrapper->appendAttribute ('class', 'hashover-required-input');
+ }
+
+ // Add a class for indicating a post failure
+ if ($this->emphasizedField === $field) {
+ $input_wrapper->appendAttribute ('class', 'hashover-emphasized-input');
+ }
+
+ // Create input element
+ $input = new HTMLTag ('input', array (
+ 'id' => $attributes['input-id'],
+ 'class' => 'hashover-input-info',
+ 'type' => $attributes['input-type'],
+ 'name' => $attributes['input-name'],
+ 'value' => $attributes['input-value'],
+ 'title' => $attributes['input-title'],
+ 'placeholder' => $attributes['placeholder']
+ ), false, true);
+
+ // Add input to wrapper element
+ $input_wrapper->appendChild ($input);
+
+ // Add input to cell element
+ $input_cell->appendChild ($input_wrapper);
+
+ // Add input cell to main inputs wrapper element
+ $login_inputs->appendChild ($input_cell);
+ }
+
+ return $login_inputs;
+ }
+
+ protected function avatar ($text)
+ {
+ // If avatars set to images
+ if ($this->setup->iconMode === 'image') {
+ // Logged in
+ if ($this->login->userIsLoggedIn === true) {
+ // Image source is avatar image
+ $hash = !empty ($this->login->email) ? md5 (mb_strtolower (trim ($this->login->email))) : '';
+ $avatar_src = $this->avatars->getGravatar ($hash);
+ } else {
+ // Logged out
+ // Image source is local default image
+ $avatar_src = $this->setup->getImagePath ('first-comment');
+ }
+
+ // Create avatar image element
+ $avatar = new HTMLTag ('div', array (
+ 'style' => 'background-image: url(\'' . $avatar_src . '\');'
+ ), false);
+ } else {
+ // Avatars set to count
+ // Create element displaying comment number user will be
+ $avatar = new HTMLTag ('span', $text, false);
+ }
+
+ return $avatar;
+ }
+
+ protected function subscribeLabel ($id = '', $type = 'main', $checked = true)
+ {
+ // Create subscribe checkbox label element
+ $subscribe_label = new HTMLTag ('label', array (
+ 'for' => 'hashover-' . $type . '-subscribe',
+ 'class' => 'hashover-' . $type . '-label',
+ 'title' => $this->locale->text['subscribe-tip']
+ ));
+
+ if (!empty ($id)) {
+ $subscribe_label->appendAttribute ('for', '-' . $id, false);
+ }
+
+ // Create subscribe element checkbox
+ $subscribe = new HTMLTag ('input', array (
+ 'id' => 'hashover-' . $type . '-subscribe',
+ 'type' => 'checkbox',
+ 'name' => 'subscribe'
+ ), false, true);
+
+ if (!empty ($id)) {
+ $subscribe->appendAttribute ('id', '-' . $id, false);
+ }
+
+ // Check checkbox
+ if ($checked === true) {
+ $subscribe->createAttribute ('checked', 'true');
+ }
+
+ // Add subscribe checkbox element to subscribe checkbox label element
+ $subscribe_label->appendChild ($subscribe);
+
+ // Add text to subscribe checkbox label element
+ $subscribe_label->appendInnerHTML ($this->locale->text['subscribe']);
+
+ return $subscribe_label;
+ }
+
+ protected function acceptedFormatCell ($format, $locale_key)
+ {
+ $title = new HTMLTag ('p', array ('class' => 'hashover-title'));
+ $accepted_format = sprintf ($this->locale->text['accepted-format'], $format);
+ $title->innerHTML ($accepted_format);
+
+ $paragraph = new HTMLTag ('p');
+ $paragraph->innerHTML ($this->locale->text[$locale_key]);
+
+ return new HTMLTag ('div', array (
+ 'children' => array ($title, $paragraph)
+ ));
+ }
+
+ protected function commentForm (HTMLTag $form, $type, $placeholder, $text, $permalink = '')
+ {
+ $permalink = !empty ($permalink) ? '-' . $permalink : '';
+ $title_locale = ($type === 'reply') ? 'reply-form' : 'comment-form';
+
+ // Create textarea
+ $textarea = new HTMLTag ('textarea', array (
+ 'id' => 'hashover-' . $type . '-comment' . $permalink,
+ 'class' => 'hashover-textarea hashover-' . $type . '-textarea',
+ 'cols' => '63',
+ 'name' => 'comment',
+ 'rows' => '6',
+ 'title' => $this->locale->text[$title_locale]
+ ), false);
+
+ // Set the placeholder attribute if one is given
+ if (!empty ($placeholder)) {
+ $textarea->createAttribute ('placeholder', $placeholder);
+ }
+
+ if ($type === 'main') {
+ // Add a class for indicating a post failure
+ if ($this->emphasizedField === 'comment') {
+ $textarea->appendAttribute ('class', 'hashover-emphasized-input');
+ }
+
+ // If the comment was a reply, have the textarea use the reply textarea locale
+ if ($this->cookies->getValue ('replied') !== null) {
+ $reply_form_placeholder = $this->locale->text['reply-form'];
+ $textarea->createAttribute ('placeholder', $reply_form_placeholder);
+ }
+ }
+
+ // Set textarea content if given any text
+ if (!empty ($text)) {
+ $textarea->innerHTML ($text);
+ }
+
+ // Add textarea element to form element
+ $form->appendChild ($textarea);
+
+ // Create element for various messages when needed
+ if ($type !== 'main') {
+ $message = new HTMLTag ('div', array (
+ 'id' => 'hashover-' . $type . '-message-container' . $permalink,
+ 'class' => 'hashover-message',
+
+ 'children' => array (
+ new HTMLTag ('div', array (
+ 'id' => 'hashover-' . $type . '-message' . $permalink
+ ))
+ )
+ ));
+
+ // Add message element to form element
+ $form->appendChild ($message);
+ }
+
+ // Create accepted HTML message element
+ $accepted_formatting_message = new HTMLTag ('div', array (
+ 'id' => 'hashover-' . $type . '-formatting-message' . $permalink,
+ 'class' => 'hashover-formatting-message'
+ ));
+
+ // Create formatting table
+ $accepted_formatting_table = new HTMLTag ('div', array (
+ 'class' => 'hashover-formatting-table',
+
+ 'children' => array (
+ $this->acceptedFormatCell ('HTML', 'accepted-html')
+ )
+ ));
+
+ // Append Markdown cell if Markdown is enabled
+ if ($this->setup->usesMarkdown !== false) {
+ $markdown_cell = $this->acceptedFormatCell ('Markdown', 'accepted-markdown');
+ $accepted_formatting_table->appendChild ($markdown_cell);
+ }
+
+ // Append formatting table to formatting message
+ $accepted_formatting_message->appendChild ($accepted_formatting_table);
+
+ // Ensure the accepted HTML message is open in PHP mode
+ if ($this->mode === 'php') {
+ $accepted_formatting_message->appendAttribute ('class', 'hashover-message-open');
+ $accepted_formatting_message->appendAttribute ('class', 'hashover-php-message-open');
+ }
+
+ // Add accepted HTML message element to form element
+ // $form->appendChild ($accepted_formatting_message);
+ }
+
+ protected function pageInfoFields (HTMLTag $form)
+ {
+ // Create hidden comment thread input element
+ $thread_input = new HTMLTag ('input', array (
+ 'type' => 'hidden',
+ 'name' => 'thread',
+ 'value' => $this->setup->threadName
+ ), false, true);
+
+ // Add hidden comments thread input element to form element
+ $form->appendChild ($thread_input);
+
+ // Create hidden page URL input element
+ $url_input = new HTMLTag ('input', array (
+ 'type' => 'hidden',
+ 'name' => 'url',
+ 'value' => $this->pageURL
+ ), false, true);
+
+ // Add hidden page URL input element to form element
+ $form->appendChild ($url_input);
+
+ // Create hidden page title input element
+ $title_input = new HTMLTag ('input', array (
+ 'type' => 'hidden',
+ 'name' => 'title',
+ 'value' => $this->pageTitle
+ ), false, true);
+
+ // Add hidden page title input element to form element
+ $form->appendChild ($title_input);
+
+ // Check if the script is being remotely accessed
+ if ($this->setup->remoteAccess === true) {
+ // Create hidden input element indicating remote access
+ $remote_access_input = new HTMLTag ('input', array (
+ 'type' => 'hidden',
+ 'name' => 'remote-access',
+ 'value' => 'true'
+ ), false, true);
+
+ // Add remote access input element to form element
+ $form->appendChild ($remote_access_input);
+ }
+ }
+
+ protected function acceptedFormatting ($type, $permalink = '')
+ {
+ $permalink = !empty ($permalink) ? '-' . $permalink : '';
+ $accepted_format = $this->locale->text['comment-formatting'];
+
+ // Create accepted HTML message revealer hyperlink
+ $accepted_formatting = new HTMLTag ('span', array (
+ 'id' => 'hashover-' . $type . '-formatting' . $permalink,
+ 'class' => 'hashover-fake-link hashover-formatting',
+ 'title' => $accepted_format,
+ 'innerHTML' => $accepted_format
+ ));
+
+ // Return the hyperlink
+ return $accepted_formatting;
+ }
+
+ public function initialHTML ($hashover_wrapper = true)
+ {
+ // Create element that HashOver comments will appear in
+ $hashover_element = new HTMLTag ('div', array (
+ 'id' => 'hashover',
+ 'class' => 'hashover'
+ ), false);
+
+ // Add class for differentiating desktop and mobile styling
+ if ($this->setup->isMobile === true) {
+ $hashover_element->appendAttribute ('class', 'hashover-mobile');
+ } else {
+ $hashover_element->appendAttribute ('class', 'hashover-desktop');
+ }
+
+ // Add class to indicate user login status
+ if ($this->login->userIsLoggedIn === true) {
+ $hashover_element->appendAttribute ('class', 'hashover-logged-in');
+ } else {
+ $hashover_element->appendAttribute ('class', 'hashover-logged-out');
+ }
+
+ // Create element for jump anchor
+ $jump_anchor = new HTMLTag ('div', array (
+ 'id' => 'comments'
+ ));
+
+ // Add jump anchor to HashOver element
+ $hashover_element->appendChild ($jump_anchor);
+
+ // Create primary form wrapper element
+ $form_section = new HTMLTag ('div', array (
+ 'id' => 'hashover-form-section'
+ ));
+
+ // Hide primary form wrapper if comments are to be initially hidden
+ if ($this->mode !== 'php' and $this->setup->collapsesInterface === true) {
+ $form_section->createAttribute ('style', 'display: none;');
+ }
+
+ // Create element for "Post Comment" title
+ $post_title = new HTMLTag ('span', array (
+ 'class' => array (
+ 'hashover-title',
+ 'hashover-main-title',
+ 'hashover-dashed-title'
+ ),
+
+ 'innerHTML' => $this->postCommentOn
+ ));
+
+ // Add "Post Comment" element to primary form wrapper
+ $form_section->appendChild ($post_title);
+
+ // Create element for various messages when needed
+ $message_container = new HTMLTag ('div', array (
+ 'id' => 'hashover-message-container',
+ 'class' => 'hashover-title hashover-message'
+ ));
+
+ // Create element for message text
+ $message_element = new HTMLTag ('div', array (
+ 'id' => 'hashover-message'
+ ));
+
+ // Check if message cookie is set
+ if ($this->cookies->getValue ('message') !== null
+ or $this->cookies->getValue ('error') !== null)
+ {
+ // If so, set the message element to open in PHP mode
+ if ($this->mode === 'php') {
+ $message_container->appendAttribute ('class', array (
+ 'hashover-message-open',
+ 'hashover-php-message-open'
+ ));
+ }
+
+ // Check if the message is a normal message
+ if ($this->cookies->getValue ('message') !== null) {
+ // If so, get an XSS safe version of the message
+ $message = $this->misc->makeXSSsafe ($this->cookies->getValue ('message'));
+ } else {
+ // If not, get an XSS safe version of the error message
+ $message = $this->misc->makeXSSsafe ($this->cookies->getValue ('error'));
+
+ // And set a class to the message element indicating an error
+ $message_container->appendAttribute ('class', 'hashover-message-error');
+ }
+
+ // And put current message into message element
+ $message_element->innerHTML ($message);
+ }
+
+ // Add message text element to message element
+ $message_container->appendChild ($message_element);
+
+ // Add message element to primary form wrapper
+ $form_section->appendChild ($message_container);
+
+ // Create main HashOver form
+ $main_form = new HTMLTag ('form', array (
+ 'id' => 'hashover-form',
+ 'class' => 'hashover-balloon',
+ 'name' => 'hashover-form',
+ 'action' => '/formactions',
+ 'method' => 'post'
+ ));
+
+ // Create wrapper element for styling inputs
+ $main_inputs = new HTMLTag ('div', array (
+ 'class' => 'hashover-inputs'
+ ));
+
+ // If avatars are enabled
+ if ($this->setup->iconMode !== 'none') {
+ // Create avatar element for main HashOver form
+ $main_avatar = new HTMLTag ('div', array (
+ 'class' => 'hashover-avatar-image'
+ ));
+
+ // Add count element to avatar element
+ $main_avatar->appendChild ($this->avatar ($this->commentCounts['primary']));
+
+ // Add avatar element to inputs wrapper element
+ $main_inputs->appendChild ($main_avatar);
+ }
+
+ // Logged in
+ if ($this->login->userIsLoggedIn === true) {
+ if (!empty ($this->login->name)) {
+ $user_name = $this->misc->makeXSSsafe ($this->login->name);
+ } else {
+ $user_name = $this->setup->defaultName;
+ }
+
+ $user_website = $this->misc->makeXSSsafe ($this->login->website);
+ $name_class = 'hashover-name-plain';
+ $is_twitter = false;
+
+ // Check if user's name is a Twitter handle
+ if ($user_name[0] === '@') {
+ $user_name = mb_substr ($user_name, 1);
+ $name_class = 'hashover-name-twitter';
+ $is_twitter = true;
+ $name_length = mb_strlen ($user_name);
+
+ if ($name_length > 1 and $name_length <= 30) {
+ if (empty ($user_website)) {
+ $user_website = 'http://twitter.com/' . $user_name;
+ }
+ }
+ }
+
+ // Create element for logged user's name
+ $main_form_column_spanner = new HTMLTag ('div', array (
+ 'class' => 'hashover-comment-name hashover-top-name'
+ ), false);
+
+ // Check if user gave website
+ if (!empty ($user_website)) {
+ if ($is_twitter === false) {
+ $name_class = 'hashover-name-website';
+ }
+
+ // Create link to user's website
+ $main_form_hyperlink = new HTMLTag ('a', array (
+ 'href' => $user_website,
+ 'rel' => 'noopener noreferrer',
+ 'target' => '_blank',
+ 'title' => $user_name,
+ 'innerHTML' => $user_name
+ ), false);
+
+ // Add username hyperlink to main form column spanner
+ $main_form_column_spanner->appendChild ($main_form_hyperlink);
+ } else {
+ // No website
+ $main_form_column_spanner->innerHTML ($user_name);
+ }
+
+ // Set classes user's name spanner
+ $main_form_column_spanner->appendAttribute ('class', $name_class);
+
+ // Add main form column spanner to inputs wrapper element
+ $main_inputs->appendChild ($main_form_column_spanner);
+ } else {
+ // Logged out
+ // Use default login inputs
+ $main_inputs->appendInnerHTML ($this->defaultLoginInputs->innerHTML);
+ }
+
+ // Add inputs wrapper to form element
+ $main_form->appendChild ($main_inputs);
+
+ // Create fake "required fields" element as a SPAM trap
+ $required_fields = new HTMLTag ('div', array (
+ 'id' => 'hashover-requiredFields'
+ ));
+
+ $fake_fields = array (
+ 'summary' => 'hidden',
+ 'age' => 'hidden',
+ 'lastname' => 'hidden',
+ 'address' => 'hidden',
+ 'zip' => 'hidden',
+ );
+
+ // Create and append fake input elements to fake required fields
+ foreach ($fake_fields as $name => $type) {
+ $fake_input = new HTMLTag ('input', array (
+ 'type' => $type,
+ 'name' => $name,
+ 'value' => ''
+ ), false, true);
+
+ // Add fake summary input element to fake required fields
+ $required_fields->appendInnerHTML ($fake_input->asHTML ());
+ }
+
+ // Add fake input elements to form element
+ $main_form->appendChild ($required_fields);
+
+ // Post button locale
+ $post_button = $this->locale->text['post-button'];
+
+ // Create label element for comment textarea
+ if ($this->setup->usesLabels === true) {
+ $main_comment_label = new HTMLTag ('label', array (
+ 'for' => 'hashover-main-comment',
+ 'class' => 'hashover-comment-label',
+ 'innerHTML' => $post_button
+ ), false);
+
+ // Add comment label to form element
+ $main_form->appendChild ($main_comment_label);
+ }
+
+ // Get comment text if a comment cookie is set
+ $comment_text = $this->misc->makeXSSsafe ($this->cookies->getValue ('comment'));
+
+ // Comment form placeholder text
+ $comment_form = $this->locale->text['comment-form'];
+
+ // Create main textarea element and add it to form element
+ $this->commentForm ($main_form, 'main', $comment_form, $comment_text);
+
+ // Add page info fields to main form
+ $this->pageInfoFields ($main_form);
+
+ // Check if comment is a failed reply
+ if ($this->cookies->getValue ('replied') !== null) {
+ // If so, get the comment being replied to
+ $replied = $this->cookies->getValue ('replied');
+
+ // Create hidden reply to input element
+ $reply_to_input = new HTMLTag ('input', array (
+ 'type' => 'hidden',
+ 'name' => 'reply-to',
+ 'value' => $this->misc->makeXSSsafe ($replied)
+ ), false, true);
+
+ // And add hidden reply to input element to form element
+ $main_form->appendChild ($reply_to_input);
+ }
+
+ // Create wrapper element for main form footer
+ $main_form_footer = new HTMLTag ('div', array (
+ 'class' => 'hashover-form-footer'
+ ));
+
+ // Create wrapper for form links
+ $main_form_links_wrapper = new HTMLTag ('span', array (
+ 'class' => 'hashover-form-links'
+ ));
+
+ // Add checkbox label element to main form buttons wrapper element
+ // if ($this->setup->fieldOptions['email'] !== false) {
+ // if ($this->login->userIsLoggedIn === false or !empty ($this->login->email)) {
+ // $main_form_links_wrapper->appendChild ($this->subscribeLabel ());
+ // }
+ // }
+
+ // Create and add accepted HTML revealer hyperlink
+ if ($this->mode !== 'php') {
+ $main_form_links_wrapper->appendChild ($this->acceptedFormatting ('main'));
+ }
+
+ // Add main form links wrapper to main form footer element
+ $main_form_footer->appendChild ($main_form_links_wrapper);
+
+ // Create wrapper for form buttons
+ $main_form_buttons_wrapper = new HTMLTag ('span', array (
+ 'class' => 'hashover-form-buttons'
+ ));
+
+ // Create "Login" / "Logout" button element
+ if ($this->setup->allowsLogin !== false or $this->login->userIsLoggedIn === true) {
+ $login_button = new HTMLTag ('input', array (
+ 'id' => 'hashover-login-button',
+ 'class' => 'hashover-submit',
+ 'type' => 'submit'
+ ), false, true);
+
+ // Check login state
+ if ($this->login->userIsLoggedIn === true) {
+ // Logged in
+ $login_button->appendAttribute ('class', 'hashover-logout');
+ $logout_locale = $this->locale->text['logout'];
+
+ // Create logged in attributes
+ $login_button->createAttributes (array (
+ 'name' => 'logout',
+ 'value' => $logout_locale,
+ 'title' => $logout_locale
+ ));
+ } else {
+ // Logged out
+ $login_button->appendAttribute ('class', 'hashover-login');
+
+ // Create logged out attributes
+ $login_button->createAttributes (array (
+ 'name' => 'login',
+ 'value' => $this->locale->text['login'],
+ 'title' => $this->locale->text['login-tip']
+ ));
+ }
+
+ // Add "Login" / "Logout" element to main form footer element
+ $main_form_buttons_wrapper->appendChild ($login_button);
+ }
+
+ // Create "Post Comment" button element
+ $main_post_button = new HTMLTag ('input', array (
+ 'id' => 'hashover-post-button',
+ 'class' => 'hashover-submit hashover-post-button',
+ 'type' => 'submit',
+ 'name' => 'post',
+ 'value' => $post_button,
+ 'title' => $post_button
+ ), false, true);
+
+ // Add "Post Comment" element to main form buttons wrapper element
+ $main_form_buttons_wrapper->appendChild ($main_post_button);
+
+ // Add main form button wrapper to main form footer element
+ $main_form_footer->appendChild ($main_form_buttons_wrapper);
+
+ // Add main form footer to main form element
+ $main_form->appendChild ($main_form_footer);
+
+ // Add main form element to primary form wrapper
+ $form_section->appendChild ($main_form);
+
+ // Check if form position setting set to 'top'
+ if ($this->setup->formPosition !== 'bottom') {
+ // Add primary form wrapper to HashOver element
+ $hashover_element->appendChild ($form_section);
+ }
+
+ if ($this->commentCounts['popular'] > 0) {
+ // Create wrapper element for popular comments
+ $popular_section = new HTMLTag ('div', array (
+ 'id' => 'hashover-popular-section'
+ ), false);
+
+ // Hide popular comments wrapper if comments are to be initially hidden
+ if ($this->mode !== 'php') {
+ if ($this->setup->collapsesInterface === true or $this->setup->collapseLimit <= 0) {
+ $popular_section->createAttribute ('style', 'display: none;');
+ }
+ }
+
+ // Create wrapper element for popular comments title
+ $pop_count_wrapper = new HTMLTag ('div', array (
+ 'class' => 'hashover-dashed-title'
+ ));
+
+ // Create element for popular comments title
+ $pop_count_element = new HTMLTag ('span', array (
+ 'class' => 'hashover-title'
+ ));
+
+ // Add popular comments title text
+ $popular_plural = ($this->commentCounts['popular'] !== 1) ? 1 : 0;
+ $popular_comments_locale = $this->locale->text['popular-comments'];
+ $pop_count_element->innerHTML ($popular_comments_locale[$popular_plural]);
+
+ // Add popular comments title element to wrapper element
+ $pop_count_wrapper->appendChild ($pop_count_element);
+
+ // Add popular comments title wrapper element to popular comments section
+ $popular_section->appendChild ($pop_count_wrapper);
+
+ // Create element for popular comments to appear in
+ $popular_comments = new HTMLTag ('div', array (
+ 'id' => 'hashover-top-comments'
+ ), false);
+
+ // Add comments to HashOver element
+ if (!empty ($this->popularComments)) {
+ $popular_comments->innerHTML (trim ($this->popularComments));
+ }
+
+ // Add popular comments element to popular comments section
+ $popular_section->appendChild ($popular_comments);
+
+ // Add popular comments section to HashOver element
+ $hashover_element->appendChild ($popular_section);
+ }
+
+ // Create wrapper element for comments
+ $comments_section = new HTMLTag ('div', array (
+ 'id' => 'hashover-comments-section'
+ ), false);
+
+ // Create wrapper element for comment count and sort dropdown menu
+ $count_sort_wrapper = new HTMLTag ('div', array (
+ 'id' => 'hashover-count-wrapper',
+ 'class' => 'hashover-count-sort hashover-dashed-title'
+ ));
+
+ // Create element for comment count
+ $count_element = new HTMLTag ('span', array (
+ 'id' => 'hashover-count'
+ ));
+
+ // Add comment count to comment count element
+ if ($this->commentCounts['total'] > 1) {
+ $count_element->innerHTML ($this->commentCounts['show-count']);
+ }
+
+ // Add comment count element to wrapper element
+ $comments_section->appendChild ($count_element);
+
+ // JavaScript mode specific HTML
+ if ($this->mode !== 'php') {
+ // Hide wrapper if comments are to be initially hidden
+ if ($this->setup->collapsesInterface === true) {
+ $comments_section->createAttribute ('style', 'display: none;');
+ }
+
+ // Hide comment count if collapse limit is set at zero
+ if ($this->setup->collapseLimit <= 0 or $this->commentCounts['total'] <= 1) {
+ $count_sort_wrapper->createAttribute ('style', 'display: none;');
+ }
+
+ if ($this->commentCounts['total'] > 2) {
+ // Create wrapper element for sort dropdown menu
+ $sort_wrapper = new HTMLTag ('span', array (
+ 'id' => 'hashover-sort',
+ 'class' => 'hashover-select-wrapper'
+ ));
+
+ // Create sort dropdown menu element
+ $sort_select = new HTMLTag ('select', array (
+ 'id' => 'hashover-sort-select',
+ 'name' => 'sort',
+ 'size' => '1',
+ 'title' => $this->locale->text['sort']
+ ));
+
+ // Array of select tag sort options
+ $sort_options = array (
+ array ('value' => 'ascending', 'innerHTML' => $this->locale->text['sort-ascending']),
+ array ('value' => 'descending', 'innerHTML' => $this->locale->text['sort-descending']),
+ array ('value' => 'by-date', 'innerHTML' => $this->locale->text['sort-by-date']),
+ array ('value' => 'by-likes', 'innerHTML' => $this->locale->text['sort-by-likes']),
+ array ('value' => 'by-replies', 'innerHTML' => $this->locale->text['sort-by-replies']),
+ array ('value' => 'by-name', 'innerHTML' => $this->locale->text['sort-by-name'])
+ );
+
+ // Create sort options for sort dropdown menu element
+ for ($i = 0, $il = count ($sort_options); $i < $il; $i++) {
+ $option = new HTMLTag ('option', array (
+ 'value' => $sort_options[$i]['value'],
+ 'innerHTML' => $sort_options[$i]['innerHTML']
+ ), false);
+
+ // Add sort option element to sort dropdown menu
+ $sort_select->appendChild ($option);
+ }
+
+ // Create empty option group as spacer
+ $spacer_optgroup = new HTMLTag ('optgroup', array (
+ 'label' => '&nbsp;'
+ ));
+
+ // Add spacer option group to sort dropdown menu
+ $sort_select->appendChild ($spacer_optgroup);
+
+ // Create option group for threaded sort options
+ $threaded_optgroup = new HTMLTag ('optgroup', array (
+ 'label' => $this->locale->text['sort-threads']
+ ));
+
+ // Array of select tag threaded sort options
+ $threaded_sort_options = array (
+ array ('value' => 'threaded-descending', 'innerHTML' => $this->locale->text['sort-descending']),
+ array ('value' => 'threaded-by-date', 'innerHTML' => $this->locale->text['sort-by-date']),
+ array ('value' => 'threaded-by-likes', 'innerHTML' => $this->locale->text['sort-by-likes']),
+ array ('value' => 'by-popularity', 'innerHTML' => $this->locale->text['sort-by-popularity']),
+ array ('value' => 'by-discussion', 'innerHTML' => $this->locale->text['sort-by-discussion']),
+ array ('value' => 'threaded-by-name', 'innerHTML' => $this->locale->text['sort-by-name'])
+ );
+
+ // Create sort options for sort dropdown menu element
+ for ($i = 0, $il = count ($threaded_sort_options); $i < $il; $i++) {
+ $option = new HTMLTag ('option', array (
+ 'value' => $threaded_sort_options[$i]['value'],
+ 'innerHTML' => $threaded_sort_options[$i]['innerHTML']
+ ), false);
+
+ // Add sort option element to threaded option group
+ $threaded_optgroup->appendChild ($option);
+ }
+
+ // Add threaded sort options group to sort dropdown menu
+ $sort_select->appendChild ($threaded_optgroup);
+
+ // Add sort dropdown menu element to sort wrapper element
+ $sort_wrapper->appendChild ($sort_select);
+
+ // Add comment count element to wrapper element
+ $count_sort_wrapper->appendChild ($sort_wrapper);
+ }
+ }
+
+ // Add comment count and sort dropdown menu wrapper to comments section
+ $comments_section->appendChild ($count_sort_wrapper);
+
+ // Create element that will hold the comments
+ $sort_div = new HTMLTag ('div', array (
+ 'id' => 'hashover-sort-section'
+ ), false);
+
+ // Add comments to HashOver element
+ if (!empty ($this->comments)) {
+ $sort_div->innerHTML (trim ($this->comments));
+ }
+
+ // Add comments element to comments section
+ $comments_section->appendChild ($sort_div);
+
+ // Add comments element to HashOver element
+ $hashover_element->appendChild ($comments_section);
+
+ // Check if form position setting set to 'bottom'
+ if ($this->setup->formPosition === 'bottom') {
+ // Add primary form wrapper to HashOver element
+ $hashover_element->appendChild ($form_section);
+ }
+
+ // Create end links wrapper element
+ $end_links_wrapper = new HTMLTag ('div', array (
+ 'id' => 'hashover-end-links'
+ ));
+
+ // Hide end links wrapper if comments are to be initially hidden
+ if ($this->mode !== 'php' and $this->setup->collapsesInterface === true) {
+ $end_links_wrapper->createAttribute ('style', 'display: none;');
+ }
+
+ // HashOver Comments hyperlink text
+ $homepage_link_text = $this->locale->text['hashover-comments'];
+
+ // Create link back to HashOver homepage (fixme! get a real page!)
+ $homepage_link = new HTMLTag ('a', array (
+ 'href' => 'http://tildehash.com/?page=hashover',
+ 'id' => 'hashover-home-link',
+ 'target' => '_blank',
+ 'title' => $homepage_link_text,
+ 'innerHTML' => $homepage_link_text
+ ), false);
+
+ // Add link back to HashOver homepage to end links wrapper element
+ // $end_links_wrapper->innerHTML ($homepage_link->asHTML () . ' &#8210;');
+
+ // End links array
+ $end_links = array ();
+
+ if ($this->commentCounts['total'] > 1) {
+ if ($this->setup->appendsRss === true
+ and $this->setup->apiStatus ('rss') !== 'disabled')
+ {
+ // Create RSS feed link
+ $rss_link = new HTMLTag ('a', array (), false);
+ $rss_link->createAttribute ('href', $this->setup->getHttpPath ('api/rss.php'));
+ $rss_link->appendAttribute ('href', '?url=' . $this->safeURLEncode ($this->setup->pageURL), false);
+
+ // RSS Feed hyperlink text
+ $rss_link_text = $this->locale->text['rss-feed'];
+
+ $rss_link->createAttributes (array (
+ 'id' => 'hashover-rss-link',
+ 'target' => '_blank',
+ 'title' => $rss_link_text,
+ 'innerHTML' => $rss_link_text
+ ));
+
+ // Add RSS hyperlink to end links array
+ $end_links[] = $rss_link->asHTML ();
+ }
+ }
+
+ // Source Code hyperlink text
+ // $source_link_text = $this->locale->text['source-code'];
+
+ // Create link to HashOver source code (fixme! can be done better)
+ // $source_link = new HTMLTag ('a', array (
+ // 'href' => $this->setup->getBackendPath ('source-viewer.php'),
+ // 'id' => 'hashover-source-link',
+ // 'rel' => 'hashover-source',
+ // 'target' => '_blank',
+ // 'title' => $source_link_text,
+ // 'innerHTML' => $source_link_text
+ // ), false);
+
+ // Add source code hyperlink to end links array
+ // $end_links[] = $source_link->asHTML ();
+
+ // if ($this->mode !== 'php') {
+ // // Create link to HashOver JavaScript source code
+ // $javascript_link = new HTMLTag ('a', array (
+ // 'href' => $this->setup->getHttpPath ('comments.php'),
+ // 'id' => 'hashover-javascript-link',
+ // 'rel' => 'hashover-javascript',
+ // 'target' => '_blank',
+ // 'title' => 'JavaScript'
+ // ), false);
+
+ // Add JavaScript code hyperlink text
+ // $javascript_link->innerHTML ('JavaScript');
+
+ // Add JavaScript hyperlink to end links array
+ // $end_links[] = $javascript_link->asHTML ();
+ // }
+
+ // Add end links to end links wrapper element
+ $end_links_wrapper->appendInnerHTML (implode (' &middot;' . PHP_EOL, $end_links));
+
+ // Add end links wrapper element to HashOver element
+ $hashover_element->appendChild ($end_links_wrapper);
+
+ // Return all HTML with the HashOver wrapper element
+ if ($hashover_wrapper === true) {
+ return $hashover_element->asHTML ();
+ }
+
+ // Return just the HashOver wrapper element's innerHTML
+ return $hashover_element->innerHTML;
+ }
+}
diff --git a/bootstrap/comments/backend/classes/hashover.php b/bootstrap/comments/backend/classes/hashover.php
new file mode 100644
index 0000000..3ab0fdd
--- /dev/null
+++ b/bootstrap/comments/backend/classes/hashover.php
@@ -0,0 +1,440 @@
+<?php
+
+// Copyright (C) 2015-2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// HashOver is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// HashOver is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with HashOver. If not, see <http://www.gnu.org/licenses/>.
+
+
+class HashOver
+{
+ public $usage = array ();
+ public $statistics;
+ public $misc;
+ public $setup;
+ public $thread;
+ public $locale;
+ public $commentParser;
+ public $markdown;
+ public $cookies;
+ public $commentCount;
+ public $popularList = array ();
+ public $popularCount = 0;
+ public $rawComments = array ();
+ public $comments = array ();
+ public $ui;
+ public $templater;
+
+ public function __construct ($mode = 'php', $context = 'normal')
+ {
+ // Store usage context information
+ $this->usage['mode'] = $mode;
+ $this->usage['context'] = $context;
+
+ // Instantiate and start statistics
+ $this->statistics = new HashOver\Statistics ($mode);
+ $this->statistics->executionStart ();
+
+ // Instantiate general setup class
+ $this->setup = new HashOver\Setup ($this->usage);
+
+ // Instantiate class for reading comments
+ $this->thread = new HashOver\Thread ($this->setup);
+
+ // Instantiate class of miscellaneous functions
+ $this->misc = new HashOver\Misc ($mode);
+ }
+
+ public function getCommentCount ($locale_key = 'showing-comments')
+ {
+ // Shorter variables
+ $primary_count = $this->thread->primaryCount;
+ $total_count = $this->thread->totalCount;
+
+ // Subtract deleted comment counts
+ if ($this->setup->countIncludesDeleted === false) {
+ $primary_count -= $this->thread->primaryDeletedCount;
+ $total_count -= $this->thread->totalDeletedCount;
+ }
+
+ // Decide which locale to use; Exclude "Showing" in API usages
+ $locale_key = ($this->usage['context'] === 'api') ? 'count-link' : $locale_key;
+
+ // Decide if comment count is pluralized
+ $prime_plural = ($primary_count !== 2) ? 1 : 0;
+
+ // Get appropriate locale
+ $showing_comments_locale = $this->locale->text[$locale_key];
+ $showing_comments = $showing_comments_locale[$prime_plural];
+
+ // Whether to show reply count separately
+ if ($this->setup->showsReplyCount === true) {
+ // If so, inject top level comment count into count locale string
+ $comment_count = sprintf ($showing_comments, $primary_count - 1);
+
+ // Check if there are any replies
+ if ($total_count !== $primary_count) {
+ // If so, decide if reply count is pluralized
+ $count_diff = $total_count - $primary_count;
+ $reply_plural = ($count_diff !== 1) ? 1 : 0;
+
+ // Get appropriate locale
+ $reply_locale = $this->locale->text['count-replies'][$reply_plural];
+
+ // Inject total comment count into reply count locale string
+ $reply_count = sprintf ($reply_locale, $total_count - 1);
+
+ // And append reply count
+ $comment_count .= ' (' . $reply_count . ')';
+ }
+
+ // And return count with separate reply count
+ return $comment_count;
+ }
+
+ // Otherwise inject total comment count into count locale string
+ return sprintf ($showing_comments, $total_count - 1);
+ }
+
+ // Begin initialization work
+ public function initiate ()
+ {
+ // Query a list of comments
+ $this->thread->queryComments ();
+
+ // Where to stop reading comments
+ if ($this->usage['mode'] !== 'php'
+ and $this->setup->collapsesComments !== false
+ and $this->setup->popularityLimit <= 0
+ and $this->setup->usesAjax !== false)
+ {
+ // Use collapse limit when collapsing and AJAX is enabled
+ $end = $this->setup->collapseLimit;
+ } else {
+ // Otherwise read all comments
+ $end = null;
+ }
+
+ // TODO: Fix structure when using starting point
+ $this->rawComments = $this->thread->read (0, $end);
+
+ // Instantiate locales class
+ $this->locale = new HashOver\Locale ($this->setup);
+
+ // Instantiate cookies class
+ $this->cookies = new HashOver\Cookies ($this->setup);
+
+ // Instantiate login class
+ $this->login = new HashOver\Login ($this->setup);
+
+ // Instantiate comment parser class
+ $this->commentParser = new HashOver\CommentParser ($this->setup);
+
+ // Generate comment count
+ $this->commentCount = $this->getCommentCount ();
+
+ // Instantiate markdown class
+ $this->markdown = new HashOver\Markdown ();
+ }
+
+ // Save various metadata about the page
+ public function defaultMetadata ()
+ {
+ // "localhost" equivalent addresses
+ $addresses = array ('127.0.0.1', '::1', 'localhost');
+
+ // Check if local metadata is disabled
+ if ($this->setup->allowLocalMetadata !== true) {
+ // If so, do nothing if we're on localhost
+ if (in_array ($_SERVER['REMOTE_ADDR'], $addresses, true)) {
+ return;
+ }
+ }
+
+ // Attempt to save default page metadata
+ $this->thread->data->saveMeta ('page-info', array (
+ 'url' => $this->setup->pageURL,
+ 'title' => $this->setup->pageTitle
+ ));
+ }
+
+ // Get reply array from comments via key
+ protected function &getRepliesLevel (&$level, $level_count, &$key_parts)
+ {
+ for ($i = 1; $i < $level_count; $i++) {
+ if (isset ($level)) {
+ $level =& $level['replies'][$key_parts[$i] - 1];
+ }
+ }
+
+ return $level;
+ }
+
+ // Adds a comment to the popular list if it has enough likes
+ protected function checkPopularity (array $comment, $key, array $key_parts)
+ {
+ $popularity = 0;
+
+ // Add number of likes to popularity value
+ if (!empty ($comment['likes'])) {
+ $popularity += $comment['likes'];
+ }
+
+ // Subtract number of dislikes to popularity value
+ if ($this->setup->allowsDislikes === true) {
+ if (!empty ($comment['dislikes'])) {
+ $popularity -= $comment['dislikes'];
+ }
+ }
+
+ // Add comment to popular comments list if popular enough
+ if ($popularity >= $this->setup->popularityThreshold) {
+ $this->popularList[] = array (
+ 'popularity' => $popularity,
+ 'comment' => $comment,
+ 'key' => $key,
+ 'parts' => $key_parts
+ );
+ }
+ }
+
+ // Parse primary comments
+ public function parsePrimary ($start = 0)
+ {
+ // Initial comments array
+ $this->comments['primary'] = array ();
+
+ // If no comments were found, setup a default message comment
+ // if ($this->thread->totalCount <= 1) {
+ // $this->comments['primary'][] = array (
+ // 'title' => $this->locale->text['be-first-name'],
+ // 'avatar' => $this->setup->getImagePath ('first-comment'),
+ // 'permalink' => 'c1',
+ // 'notice' => $this->locale->text['be-first-note'],
+ // 'notice-class' => 'hashover-first'
+ // );
+
+ // return;
+ // }
+
+ // Last existing comment date for sorting deleted comments
+ $last_date = 0;
+
+ // Allowed comment count
+ $allowed_count = 0;
+
+ // Where to stop reading comments
+ if ($this->usage['mode'] !== 'php'
+ and $this->setup->collapsesComments !== false
+ and $this->setup->usesAjax !== false)
+ {
+ // Use collapse limit when collapsing and AJAX is enabled
+ $end = $this->setup->collapseLimit;
+ } else {
+ // Otherwise read all comments
+ $end = null;
+ }
+
+ // Run all comments through parser
+ foreach ($this->rawComments as $key => $comment) {
+ $key_parts = explode ('-', $key);
+ $indentions = count ($key_parts);
+ $status = 'approved';
+
+ // Check comment's popularity
+ if ($this->setup->popularityLimit > 0) {
+ $this->checkPopularity ($comment, $key, $key_parts);
+ }
+
+ // Stop parsing after end point
+ if ($end !== null and $allowed_count >= $end) {
+ continue;
+ }
+
+ if ($indentions > 1 and $this->setup->streamDepth > 0) {
+ $level =& $this->comments['primary'][$key_parts[0] - 1];
+
+ if ($this->setup->replyMode === 'stream'
+ and $indentions > $this->setup->streamDepth)
+ {
+ $level =& $this->getRepliesLevel ($level, $this->setup->streamDepth, $key_parts);
+ $level =& $level['replies'][];
+ } else {
+ $level =& $this->getRepliesLevel ($level, $indentions, $key_parts);
+ }
+ } else {
+ $level =& $this->comments['primary'][];
+ }
+
+ // Set status to what's stored in the comment
+ if (!empty ($comment['status'])) {
+ $status = $comment['status'];
+ }
+
+ switch ($status) {
+ // Parse as pending notice, viewable and editable by owner and admin
+ case 'pending': {
+ $parsed = $this->commentParser->parse ($comment, $key, $key_parts, false);
+
+ if (!isset ($parsed['editable'])) {
+ $level = $this->commentParser->notice ('pending', $key, $last_date);
+ break;
+ }
+
+ $last_date = $parsed['sort-date'];
+ $level = $parsed;
+
+ break;
+ }
+
+ // Parse as deletion notice, viewable and editable by admin
+ case 'deleted': {
+ if ($this->login->userIsAdmin === true) {
+ $level = $this->commentParser->parse ($comment, $key, $key_parts, false);
+ $last_date = $level['sort-date'];
+ } else {
+ $level = $this->commentParser->notice ('deleted', $key, $last_date);
+ }
+
+ break;
+ }
+
+ // Parse as deletion notice, non-existent comment
+ case 'missing': {
+ $level = $this->commentParser->notice ('deleted', $key, $last_date);
+ break;
+ }
+
+ // Parse as an unknown/error notice
+ case 'read-error': {
+ $level = $this->commentParser->notice ('error', $key, $last_date);
+ break;
+ }
+
+ // Otherwise parse comment normally
+ default: {
+ $comment['status'] = 'approved';
+ $level = $this->commentParser->parse ($comment, $key, $key_parts);
+ $last_date = $level['sort-date'];
+
+ break;
+ }
+ }
+
+ $allowed_count++;
+ }
+
+ // Reset array keys
+ $this->comments['primary'] = array_values ($this->comments['primary']);
+ }
+
+ // Parse popular comments
+ public function parsePopular ()
+ {
+ // Initial popular comments array
+ $this->comments['popular'] = array ();
+
+ // If no comments or popularity limit is 0, return void
+ if ($this->thread->totalCount <= 1
+ or $this->setup->popularityLimit <= 0)
+ {
+ return;
+ }
+
+ // Sort popular comments
+ usort ($this->popularList, function ($a, $b) {
+ return ($b['popularity'] > $a['popularity']);
+ });
+
+ // Calculate how many popular comments will be shown
+ $limit = $this->setup->popularityLimit;
+ $count = count ($this->popularList);
+ $this->popularCount = min ($limit, $count);
+
+ // Run through each popular comment
+ for ($i = 0; $i < $this->popularCount; $i++) {
+ $item =& $this->popularList[$i];
+
+ // Parse comment
+ $parsed = $this->commentParser->parse ($item['comment'], $item['key'], $item['parts'], true);
+
+ // And add it to popular comments
+ $this->comments['popular'][$i] = $parsed;
+ }
+ }
+
+ // Do final initialization work
+ public function finalize ()
+ {
+ // Expire various temporary cookies
+ $this->cookies->clear ();
+
+ // Various comment count numbers
+ $commentCounts = array (
+ 'show-count' => $this->commentCount,
+ 'primary' => $this->thread->primaryCount,
+ 'total' => $this->thread->totalCount,
+ 'popular' => $this->popularCount
+ );
+
+ // Instantiate UI output class
+ $this->ui = new HashOver\CommentsUI (
+ $this->setup,
+ $commentCounts
+ );
+
+ // Instantiate comment theme templater class
+ $this->templater = new HashOver\Templater (
+ $this->usage['mode'],
+ $this->setup
+ );
+ }
+
+ // Display all comments as HTML
+ public function displayComments ()
+ {
+ // Set/update default page metadata
+ $this->defaultMetadata ();
+
+ // Instantiate PHP mode class
+ $phpmode = new HashOver\PHPMode (
+ $this->setup,
+ $this->ui,
+ $this->comments
+ );
+
+ // Run popular comments through parser
+ if (!empty ($this->comments['popular'])) {
+ foreach ($this->comments['popular'] as $comment) {
+ $this->ui->popularComments .= $phpmode->parseComment ($comment, null, true) . PHP_EOL;
+ }
+ }
+
+ // Run primary comments through parser
+ if (!empty ($this->comments['primary'])) {
+ foreach ($this->comments['primary'] as $comment) {
+ $this->ui->comments .= $phpmode->parseComment ($comment, null) . PHP_EOL;
+ }
+ }
+
+ // Start UI output with initial HTML
+ $html = $this->ui->initialHTML ();
+
+ // End statistics and add them as code comment
+ $html .= $this->statistics->executionEnd ();
+
+ // Return final HTML
+ return $html;
+ }
+}
diff --git a/bootstrap/comments/backend/classes/htmltag.php b/bootstrap/comments/backend/classes/htmltag.php
new file mode 100644
index 0000000..79d5db1
--- /dev/null
+++ b/bootstrap/comments/backend/classes/htmltag.php
@@ -0,0 +1,282 @@
+<?php namespace HashOver;
+
+// Copyright (C) 2015-2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// HashOver is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// HashOver is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with HashOver. If not, see <http://www.gnu.org/licenses/>.
+
+
+class HTMLTag
+{
+ protected $tag;
+ protected $usesPrettyPrint = true;
+ protected $isSingleton;
+ protected $attributes = array ();
+ protected $children = array ();
+
+ public function __construct ($tag = '', $attributes = '', $pretty = true, $singleton = false, $spaced = true)
+ {
+ if (!is_string ($tag) or !$this->isWord ($tag)) {
+ $this->throwError ('Tag must have a single word String value.');
+ return;
+ }
+
+ if (!is_bool ($singleton)) {
+ $this->throwError ('Singleton parameter must have a Boolean value.');
+ return;
+ }
+
+ if (!is_bool ($pretty)) {
+ $this->throwError ('Pretty Print parameter must have a Boolean value.');
+ return;
+ }
+
+ $this->tag = !empty ($tag) ? $tag : 'span';
+ $this->usesPrettyPrint = $pretty;
+ $this->isSingleton = $singleton;
+
+ switch (gettype ($attributes)) {
+ case 'object': {
+ $this->appendChild ($attributes);
+ break;
+ }
+
+ case 'array': {
+ $this->createAttributes ($attributes, $spaced);
+ break;
+ }
+
+ default: {
+ $this->innerHTML ($attributes);
+ break;
+ }
+ }
+ }
+
+ public function __get ($name)
+ {
+ switch ($name) {
+ case 'innerHTML': {
+ return $this->getInnerHTML ();
+ }
+ }
+ }
+
+ protected function isWord ($string)
+ {
+ if (empty ($string)) {
+ return false;
+ }
+
+ if (!preg_match ('/[a-z0-9:-_.]+/iS', $string)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ protected function throwError ($error)
+ {
+ $backtrace = debug_backtrace ();
+ $line = $backtrace[1]['line'];
+
+ throw new \Exception ('Error on line ' . $line . ': ' . $error);
+ }
+
+ public function getInnerHTML ($indention = '')
+ {
+ $inner_html = array ();
+
+ foreach ($this->children as $child) {
+ if (is_object ($child)) {
+ $inner_html[] = $child->asHTML ($indention);
+ continue;
+ }
+
+ $inner_html[] = $child;
+ }
+
+ return implode (PHP_EOL . $indention, $inner_html);
+ }
+
+ public function createAttribute ($name = '', $value = '', $spaced = true)
+ {
+ if (!is_string ($name) or !$this->isWord ($name)) {
+ $this->throwError ('Attribute name must have a single word String value.');
+ return false;
+ }
+
+ if (is_array ($value)) {
+ $glue = ($spaced !== false) ? ' ' : '';
+ $this->attributes[$name] = implode ($glue, $value);
+ return true;
+ }
+
+ $this->attributes[$name] = $value;
+ return true;
+ }
+
+ public function innerHTML ($html = '')
+ {
+ if ($this->isSingleton === true) {
+ $this->throwError ('Singleton tags do not have innerHTML.');
+ return false;
+ }
+
+ if (!empty ($html)) {
+ $this->children = array ($html);
+ }
+
+ return true;
+ }
+
+ public function appendInnerHTML ($html = '')
+ {
+ if ($this->isSingleton === true) {
+ $this->throwError ('Singleton tags do not have innerHTML.');
+ return false;
+ }
+
+ if (!empty ($html)) {
+ $this->children[] = $html;
+ }
+
+ return true;
+ }
+
+ public function createAttributes (array $attributes, $spaced = true)
+ {
+ if (!is_array ($attributes)) {
+ $this->throwError ('Attributes parameter must have an Array value.');
+ return;
+ }
+
+ foreach ($attributes as $key => $value) {
+ switch ($key) {
+ case 'children': {
+ if (is_array ($value)) {
+ for ($i = 0, $il = count ($value); $i < $il; $i++) {
+ $this->appendChild ($value[$i]);
+ }
+ }
+
+ break;
+ }
+
+ case 'innerHTML': {
+ $this->innerHTML ($value);
+ break;
+ }
+
+ default: {
+ $this->createAttribute ($key, $value, $spaced);
+ break;
+ }
+ }
+ }
+ }
+
+ public function appendAttribute ($name = '', $value = '', $spaced = true)
+ {
+ if (!is_string ($name) or !$this->isWord ($name)) {
+ $this->throwError ('Attribute name must have a single word String value.');
+ return false;
+ }
+
+ if (!empty ($this->attributes[$name])) {
+ if ($spaced !== false) {
+ $this->attributes[$name] .= ' ';
+ }
+ } else {
+ $this->attributes[$name] = '';
+ }
+
+ if (is_array ($value)) {
+ $glue = ($spaced !== false) ? ' ' : '';
+ $this->attributes[$name] .= implode ($glue, $value);
+ return true;
+ }
+
+ $this->attributes[$name] .= $value;
+ return true;
+ }
+
+ public function appendAttributes (array $attributes, $spaced = true)
+ {
+ if (!is_array ($attributes)) {
+ $this->throwError ('Attributes parameter must have an Array value.');
+ return;
+ }
+
+ foreach ($attributes as $key => $value) {
+ if ($key === 'innerHTML') {
+ $this->appendInnerHTML ($value);
+ continue;
+ }
+
+ $this->appendAttribute ($key, $value, $spaced);
+ }
+ }
+
+ public function appendChild (HTMLTag $object)
+ {
+ if ($this->isSingleton === true) {
+ $this->throwError ('Singleton tags do not have children.');
+ return false;
+ }
+
+ if (!is_object ($object)) {
+ $given_type = ucwords (gettype ($object));
+ $this->throwError ($given_type . ' given, when Object is expected.');
+ return false;
+ }
+
+ $this->children[] = $object;
+ return true;
+ }
+
+ public function asHTML ($indention = '')
+ {
+ $attributes = '';
+
+ foreach ($this->attributes as $name => $value) {
+ $value = str_replace ('"', '&quot;', $value);
+ $attributes .= ' ' . $name . '="' . $value . '"';
+ }
+
+ $tag = '<' . $this->tag . $attributes . '>';
+
+ if ($this->isSingleton === false) {
+ if (!empty ($this->children)) {
+ $inner_html = $this->getInnerHTML ();
+
+ if ($this->usesPrettyPrint !== false) {
+ $tag .= PHP_EOL . "\t";
+ $tag .= str_replace (PHP_EOL, PHP_EOL . "\t", $inner_html);
+ $tag .= PHP_EOL;
+ } else {
+ $tag .= $inner_html;
+ }
+ }
+
+ $tag .= '</' . $this->tag . '>';
+ }
+
+ if (!empty ($indention)) {
+ return str_replace (PHP_EOL, PHP_EOL . $indention, $tag);
+ }
+
+ return $tag;
+ }
+}
diff --git a/bootstrap/comments/backend/classes/javascriptbuild.php b/bootstrap/comments/backend/classes/javascriptbuild.php
new file mode 100644
index 0000000..14fedcd
--- /dev/null
+++ b/bootstrap/comments/backend/classes/javascriptbuild.php
@@ -0,0 +1,144 @@
+<?php namespace HashOver;
+
+// Copyright (C) 2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// HashOver is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// HashOver is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with HashOver. If not, see <http://www.gnu.org/licenses/>.
+
+
+class JavaScriptBuild
+{
+ protected $directory;
+ protected $files = array ();
+
+ public function __construct ($directory = '.')
+ {
+ $this->changeDirectory ($directory);
+ }
+
+ public function changeDirectory ($directory = '.')
+ {
+ $this->directory = trim ($directory, '/') . '/';
+ }
+
+ protected function addFile ($file)
+ {
+ // Add file to files array if it isn't already present
+ if (!in_array ($file, $this->files, true)) {
+ $this->files[] = $file;
+ }
+ }
+
+ protected function addDependencies ($file, array $dependencies)
+ {
+ // Add each dependency to files array
+ foreach ($dependencies as $dependency) {
+ $dependency = realpath(__DIR__ . '/' . '/../../' . $this->directory) . '/' . $dependency;
+
+ // Check if the file exists
+ if (file_exists ($file)) {
+ // If so, add file to files array
+ $this->addFile ($dependency);
+ } else {
+ // If not, throw exception on failure
+ $exception = '"%s" depends on "%s" but it does not exist.';
+ $exception = sprintf ($exception, $file, $dependency);
+
+ throw new \Exception ($exception);
+ }
+ }
+
+ return true;
+ }
+
+ protected function includeFile ($file)
+ {
+ // Attempt to read JavaScript file
+ $file = @file_get_contents ($file);
+
+ // Check if the file read successfully
+ if ($file !== false) {
+ // If so, return the contents
+ return trim ($file);
+ }
+
+ // Otherwise throw exception
+ throw new \Exception (
+ sprintf ('Unable to include "%s"', $file)
+ );
+ }
+
+ public function registerFile ($file, array $options = array ())
+ {
+ $file = realpath(__DIR__ . '/' . '/../../' . $this->directory) . '/' . $file;
+
+ if (!empty ($options)) {
+ // Check if there is an include condition
+ if (isset ($options['include'])) {
+ // If so, return void if include is false
+ if ($options['include'] === false) {
+ return;
+ }
+ }
+
+ // Add optional dependencies to files array
+ if (!empty ($options['dependencies'])) {
+ $dependencies = $options['dependencies'];
+ $this->addDependencies ($file, $dependencies);
+ }
+ }
+
+ // Check if the file exists
+ if (file_exists ($file)) {
+ // If so, add file to files array
+ $this->addFile ($file);
+ } else {
+ // If not, throw exception
+ throw new \Exception ('"' . $file . '" does not exist.');
+ }
+
+ return true;
+ }
+
+ public function build ($minify = false, $minify_level = 0)
+ {
+ // Array for included JavaScript files
+ $files = array ();
+
+ // Attempt to include registered JavaScript files
+ foreach ($this->files as $file) {
+ $files[] = $this->includeFile ($file);
+ }
+
+ // Join the included JavaScript files
+ $javascript = implode (PHP_EOL . PHP_EOL, $files);
+
+ // Minify the JavaScript if told to
+ if (!isset ($_GET['hashover-unminified'])) {
+ if ($minify === true and $minify_level > 0) {
+ // Instantiate JavaScript minification class
+ $minifier = new JavaScriptMinifier ();
+
+ // Minify JavaScript build result
+ $minified = $minifier->minify ($javascript, $minify_level);
+
+ // Set minified result as JavaScript output
+ $javascript = $minified;
+ }
+ }
+
+ // Return normal JavaScript code
+ return $javascript;
+ }
+}
diff --git a/bootstrap/comments/backend/classes/javascriptminifier.php b/bootstrap/comments/backend/classes/javascriptminifier.php
new file mode 100644
index 0000000..35a57a6
--- /dev/null
+++ b/bootstrap/comments/backend/classes/javascriptminifier.php
@@ -0,0 +1,121 @@
+<?php namespace HashOver;
+
+// Copyright (C) 2015 Jacob Barkdull
+// This file is part of HashOver.
+//
+// HashOver is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// HashOver is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with HashOver. If not, see <http://www.gnu.org/licenses/>.
+
+
+class JavaScriptMinifier
+{
+ // Array for locking minification
+ protected $lock = array (
+ 'status' => false,
+ 'char' => ''
+ );
+
+ // JavaScript minification function
+ public function minify ($js, $level = 4)
+ {
+ if ($level <= 0) {
+ return $js;
+ }
+
+ if ($level >= 1) {
+ // Remove single-line code comments
+ $js = preg_replace ('/^[\t ]*?\/\/.*\s?/m', '', $js);
+
+ // Remove end-of-line code comments
+ $js = preg_replace ('/([\s;})]+)\/\/.*/m', '\\1', $js);
+
+ // Remove multi-line code comments
+ $js = preg_replace ('/\/\*[\s\S]*?\*\//', '', $js);
+ }
+
+ if ($level >= 2) {
+ // Remove whitespace
+ $js = preg_replace ('/^\s*/m', '', $js);
+
+ // Replace multiple tabs with a single space
+ $js = preg_replace ('/\t+/m', ' ', $js);
+ }
+
+ if ($level >= 3) {
+ // Remove newlines
+ $js = preg_replace ('/[\r\n]+/', '', $js);
+ }
+
+ if ($level >= 4) {
+ // Split input JavaScript by single and double quotes
+ $js_substrings = preg_split ('/([\'"])/', $js, -1, PREG_SPLIT_DELIM_CAPTURE);
+
+ // Empty variable for minified JavaScript
+ $js = '';
+
+ foreach ($js_substrings as $substring) {
+ // Check if substring is split delimiter
+ if ($substring === '\'' or $substring === '"') {
+ // If so, check whether minification is unlocked
+ if ($this->lock['status'] === false) {
+ // If so, lock it and set lock character
+ $this->lock['status'] = true;
+ $this->lock['char'] = $substring;
+ } else {
+ // If not, check if substring is lock character
+ if ($substring === $this->lock['char']) {
+ // If so, unlock minification
+ $this->lock['status'] = false;
+ $this->lock['char'] = '';
+ }
+ }
+
+ // Add substring to minified output
+ $js .= $substring;
+
+ continue;
+ }
+
+ // Minify current substring if minification is unlocked
+ if ($this->lock['status'] === false) {
+ // Remove unnecessary semicolons
+ $substring = str_replace (';}', '}', $substring);
+
+ // Remove spaces round operators
+ $substring = preg_replace ('/ *([<>=+\-!\|{(},;&:?]+) */', '\\1', $substring);
+ }
+
+ // Add substring to minified output
+ $js .= $substring;
+ }
+ }
+
+ // Get URL add "unminified" URL query
+ $unminified_url = 'http' . (isset ($_SERVER['HTTPS']) ? 's' : '') . '://';
+ $unminified_url .= $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
+ $unminified_url .= '&hashover-unminified';
+
+ // Copyright notice and URL to unminified code
+ $copyright = array (
+ '// Copyright (C) 2015 Jacob Barkdull',
+ '// Under the terms of the GNU Affero General Public License.',
+ '//',
+ '// Non-minified JavaScript:',
+ '//',
+ '// ' . $unminified_url . PHP_EOL . PHP_EOL
+ );
+
+ // Return final minified JavaScript
+ return implode (PHP_EOL, $copyright) . trim ($js);
+ }
+}
diff --git a/bootstrap/comments/backend/classes/locale.php b/bootstrap/comments/backend/classes/locale.php
new file mode 100644
index 0000000..8927af7
--- /dev/null
+++ b/bootstrap/comments/backend/classes/locale.php
@@ -0,0 +1,177 @@
+<?php namespace HashOver;
+
+// Copyright (C) 2010-2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// HashOver is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// HashOver is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with HashOver. If not, see <http://www.gnu.org/licenses/>.
+
+
+class Locale
+{
+ public $setup;
+ public $mode;
+ public $text;
+
+ public function __construct (Setup $setup)
+ {
+ $this->setup = $setup;
+ $this->mode = $setup->usage['mode'];
+
+ // Get appropriate locale file
+ $locale_file_path = $this->getLocaleFile ();
+
+ // Include the locale file
+ $this->includeLocaleFile ($locale_file_path);
+
+ // Prepare locale
+ $this->prepareLocale ();
+ }
+
+ // Check for PHP locale file
+ protected function getLocaleFile ()
+ {
+ // Locales checklist
+ $locales = array ();
+
+ // Lowercase language code
+ $language = mb_strtolower ($this->setup->language);
+
+ // Get path to locales directory
+ $locales_directory = $this->setup->getAbsolutePath ('backend/locales');
+
+ // Check if we are automatically selecting the locale
+ if ($language === 'auto') {
+ // If so, get system locale
+ $system_locale = mb_strtolower (setlocale (LC_CTYPE, 0));
+
+ // Split the locale into specific parts
+ $locale_parts = explode ('.', $system_locale);
+ $language_parts = explode ('_', $locale_parts[0]);
+
+ // Add locale in 'en-us' format to checklist
+ $full_locale = str_replace ('_', '-', $locale_parts[0]);
+ $locales[] = $full_locale;
+
+ // Add front part of locale ('en') to checklist
+ $locales[] = $language_parts[0];
+
+ // Add end part of locale ('us') to checklist
+ if (!empty ($language_parts[1])) {
+ $locales[] = $language_parts[1];
+ }
+ } else {
+ // If not, add configured locale to checklist
+ $locales[] = $language;
+ }
+
+ foreach ($locales as $locale) {
+ // Locale file path
+ $locale_file = $locales_directory . '/' . $locale . '.php';
+
+ // Check if a locale file exists for current locale
+ if (file_exists ($locale_file)) {
+ // If so, return PHP locale file path
+ return $locale_file;
+ }
+ }
+
+ // Otherwise, default to English
+ return $locales_directory . '/en.php';
+ }
+
+ protected function includeLocaleFile ($file)
+ {
+ // Check if the locale file can be included
+ if (@include ($file)) {
+ // If so, set locale to array stored in the file
+ $this->text = $locale;
+ } else {
+ // If not, throw exception
+ $language = mb_strtoupper ($this->setup->language);
+ $exception = $language . ' locale file could not be included!';
+
+ throw new \Exception ($exception);
+ }
+ }
+
+ // Injects optionality into a given locale string
+ public function optionality ($locale, $choice = 'optional')
+ {
+ // Optionality locale key (default to optional)
+ $key = ($choice === 'required') ? 'required' : 'optional';
+
+ // Optionality locale string
+ $optionality = mb_strtolower ($this->text[$key]);
+
+ // Inject optionality into locale string
+ $new_locale = sprintf ($locale, $optionality);
+
+ return $new_locale;
+ }
+
+ // Adds optionality to any given locale string
+ public function optionalize ($key, $choice = 'optional')
+ {
+ return $this->optionality ($this->text[$key] . ' (%s)', $choice);
+ }
+
+ // Prepares locale by modifying them in various ways
+ public function prepareLocale ()
+ {
+ // Add optionality to form field title locales
+ foreach ($this->setup->fieldOptions as $field => $option) {
+ // Title locale key
+ $tooltip_key = $field . '-tip';
+
+ // Title locale string
+ $tooltip_locale = $this->text[$tooltip_key];
+
+ // Inject optionality into title locale
+ $optionality = $this->optionality ($tooltip_locale, $option);
+
+ // Update the locale
+ $this->text[$tooltip_key] = $optionality;
+ }
+
+ // Run through each locale string
+ foreach ($this->text as $key => $value) {
+ switch ($key) {
+ // Inject date and time formats into date and time locale
+ case 'date-time': {
+ $this->text[$key] = sprintf (
+ $value,
+ $this->setup->dateFormat,
+ $this->setup->timeFormat
+ );
+
+ break;
+ }
+ }
+ }
+ }
+
+ // Return file permissions locale with directory and PHP user
+ public function permissionsInfo ($file)
+ {
+ // PHP user, or www-data
+ $php_user = isset ($_SERVER['USER']) ? $_SERVER['USER'] : '';
+ $php_user = !empty ($php_user) ? $php_user : 'www-data';
+
+ return sprintf (
+ $this->text['permissions-info'],
+ $this->setup->getHttpPath ($file),
+ $php_user
+ );
+ }
+}
diff --git a/bootstrap/comments/backend/classes/login.php b/bootstrap/comments/backend/classes/login.php
new file mode 100644
index 0000000..7953c89
--- /dev/null
+++ b/bootstrap/comments/backend/classes/login.php
@@ -0,0 +1,202 @@
+<?php namespace HashOver;
+
+// Copyright (C) 2015-2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// HashOver is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// HashOver is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with HashOver. If not, see <http://www.gnu.org/licenses/>.
+
+
+class Login extends PostData
+{
+ public $setup;
+ public $encryption;
+ public $cookies;
+ public $locale;
+ public $loginMethod;
+ public $fieldNeeded;
+ public $name = '';
+ public $password = '';
+ public $loginHash = '';
+ public $email = '';
+ public $website = '';
+ public $userIsLoggedIn = false;
+ public $userIsAdmin = false;
+
+ public function __construct (Setup $setup)
+ {
+ parent::__construct ();
+
+ $this->setup = $setup;
+ $this->encryption = $setup->encryption;
+ $this->cookies = new Cookies ($setup);
+ $this->locale = new Locale ($setup);
+
+ // Instantiate login method class
+ $login_class = 'HashOver\\' . $this->setup->loginMethod;
+ $this->loginMethod = new $login_class ($setup, $this->cookies, $this->locale);
+
+ // Error message to display to the user
+ $this->fieldNeeded = $this->locale->text['field-needed'];
+
+ // Check if user is logged in
+ $this->getLogin ();
+ }
+
+ // Prepares login credentials
+ public function prepareCredentials ()
+ {
+ // Set name
+ if (isset ($this->postData['name'])) {
+ $this->loginMethod->name = $this->postData['name'];
+ }
+
+ // Attempt to get name
+ $name = $this->setup->getRequest ('name');
+
+ // Attempt to get password
+ $password = $this->setup->getRequest ('password', null);
+
+ // Set password
+ if ($password !== null) {
+ $this->loginMethod->password = $this->encryption->createHash ($password);
+ } else {
+ $this->loginMethod->password = '';
+ }
+
+ // Check that login hash cookie is not set
+ if ($this->cookies->getValue ('login') === null) {
+ // If so, generate a random password
+ $random_password = bin2hex (openssl_random_pseudo_bytes (16));
+
+ // And use user password or random password
+ $password = $password ? $password : $random_password;
+ }
+
+ // Generate a RIPEMD-160 hash to indicate user login
+ $this->loginMethod->loginHash = hash ('ripemd160', $name . $password);
+
+ // Set e-mail address
+ if (isset ($this->postData['email'])) {
+ $this->loginMethod->email = $this->postData['email'];
+ }
+
+ // Set website URL
+ if (isset ($this->postData['website'])) {
+ $this->loginMethod->website = $this->postData['website'];
+ }
+ }
+
+ // Update login credentials
+ public function updateCredentials ()
+ {
+ $this->name = $this->loginMethod->name;
+ $this->password = $this->loginMethod->password;
+ $this->loginHash = $this->loginMethod->loginHash;
+ $this->email = $this->loginMethod->email;
+ $this->website = $this->loginMethod->website;
+
+ // Validate e-mail address
+ if (!empty ($this->email)) {
+ if (!filter_var ($this->email, FILTER_VALIDATE_EMAIL)) {
+ $this->email = '';
+ }
+ }
+
+ // Prepend "http://" to website URL if missing
+ if (!empty ($this->website)) {
+ if (!preg_match ('/htt(p|ps):\/\//i', $this->website)) {
+ $this->website = 'http://' . $this->website;
+ }
+ }
+ }
+
+ // Set login credentials
+ public function setCredentials ()
+ {
+ // Prepare login credentials
+ $this->prepareCredentials ();
+
+ // Set login method credentials
+ $this->loginMethod->setCredentials ();
+
+ // Update login credentials
+ $this->updateCredentials ();
+ }
+
+ // Get login method credentials
+ public function getCredentials ()
+ {
+ $this->loginMethod->getCredentials ();
+ $this->updateCredentials ();
+ }
+
+ // Checks if required fields have values
+ public function validateFields ()
+ {
+ // Check required fields, throw error if any are empty
+ foreach ($this->setup->fieldOptions as $field => $status) {
+ if ($status === 'required' and empty ($this->$field)) {
+ // Don't set cookies if the request is via AJAX
+ if ($this->viaAJAX !== true) {
+ $this->cookies->setFailedOn ($field, $this->replyTo);
+ }
+
+ throw new \Exception (sprintf (
+ $this->fieldNeeded, $this->locale->text[$field]
+ ));
+ }
+ }
+
+ return true;
+ }
+
+ // Main login method
+ public function setLogin ()
+ {
+ // Set login method credentials
+ $this->setCredentials ();
+
+ // Check if required fields have values
+ $this->validateFields ();
+
+ // Execute login method's setLogin
+ if ($this->loginMethod->enabled === true) {
+ $this->loginMethod->setLogin ();
+ }
+ }
+
+ // Check if user is logged in
+ public function getLogin ()
+ {
+ // Get login method credentials
+ $this->getCredentials ();
+
+ // Check if user is logged in
+ if (!empty ($this->loginHash)) {
+ // If so, set login indicator
+ $this->userIsLoggedIn = true;
+
+ // Check if user is logged in as admin
+ if ($this->setup->adminLogin ($this->loginHash) === true) {
+ $this->userIsAdmin = true;
+ }
+ }
+ }
+
+ // Main logout method
+ public function clearLogin ()
+ {
+ $this->loginMethod->clearLogin ();
+ }
+}
diff --git a/bootstrap/comments/backend/classes/markdown.php b/bootstrap/comments/backend/classes/markdown.php
new file mode 100644
index 0000000..bf278b7
--- /dev/null
+++ b/bootstrap/comments/backend/classes/markdown.php
@@ -0,0 +1,126 @@
+<?php namespace HashOver;
+
+// Copyright (C) 2015-2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// HashOver is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// HashOver is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with HashOver. If not, see <http://www.gnu.org/licenses/>.
+
+
+class Markdown
+{
+ public $blockCodeRegex = '/```([\s\S]+?)```/S';
+ protected $paragraphRegex = '/(?:\r\n|\r|\n){2}/S';
+ public $inlineCodeRegex = '/(^|[^a-z0-9`])`([^`]+?[\s\S]+?)`([^a-z0-9`]|$)/iS';
+
+ // Array for inline code and code block markers
+ protected $codeMarkers = array (
+ 'block' => array ('marks' => array (), 'count' => 0),
+ 'inline' => array ('marks' => array (), 'count' => 0)
+ );
+
+ // Markdown patterns to search for
+ public $search = array (
+ '/\*\*([^ *])([\s\S]+?)([^ *])\*\*/S',
+ '/\*([^ *])([\s\S]+?)([^ *])\*/S',
+ '/(^|\W)_([^_]+?[\s\S]+?)_(\W|$)/S',
+ '/__([^ _])([\s\S]+?)([^ _])__/S',
+ '/~~([^ ~])([\s\S]+?)([^ ~])~~/S'
+ );
+
+ // HTML replacements for markdown patterns
+ public $replace = array (
+ '<strong>\\1\\2\\3</strong>',
+ '<em>\\1\\2\\3</em>',
+ '\\1<u>\\2</u>\\3',
+ '<u>\\1\\2\\3</u>',
+ '<s>\\1\\2\\3</s>'
+ );
+
+ // Replaces markdown for inline code with a marker
+ protected function codeReplace ($grp, $display)
+ {
+ $markName = 'CODE_' . strtoupper ($display);
+ $markCount = $this->codeMarkers[$display]['count']++;
+
+ if ($display !== 'block') {
+ $codeMarker = $grp[1] . $markName . '[' . $markCount . ']' . $grp[3];
+ $this->codeMarkers[$display]['marks'][$markCount] = trim ($grp[2], "\r\n");
+ } else {
+ $codeMarker = $markName . '[' . $markCount . ']';
+ $this->codeMarkers[$display]['marks'][$markCount] = trim ($grp[1], "\r\n");
+ }
+
+ return $codeMarker;
+ }
+
+ // Replaces markdown for code block with a marker
+ protected function blockCodeReplace ($grp)
+ {
+ return $this->codeReplace ($grp, 'block');
+ }
+
+ // Replaces markdown for inline code with a marker
+ protected function inlineCodeReplace ($grp)
+ {
+ return $this->codeReplace ($grp, 'inline');
+ }
+
+ // Returns the original inline markdown code with HTML replacement
+ protected function inlineCodeReturn ($grp)
+ {
+ return '<code class="hashover-inline">' . $this->codeMarkers['inline']['marks'][($grp[1])] . '</code>';
+ }
+
+ // Returns the original markdown code block with HTML replacement
+ protected function blockCodeReturn ($grp)
+ {
+ return '<code>' . $this->codeMarkers['block']['marks'][($grp[1])] . '</code>';
+ }
+
+ // Parses a string as markdown
+ public function parseMarkdown ($string)
+ {
+ // Reset marker arrays
+ $this->codeMarkers = array (
+ 'block' => array ('marks' => array (), 'count' => 0),
+ 'inline' => array ('marks' => array (), 'count' => 0)
+ );
+
+ // Replace code blocks with markers
+ $string = preg_replace_callback ($this->blockCodeRegex, 'self::blockCodeReplace', $string);
+
+ // Break string into paragraphs
+ $paragraphs = preg_split ($this->paragraphRegex, $string);
+
+ // Run through each paragraph
+ for ($i = 0, $il = count ($paragraphs); $i < $il; $i++) {
+ // Replace inline code with markers
+ $paragraphs[$i] = preg_replace_callback ($this->inlineCodeRegex, 'self::inlineCodeReplace', $paragraphs[$i]);
+
+ // Replace markdown patterns
+ $paragraphs[$i] = preg_replace ($this->search, $this->replace, $paragraphs[$i]);
+
+ // Replace markers with original markdown code
+ $paragraphs[$i] = preg_replace_callback ('/CODE_INLINE\[([0-9]+)\]/S', 'self::inlineCodeReturn', $paragraphs[$i]);
+ }
+
+ // Join paragraphs
+ $string = implode (PHP_EOL . PHP_EOL, $paragraphs);
+
+ // Replace code block markers with original markdown code
+ $string = preg_replace_callback ('/CODE_BLOCK\[([0-9]+)\]/S', 'self::blockCodeReturn', $string);
+
+ return $string;
+ }
+}
diff --git a/bootstrap/comments/backend/classes/metadata.php b/bootstrap/comments/backend/classes/metadata.php
new file mode 100644
index 0000000..4619ba8
--- /dev/null
+++ b/bootstrap/comments/backend/classes/metadata.php
@@ -0,0 +1,104 @@
+<?php namespace HashOver;
+
+// Copyright (C) 2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// HashOver is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// HashOver is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with HashOver. If not, see <http://www.gnu.org/licenses/>.
+
+
+class Metadata
+{
+ public $setup;
+ public $thread;
+ public $data;
+
+ public function __construct (Setup $setup, Thread $thread)
+ {
+ $this->setup = $setup;
+ $this->thread = $thread;
+ $this->data = $thread->data;
+ }
+
+ protected function prependLatestComments ($file, $global = false)
+ {
+ // Add thread to file if metadata is global
+ if ($global === true) {
+ $file = $this->setup->threadName . '/' . $file;
+ }
+
+ // Initial latest comments metadata array
+ $latest = array ($file);
+
+ // Attempt to read existing latest comments metadata
+ $metadata = $this->data->readMeta ('latest-comments', 'auto', $global);
+
+ // Check if latest comments metadata read successfully
+ if ($metadata !== false) {
+ // If so, merge existing comments with initial array
+ $latest = array_merge ($latest, $metadata);
+ }
+
+ // Maximum number of latest comments to store
+ $latest_max = max (10, $this->setup->latestMax);
+
+ // Limit latest comments metadata array to configurable size
+ $latest = array_slice ($latest, 0, $latest_max);
+
+ // Attempt to save latest comments metadata
+ $this->data->saveMeta ('latest-comments', $latest, 'auto', $global);
+ }
+
+ protected function spliceLatestComments ($file, $global = false)
+ {
+ // Add thread to file if metadata is global
+ if ($global === true) {
+ $file = $this->setup->threadName . '/' . $file;
+ }
+
+ // Attempt to read existing latest comments metadata
+ $latest = $this->data->readMeta ('latest-comments', 'auto', $global);
+
+ // Check if latest comments metadata read successfully
+ if ($latest !== false) {
+ $index = array_search ($file, $latest);
+
+ // Check if the comment is in the latest array
+ if ($index !== false) {
+ // If so, remove it from the array
+ array_splice ($latest, $index, 1);
+ }
+
+ // Attempt to save latest comments metadata
+ $this->data->saveMeta ('latest-comments', $latest, 'auto', $global);
+ }
+ }
+
+ public function addLatestComment ($file)
+ {
+ // Add comment to thread-specific latest comments metadata
+ $this->prependLatestComments ($file);
+
+ // Add comment to global latest comments metadata
+ $this->prependLatestComments ($file, true);
+ }
+
+ public function removeFromLatest ($file)
+ {
+ // Add comment to thread-specific latest comments metadata
+ $this->spliceLatestComments ($file);
+
+ // Add comment to global latest comments metadata
+ $this->spliceLatestComments ($file, true);
+ }
+}
diff --git a/bootstrap/comments/backend/classes/misc.php b/bootstrap/comments/backend/classes/misc.php
new file mode 100644
index 0000000..3aff4e9
--- /dev/null
+++ b/bootstrap/comments/backend/classes/misc.php
@@ -0,0 +1,148 @@
+<?php namespace HashOver;
+
+// Copyright (C) 2010-2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// HashOver is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// HashOver is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with HashOver. If not, see <http://www.gnu.org/licenses/>.
+
+
+class Misc
+{
+ public $mode;
+
+ // XSS-unsafe characters to search for
+ protected $searchXSS = array (
+ '&',
+ '<',
+ '>',
+ '"',
+ "'",
+ '/',
+ '\\'
+ );
+
+ // XSS-safe replacement character entities
+ protected $replaceXSS = array (
+ '&amp;',
+ '&lt;',
+ '&gt;',
+ '&quot;',
+ '&#x27;',
+ '&#x2F;',
+ '&#92;'
+ );
+
+ // Allowed JavaScript constructors
+ protected $objects = array (
+ 'HashOver',
+ 'HashOverCountLink',
+ 'HashOverLatest'
+ );
+
+ public function __construct ($mode)
+ {
+ $this->mode = $mode;
+ }
+
+ // Return JSON or JSONP function call
+ public function jsonData ($data, $self_error = false)
+ {
+ // Encode JSON data
+ $json = json_encode ($data);
+
+ // Return JSON as-is if the request isn't for JSONP
+ if (!isset ($_GET['jsonp']) or !isset ($_GET['jsonp_object'])) {
+ return $json;
+ }
+
+ // Otherwise, make JSONP callback index XSS safe
+ $index = $this->makeXSSsafe ($_GET['jsonp']);
+
+ // Make JSONP object constructor name XSS safe
+ $object = $this->makeXSSsafe ($_GET['jsonp_object']);
+
+ // Check if constructor is allowed, if not use default
+ $allowed_object = in_array ($object, $this->objects, true);
+ $object = $allowed_object ? $object : 'HashOver';
+
+ // Check if the JSONP index contains a numeric value
+ if (is_numeric ($index) or $self_error === true) {
+ // If so, cast index to positive integer
+ $index = ($self_error === true) ? 0 : (int)(abs ($index));
+
+ // Construct JSONP function call
+ $jsonp = sprintf ('%s.jsonp[%d] (%s);', $object, $index, $json);
+
+ // And return the JSONP script
+ return $jsonp;
+ }
+
+ // Otherwise, return an error
+ return $this->jsonData (array (
+ 'message' => 'JSONP index must have a numeric value.',
+ 'type' => 'error'
+ ), true);
+ }
+
+ // Make a string XSS-safe
+ public function makeXSSsafe ($string)
+ {
+ // Return cookie value without harmful characters
+ return str_replace ($this->searchXSS, $this->replaceXSS, $string);
+ }
+
+ // Returns error in HTML paragraph
+ public function displayError ($error = 'Something went wrong!')
+ {
+ $xss_safe = $this->makeXSSsafe ($error);
+ $data = array ();
+
+ switch ($this->mode) {
+ // Minimal JavaScript to display error message on page
+ case 'javascript': {
+ $data[] = 'var hashover = document.getElementById (\'hashover\') || document.body;';
+ $data[] = 'var error = \'<p><b>HashOver</b>: ' . $xss_safe . '</p>\';' . PHP_EOL;
+ $data[] = 'hashover.innerHTML += error;';
+
+ break;
+ }
+
+ // RSS XML to indicate error
+ case 'rss': {
+ $data[] = '<?xml version="1.0" encoding="UTF-8"?>';
+ $data[] = '<error>HashOver: ' . $xss_safe . '</error>';
+
+ break;
+ }
+
+ // JSON to indicate error
+ case 'json': {
+ $data[] = $this->jsonData (array (
+ 'message' => $error,
+ 'type' => 'error'
+ ));
+
+ break;
+ }
+
+ // Default just return the error message
+ default: {
+ $data[] = '<p><b>HashOver</b>: ' . $error . '</p>';
+ break;
+ }
+ }
+
+ echo implode (PHP_EOL, $data);
+ }
+}
diff --git a/bootstrap/comments/backend/classes/parsejson.php b/bootstrap/comments/backend/classes/parsejson.php
new file mode 100644
index 0000000..48e4a59
--- /dev/null
+++ b/bootstrap/comments/backend/classes/parsejson.php
@@ -0,0 +1,90 @@
+<?php namespace HashOver;
+
+// Copyright (C) 2010-2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// HashOver is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// HashOver is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with HashOver. If not, see <http://www.gnu.org/licenses/>.
+
+
+// Functions for reading and writing JSON files
+class ParseJSON extends CommentFiles
+{
+ public function __construct (Setup $setup)
+ {
+ parent::__construct ($setup);
+
+ // Throw exception if the JSON extension isn't loaded
+ $setup->extensionsLoaded (array ('json'));
+ }
+
+ public function query (array $files = array (), $auto = true)
+ {
+ // Return array of files
+ return $this->loadFiles ('json', $files, $auto);
+ }
+
+ public function read ($file, $thread = 'auto')
+ {
+ // Get comment file path
+ $file = $this->getCommentPath ($file, 'json', $thread);
+
+ // Read and parse JSON comment file
+ $json = $this->readJSON ($file);
+
+ return $json;
+ }
+
+ public function save ($file, array $contents, $editing = false, $thread = 'auto')
+ {
+ // Get comment file path
+ $file = $this->getCommentPath ($file, 'json', $thread);
+
+ // Return false on attempts to override an existing file
+ if (file_exists ($file) and $editing === false) {
+ return false;
+ }
+
+ // Save the JSON data to the comment file
+ if ($this->saveJSON ($file, $contents)) {
+ @chmod ($file, 0600);
+ return true;
+ }
+
+ return false;
+ }
+
+ public function delete ($file, $hard_unlink = false)
+ {
+ // Actually delete the comment file
+ if ($hard_unlink === true) {
+ return unlink ($this->getCommentPath ($file, 'json'));
+ }
+
+ // Read comment file
+ $json = $this->read ($file);
+
+ // Check for JSON parse error
+ if ($json !== false) {
+ // Change status to deleted
+ $json['status'] = 'deleted';
+
+ // Attempt to save file
+ if ($this->save ($file, $json, true)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/bootstrap/comments/backend/classes/parsesql.php b/bootstrap/comments/backend/classes/parsesql.php
new file mode 100644
index 0000000..9185ced
--- /dev/null
+++ b/bootstrap/comments/backend/classes/parsesql.php
@@ -0,0 +1,170 @@
+<?php namespace HashOver;
+
+// Copyright (C) 2010-2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// HashOver is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// HashOver is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with HashOver. If not, see <http://www.gnu.org/licenses/>.
+
+
+// Read and count comments
+class ParseSQL extends Database
+{
+ protected $insert = array (
+ 'id' => null,
+ 'body' => null,
+ 'status' => null,
+ 'date' => null,
+ 'name' => null,
+ 'password' => null,
+ 'login_id' => null,
+ 'email' => null,
+ 'encryption' => null,
+ 'email_hash' => null,
+ 'notifications' => null,
+ 'website' => null,
+ 'ipaddr' => null,
+ 'likes' => null,
+ 'dislikes' => null
+ );
+
+ protected $update = array (
+ 'id' => null,
+ 'body' => null,
+ 'status' => null,
+ 'name' => null,
+ 'password' => null,
+ 'email' => null,
+ 'encryption' => null,
+ 'email_hash' => null,
+ 'notifications' => null,
+ 'website' => null,
+ 'likes' => null,
+ 'dislikes' => null
+ );
+
+ public function __construct (Setup $setup)
+ {
+ parent::__construct ($setup);
+
+ // Throw exception if the SQL extension isn't loaded
+ switch ($setup->databaseType) {
+ case 'sqlite': {
+ $setup->extensionsLoaded (array (
+ 'pdo_sqlite',
+ 'sqlite3'
+ ));
+
+ break;
+ }
+
+ case 'mysql': {
+ $setup->extensionsLoaded (array (
+ 'pdo_mysql'
+ ));
+
+ break;
+ }
+ }
+ }
+
+ public function query (array $files = array (), $auto = true)
+ {
+ $statement = 'SELECT `id` FROM `' . $this->setup->threadName . '`';
+ $results = $this->database->query ($statement);
+
+ if ($results !== false) {
+ $fetch_all = $results->fetchAll (\PDO::FETCH_NUM);
+ $return_array = array ();
+
+ for ($i = 0, $il = count ($fetch_all); $i < $il; $i++) {
+ $key = $fetch_all[$i][0];
+ $return_array[$key] = (string)($key);
+ }
+
+ return $return_array;
+ }
+
+ return false;
+ }
+
+ public function read ($id, $thread = 'auto')
+ {
+ $thread = $this->getCommentThread ($thread);
+
+ $columns = implode (', ', array (
+ '`body`',
+ '`status`',
+ '`date`',
+ '`name`',
+ '`password`',
+ '`login_id`',
+ '`email`',
+ '`encryption`',
+ '`email_hash`',
+ '`notifications`',
+ '`website`',
+ '`ipaddr`',
+ '`likes`',
+ '`dislikes`'
+ ));
+
+ $statement = 'SELECT ' . $columns . ' ';
+ $statement .= 'FROM `' . $thread . '` ';
+ $statement .= 'WHERE id=\'' . $id . '\'';
+
+ $result = $this->database->query ($statement);
+
+ if ($result !== false) {
+ return (array) $result->fetch (\PDO::FETCH_ASSOC);
+ }
+
+ return false;
+ }
+
+ protected function prepareQuery ($id, array $contents, array $defaults)
+ {
+ $query = array_merge ($defaults, array ('id' => $id));
+
+ foreach ($contents as $key => $value) {
+ if (array_key_exists ($key, $defaults)) {
+ $query[$key] = $value;
+ }
+ }
+
+ return $query;
+ }
+
+ public function save ($id, array $contents, $editing = false, $thread = 'auto')
+ {
+ $thread = $this->getCommentThread ($thread);
+ $action = ($editing === true) ? 'update' : 'insert';
+ $query = $this->prepareQuery ($id, $contents, $this->$action);
+ $status = $this->write ($action, $thread, $query);
+
+ return $status;
+ }
+
+ public function delete ($id, $delete = false)
+ {
+ $query = array ('id' => $id);
+
+ if ($delete !== true) {
+ $query['status'] = 'deleted';
+ }
+
+ $status = $this->write ('delete', 'auto', $query, $delete);
+
+ return $status;
+ }
+}
diff --git a/bootstrap/comments/backend/classes/parsexml.php b/bootstrap/comments/backend/classes/parsexml.php
new file mode 100644
index 0000000..2cea306
--- /dev/null
+++ b/bootstrap/comments/backend/classes/parsexml.php
@@ -0,0 +1,147 @@
+<?php namespace HashOver;
+
+// Copyright (C) 2010-2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// HashOver is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// HashOver is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with HashOver. If not, see <http://www.gnu.org/licenses/>.
+
+
+// Functions for reading and writing XML files
+class ParseXML extends CommentFiles
+{
+ public function __construct (Setup $setup)
+ {
+ parent::__construct ($setup);
+
+ // Enable XML user error handling
+ libxml_use_internal_errors (true);
+
+ // Throw exception if the XML extension isn't loaded
+ $setup->extensionsLoaded (array ('xml', 'libxml'));
+ }
+
+ public function query (array $files = array (), $auto = true)
+ {
+ // Return array of files
+ return $this->loadFiles ('xml', $files, $auto);
+ }
+
+ public function read ($file, $thread = 'auto')
+ {
+ // Get comment file path
+ $file = $this->getCommentPath ($file, 'xml', $thread);
+
+ // Read XML comment file
+ $data = @file_get_contents ($file);
+
+ // Check for file read error
+ if ($data !== false) {
+ // Parse XML comment file
+ $xml = @simplexml_load_string ($data, 'SimpleXMLElement', LIBXML_COMPACT | LIBXML_NOCDATA);
+
+ // Check for XML parse error
+ if ($xml !== false) {
+ // Remove first two levels of indentation from comment
+ $xml->body = preg_replace ('/^\t{0,2}/mS', '', trim ($xml->body, "\r\n\t"));
+
+ return (array) $xml;
+ }
+ }
+
+ return false;
+ }
+
+ public function save ($file, array $contents, $editing = false, $thread = 'auto')
+ {
+ // Get comment file path
+ $file = $this->getCommentPath ($file, 'xml', $thread);
+
+ // Return false on attempts to override an existing file
+ if (file_exists ($file) and $editing === false) {
+ return false;
+ }
+
+ // Create empty XML DOM document
+ $dom = new \DOMDocument ('1.0', 'UTF-8');
+ $dom->preserveWhiteSpace = false;
+ $dom->formatOutput = true;
+
+ // Create root element "comment"
+ $comment = $dom->createElement ('comment');
+
+ // Add comment data to root "comment" element
+ foreach ($contents as $key => $value) {
+ $element = $dom->createElement ($key);
+
+ if ($key === 'body') {
+ $new_value = '';
+
+ foreach (explode (PHP_EOL, trim ($value, PHP_EOL)) as $line) {
+ if (!empty ($line)) {
+ $new_value .= "\t\t";
+ }
+
+ $new_value .= $line . "\n";
+ }
+
+ $value = "\n" . $new_value . "\t";
+ }
+
+ $text_node = $dom->createTextNode ($value);
+ $element->appendChild ($text_node);
+ $comment->appendChild ($element);
+ }
+
+ // Append root element "comment"
+ $dom->appendChild ($comment);
+
+ // Replace double spaces with single tab
+ $tabbed_dom = str_replace (' ', "\t", $dom->saveXML ());
+
+ // Convert line endings to OS specific style
+ $tabbed_dom = $this->osLineEndings ($tabbed_dom);
+
+ // Attempt to write file
+ if (@file_put_contents ($file, $tabbed_dom, LOCK_EX)) {
+ @chmod ($file, 0600);
+ return true;
+ }
+
+ return false;
+ }
+
+ public function delete ($file, $hard_unlink = false)
+ {
+ // Actually delete the comment file
+ if ($hard_unlink === true) {
+ return unlink ($this->getCommentPath ($file, 'xml'));
+ }
+
+ // Read comment file
+ $xml = $this->read ($file);
+
+ // Check for XML parse error
+ if ($xml !== false) {
+ // Change status to deleted
+ $xml['status'] = 'deleted';
+
+ // Attempt to save file
+ if ($this->save ($file, $xml, true)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/bootstrap/comments/backend/classes/phpmode.php b/bootstrap/comments/backend/classes/phpmode.php
new file mode 100644
index 0000000..60d507e
--- /dev/null
+++ b/bootstrap/comments/backend/classes/phpmode.php
@@ -0,0 +1,440 @@
+<?php namespace HashOver;
+
+// Copyright (C) 2010-2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// HashOver is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// HashOver is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with HashOver. If not, see <http://www.gnu.org/licenses/>.
+
+
+class PHPMode
+{
+ public $setup;
+ public $ui;
+ public $locale;
+ public $templater;
+ public $markdown;
+ public $comments;
+
+ protected $trimTagRegexes = array (
+ 'blockquote' => '/(<blockquote>)([\s\S]*?)(<\/blockquote>)/iS',
+ 'ul' => '/(<ul>)([\s\S]*?)(<\/ul>)/iS',
+ 'ol' => '/(<ol>)([\s\S]*?)(<\/ol>)/iS'
+ );
+
+ protected $linkRegex = '/((http|https|ftp):\/\/[a-z0-9-@:;%_\+.~#?&\/=]+) {0,1}/iS';
+ protected $codeTagCount = 0;
+ protected $codeTags = array ();
+ protected $preTagCount = 0;
+ protected $preTags = array ();
+ protected $paragraphRegex = '/(?:\r\n|\r|\n){2}/S';
+ protected $lineRegex = '/(?:\r\n|\r|\n)/S';
+
+ public function __construct (Setup $setup, CommentsUI $ui, array $comments)
+ {
+ $this->setup = $setup;
+ $this->ui = $ui;
+ $this->locale = new Locale ($setup);
+ $this->templater = new Templater ($setup->usage['mode'], $setup);
+ $this->markdown = new Markdown ();
+ $this->comments = $comments;
+ }
+
+ protected function fileFromPermalink ($permalink)
+ {
+ $file = substr ($permalink, 1);
+ $file = str_replace ('r', '-', $file);
+ $file = str_replace ('-pop', '', $file);
+
+ return $file;
+ }
+
+ protected function replyCheck ($permalink)
+ {
+ if (empty ($_GET['hashover-reply'])) {
+ return;
+ }
+
+ if ($_GET['hashover-reply'] === $permalink) {
+ $file = $this->fileFromPermalink ($permalink);
+
+ $form = new HTMLTag ('form', array (
+ 'id' => 'hashover-reply-' . $permalink,
+ 'class' => 'hashover-reply-form',
+ 'method' => 'post',
+ 'action' => $this->setup->getBackendPath ('form-actions.php')
+ ));
+
+ $form->innerHTML ($this->ui->replyForm ($permalink, $file));
+
+ return $form->asHTML ();
+ }
+ }
+
+ protected function editCheck ($comment)
+ {
+ if (empty ($_GET['hashover-edit'])) {
+ return;
+ }
+
+ $permalink = !empty ($comment['permalink']) ? $comment['permalink'] : '';
+
+ if ($_GET['hashover-edit'] === $permalink) {
+ $file = $this->fileFromPermalink ($permalink);
+
+ $body = $comment['body'];
+ $body = preg_replace ($this->linkRegex, '\\1', $body);
+ $status = !empty ($comment['status']) ? $comment['status'] : 'approved';
+ $name = !empty ($comment['name']) ? $comment['name'] : '';
+ $website = !empty ($comment['website']) ? $comment['website'] : '';
+ $subscribed = isset ($comment['subscribed']);
+
+ $form = new HTMLTag ('form', array (
+ 'id' => 'hashover-edit-' . $permalink,
+ 'class' => 'hashover-edit-form',
+ 'method' => 'post',
+ 'action' => $this->setup->getBackendPath ('form-actions.php')
+ ), false);
+
+ $edit_form = $this->ui->editForm ($permalink, $file, $name, $website, $body, $status, $subscribed);
+
+ $form->innerHTML ($edit_form);
+
+ return $form->asHTML ();
+ }
+ }
+
+ protected function codeTagReplace ($grp)
+ {
+ $code_placeholder = $grp[1] . 'CODE_TAG[' . $this->codeTagCount . ']' . $grp[3];
+ $this->codeTags[$this->codeTagCount] = trim ($grp[2], "\r\n");
+ $this->codeTagCount++;
+
+ return $code_placeholder;
+ }
+
+ protected function codeTagReturn ($grp) {
+ return $this->codeTags[($grp[1])];
+ }
+
+ protected function preTagReplace ($grp)
+ {
+ $pre_placeholder = $grp[1] . 'PRE_TAG[' . $this->preTagCount . ']' . $grp[3];
+ $this->preTags[$this->preTagCount] = trim ($grp[2], "\r\n");
+ $this->preTagCount++;
+
+ return $pre_placeholder;
+ }
+
+ protected function preTagReturn ($grp) {
+ return $this->preTags[($grp[1])];
+ }
+
+ // Returns the permalink of a comment's parent
+ protected function getParentPermalink ($permalink)
+ {
+ $permalink_parts = explode ('r', $permalink);
+ array_pop ($permalink_parts);
+
+ return implode ('r', $permalink_parts);
+ }
+
+ // Find a comment by its permalink
+ protected function findByPermalink ($permalink, $comments)
+ {
+ // Loop through all comments
+ foreach ($comments as $comment) {
+ // Return comment if its permalink matches
+ if ($comment['permalink'] === $permalink) {
+ return $comment;
+ }
+
+ // Recursively check replies when present
+ if (!empty ($comment['replies'])) {
+ $reply = $this->findByPermalink ($permalink, $comment['replies']);
+
+ if ($reply !== null) {
+ return $reply;
+ }
+ }
+ }
+
+ // Otherwise return null
+ return null;
+ }
+
+ public function parseComment (array $comment, $parent = null, $popular = false)
+ {
+ $template = array ();
+ $name_class = 'hashover-name-plain';
+ $comment_key = $comment['permalink'];
+ $permalink = 'hashover-' . $comment['permalink'];
+ $template['permalink'] = $comment_key;
+ $is_reply = ($parent !== null);
+ $this->codeTagCount = 0;
+ $this->codeTags = array ();
+ $this->preTagCount = 0;
+ $this->preTags = array ();
+
+ // Text for avatar image alt attribute
+ $permatext = substr ($comment_key, 1);
+ $permatext = explode ('r', $permatext);
+ $permatext = array_pop ($permatext);
+
+ // Wrapper element for each comment
+ $comment_wrapper = $this->ui->commentWrapper ($permalink);
+
+ // Get parent comment via permalink
+ if ($is_reply === false and strpos ($comment_key, 'r') !== false) {
+ $parent_permalink = $this->getParentPermalink ($comment_key);
+ $parent = $this->findByPermalink ($parent_permalink, $this->comments['primary']);
+ $is_reply = ($parent !== null);
+ }
+
+ // Check if this comment is a popular comment
+ if ($popular === true) {
+ // Remove "-pop" from text for avatar
+ $permatext = str_replace ('-pop', '', $permatext);
+ } else {
+ // Check if comment is a reply
+ if ($is_reply === true) {
+ // Append class to indicate comment is a reply
+ $comment_wrapper->appendAttribute ('class', 'hashover-reply');
+ }
+ }
+
+ // Add avatar image to template
+ $template['avatar'] = $this->ui->userAvatar ($comment['avatar'], $permalink, $permatext);
+
+ if (!isset ($comment['notice'])) {
+ $name = !empty ($comment['name']) ? $comment['name'] : $this->setup->defaultName;
+ $is_twitter = false;
+
+ // Check if user's name is a Twitter handle
+ if ($name[0] === '@') {
+ $name = mb_substr ($name, 1);
+ $name_class = 'hashover-name-twitter';
+ $is_twitter = true;
+ $name_length = mb_strlen ($name);
+
+ // Check if Twitter handle is valid length
+ if ($name_length > 1 and $name_length <= 30) {
+ // Set website to Twitter profile if a specific website wasn't given
+ if (empty ($comment['website'])) {
+ $comment['website'] = 'http://twitter.com/' . $name;
+ }
+ }
+ }
+
+ // Check whether user gave a website
+ if (!empty ($comment['website'])) {
+ if ($is_twitter === false) {
+ $name_class = 'hashover-name-website';
+ }
+
+ // If so, display name as a hyperlink
+ $name_link = $this->ui->nameElement ('a', $comment_key, $name, $comment['website']);
+ } else {
+ // If not, display name as plain text
+ $name_link = $this->ui->nameElement ('span', $comment_key, $name);
+ }
+
+ // Add "Top of Thread" hyperlink to template
+ if ($is_reply === true) {
+ $parent_thread = 'hashover-' . $parent['permalink'];
+ $parent_name = !empty ($parent['name']) ? $parent['name'] : $this->setup->defaultName;
+ $template['parent-link'] = $this->ui->parentThreadLink ($parent_thread, $comment_key, $parent_name);
+ }
+
+ if (isset ($comment['user-owned'])) {
+ // Append class to indicate comment is from logged in user
+ $comment_wrapper->appendAttribute ('class', 'hashover-user-owned');
+
+ // Define "Reply" link with original poster title
+ $reply_title = $this->locale->text['commenter-tip'];
+ $reply_class = 'hashover-no-email';
+ } else {
+ // Check if commenter is subscribed
+ if (isset ($comment['subscribed'])) {
+ // If so, set subscribed title
+ $reply_title = $name . ' ' . $this->locale->text['subscribed-tip'];
+ $reply_class = 'hashover-has-email';
+ } else{
+ // If not, set unsubscribed title
+ $reply_title = $name . ' ' . $this->locale->text['unsubscribed-tip'];
+ $reply_class = 'hashover-no-email';
+ }
+ }
+
+ // Check if the comment is editable for the user
+ if (isset ($comment['editable'])) {
+ // If so, add "Edit" hyperlink to template
+ if (!empty ($_GET['hashover-edit']) and $_GET['hashover-edit'] === $comment_key) {
+ $template['edit-link'] = $this->ui->cancelLink ($permalink, 'edit');
+ } else {
+ $template['edit-link'] = $this->ui->formLink ('', 'edit', $comment_key);
+ }
+ }
+
+ // Check if the comment has been liked
+ if (isset ($comment['likes'])) {
+ // Add likes to HTML template
+ $template['likes'] = $comment['likes'];
+
+ // Get "X Like(s)" locale
+ $plural = ($comment['likes'] === 1 ? 0 : 1);
+ $like_count = $comment['likes'] . ' ' . $this->locale->text['like'][$plural];
+
+ // Add like count to HTML template
+ $template['like-count'] = $this->ui->likeCount ('likes', $comment_key, $like_count);
+ }
+
+ // Check if dislikes are enabled and the comment's been disliked
+ if ($this->setup->allowsDislikes === true
+ and isset ($comment['dislikes']))
+ {
+ // Add likes to HTML template
+ $template['dislikes'] = $comment['dislikes'];
+
+ // Get "X Dislike(s)" locale
+ $plural = ($comment['dislikes'] === 1 ? 0 : 1);
+ $dislike_count = $comment['dislikes'] . ' ' . $this->locale->text['dislike'][$plural];
+
+ // Add dislike count to HTML template
+ $template['dislike-count'] = $this->ui->likeCount ('dislikes', $comment_key, $dislike_count);
+ }
+
+ // Add name HTML to template
+ $template['name'] = $this->ui->nameWrapper ($name_class, $name_link);
+
+ // Append status text to date
+ if (!empty ($comment['status-text'])) {
+ $comment['date'] .= ' (' . $comment['status-text'] . ')';
+ }
+
+ // Add date permalink hyperlink to template
+ $template['date'] = $this->ui->dateLink ('', $permalink, $comment['date']);
+
+ // Add "Reply" hyperlink to template
+ if (!empty ($_GET['hashover-reply']) and $_GET['hashover-reply'] === $comment_key) {
+ $template['reply-link'] = $this->ui->cancelLink ($permalink, 'reply', $reply_class);
+ } else {
+ $template['reply-link'] = $this->ui->formLink ('', 'reply', $comment_key, $reply_class, $reply_title);
+ }
+
+ // Add edit form HTML to template
+ if (isset ($comment['editable'])) {
+ $template['edit-form'] = $this->editCheck ($comment);
+ }
+
+ // Add reply form HTML to template
+ $template['reply-form'] = $this->replyCheck ($comment_key);
+
+ // Add reply count to template
+ if (!empty ($comment['replies'])) {
+ $template['reply-count'] = count ($comment['replies']);
+
+ if ($template['reply-count'] > 0) {
+ if ($template['reply-count'] !== 1) {
+ $template['reply-count'] .= ' ' . $this->locale->text['replies'];
+ } else {
+ $template['reply-count'] .= ' ' . $this->locale->text['reply'];
+ }
+ }
+ }
+
+ // Add comment data to template
+ $template['comment'] = $comment['body'];
+
+ // Remove [img] tags
+ $template['comment'] = preg_replace ('/\[(img|\/img)\]/iS', '', $template['comment']);
+
+ // Add HTML anchor tag to URLs (hyperlinks)
+ $template['comment'] = preg_replace ($this->linkRegex, '<a href="\\1" rel="noopener noreferrer" target="_blank">\\1</a>', $template['comment']);
+
+ // Parse markdown in comment
+ if ($this->setup->usesMarkdown !== false) {
+ $template['comment'] = $this->markdown->parseMarkdown ($template['comment']);
+ }
+
+ // Check for code tags
+ if (mb_strpos ($template['comment'], '<code>') !== false) {
+ // Replace code tags with placeholder text
+ $template['comment'] = preg_replace_callback ('/(<code>)([\s\S]*?)(<\/code>)/iS', 'self::codeTagReplace', $template['comment']);
+ }
+
+ // Check for pre tags
+ if (mb_strpos ($template['comment'], '<pre>') !== false) {
+ // Replace pre tags with placeholder text
+ $template['comment'] = preg_replace_callback ('/(<pre>)([\s\S]*?)(<\/pre>)/iS', 'self::preTagReplace', $template['comment']);
+ }
+
+ // Check for various multi-line tags
+ foreach ($this->trimTagRegexes as $tag => $trimTagRegex) {
+ if (mb_strpos ($template['comment'], '<' . $tag . '>') !== false) {
+ // Trim leading and trailing whitespace
+ $template['comment'] = preg_replace_callback ($trimTagRegex, function ($grp) {
+ return $grp[1] . trim ($grp[2], "\r\n") . $grp[3];
+ }, $template['comment']);
+ }
+ }
+
+ // Break comment into paragraphs
+ $paragraphs = preg_split ($this->paragraphRegex, $template['comment']);
+ $pd_comment = '';
+
+ for ($i = 0, $il = count ($paragraphs); $i < $il; $i++) {
+ // Wrap comment in paragraph tag, replace single line breaks with break tags
+ $pd_comment .= '<p>' . preg_replace ($this->lineRegex, '<br>', $paragraphs[$i]) . '</p>' . PHP_EOL;
+ }
+
+ // Replace code tag placeholders with original code tag HTML
+ if ($this->codeTagCount > 0) {
+ $pd_comment = preg_replace_callback ('/CODE_TAG\[([0-9]+)\]/S', 'self::codeTagReturn', $pd_comment);
+ }
+
+ // Replace pre tag placeholders with original pre tag HTML
+ if ($this->preTagCount > 0) {
+ $pd_comment = preg_replace_callback ('/PRE_TAG\[([0-9]+)\]/S', 'self::preTagReturn', $pd_comment);
+ }
+
+ // Add paragraph'd comment data to template
+ $template['comment'] = $pd_comment;
+ } else {
+ // Append notice class
+ $comment_wrapper->appendAttribute ('class', 'hashover-notice');
+ $comment_wrapper->appendAttribute ('class', $comment['notice-class']);
+
+ // Add notice to template
+ $template['comment'] = $comment['notice'];
+
+ // Set name to 'Comment Deleted!'
+ $template['name'] = $this->ui->nameWrapper ($name_class, $comment['title']);
+ }
+
+ // Parse theme layout HTML template
+ $theme_html = $this->templater->parseTheme ('comments.html', $template);
+
+ // Comment HTML template
+ $comment_wrapper->innerHTML ($theme_html);
+
+ // Recursively parse replies
+ if (!empty ($comment['replies'])) {
+ foreach ($comment['replies'] as $reply) {
+ $comment_wrapper->appendInnerHTML ($this->parseComment ($reply, $comment));
+ }
+ }
+
+ return $comment_wrapper->asHTML ();
+ }
+}
diff --git a/bootstrap/comments/backend/classes/postdata.php b/bootstrap/comments/backend/classes/postdata.php
new file mode 100644
index 0000000..7ca143a
--- /dev/null
+++ b/bootstrap/comments/backend/classes/postdata.php
@@ -0,0 +1,93 @@
+<?php namespace HashOver;
+
+// Copyright (C) 2015-2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// HashOver is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// HashOver is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with HashOver. If not, see <http://www.gnu.org/licenses/>.
+
+
+class PostData
+{
+ public $postData = array ();
+ public $remoteAccess = false;
+ public $file;
+ public $replyTo;
+ public $viaAJAX = false;
+
+ public function __construct ()
+ {
+ // Use POST or GET based on whether request is for JSONP
+ $postget = isset ($_GET['jsonp']) ? $_GET : $_POST;
+
+ // Set status
+ if (isset ($postget['status'])) {
+ $this->postData['status'] = $this->ForceUTF8 ($postget['status']);
+ }
+
+ // Set name
+ if (isset ($postget['name'])) {
+ $this->postData['name'] = $this->ForceUTF8 ($postget['name']);
+ }
+
+ // Set password
+ if (isset ($postget['password'])) {
+ $this->postData['password'] = $this->ForceUTF8 ($postget['password']);
+ }
+
+ // Set e-mail address
+ if (isset ($postget['email'])) {
+ $this->postData['email'] = $this->ForceUTF8 ($postget['email']);
+ }
+
+ // Set website URL
+ if (isset ($postget['website'])) {
+ $this->postData['website'] = $this->ForceUTF8 ($postget['website']);
+ }
+
+ // Set comment
+ if (isset ($postget['comment'])) {
+ $this->postData['comment'] = $this->ForceUTF8 ($postget['comment']);
+ }
+
+ // Set indicator of remote access
+ if (isset ($postget['remote-access'])) {
+ $this->remoteAccess = true;
+ }
+
+ // Get comment file
+ if (isset ($postget['file'])) {
+ $this->file = $postget['file'];
+ }
+
+ // Get reply comment file
+ if (isset ($postget['reply-to'])) {
+ $this->replyTo = $postget['reply-to'];
+ }
+
+ // Set indicator of AJAX requests
+ if (isset ($postget['ajax'])) {
+ $this->viaAJAX = true;
+ }
+ }
+
+ // Force a string to UTF-8 encoding and acceptable character range
+ protected function ForceUTF8 ($string)
+ {
+ $string = mb_convert_encoding ($string, 'UTF-16', 'UTF-8');
+ $string = mb_convert_encoding ($string, 'UTF-8', 'UTF-16');
+ $string = preg_replace ('/[^\x{0009}\x{000A}\x{000D}\x{0020}-\x{D7FF}\x{E000}-\x{FFFD}\x{10000}-\x{10FFFF}]/u', '?', $string);
+
+ return trim ($string, " \r\n\t");
+ }
+}
diff --git a/bootstrap/comments/backend/classes/secrets.php b/bootstrap/comments/backend/classes/secrets.php
new file mode 100644
index 0000000..6c704ca
--- /dev/null
+++ b/bootstrap/comments/backend/classes/secrets.php
@@ -0,0 +1,41 @@
+<?php namespace HashOver;
+
+// Copyright (C) 2010-2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// I, Jacob Barkdull, hereby release this work into the public domain.
+// This applies worldwide. If this is not legally possible, I grant any
+// entity the right to use this work for any purpose, without any
+// conditions, unless such conditions are required by law.
+//
+//--------------------
+//
+// IMPORTANT NOTICE:
+//
+// To retain your settings and maintain proper functionality, when
+// downloading or otherwise upgrading to a new version of HashOver it
+// is important that you preserve this file, unless directed otherwise.
+//
+// It is also important to choose UNIQUE values for the encryption key,
+// admin name, and admin password, as not doing so puts HashOver at
+// risk of being hijacked. Allowing someone to delete comments and/or
+// edit existing comments to post spam, impersonate you or your
+// visitors in order to push some sort of agenda/propaganda, to defame
+// you or your visitors, or to imply endorsement of some product(s),
+// service(s), and/or political ideology.
+
+
+class Secrets
+{
+ // E-mail for notification of new comments
+ public $notificationEmail = 'edwinmattiacci@yahoo.com';
+
+ // Unique encryption key (case-sensitive)
+ protected $encryptionKey = 'nC6s-@H\ _';
+
+ // Login name to gain admin rights (case-sensitive)
+ protected $adminName = 'admin';
+
+ // Login password to gain admin rights (case-sensitive)
+ protected $adminPassword = 'passwd';
+}
diff --git a/bootstrap/comments/backend/classes/settings.php b/bootstrap/comments/backend/classes/settings.php
new file mode 100644
index 0000000..5a50c6e
--- /dev/null
+++ b/bootstrap/comments/backend/classes/settings.php
@@ -0,0 +1,341 @@
+<?php namespace HashOver;
+
+// Copyright (C) 2010-2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// HashOver is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// HashOver is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with HashOver. If not, see <http://www.gnu.org/licenses/>.
+//
+//--------------------
+//
+// IMPORTANT NOTICE:
+//
+// Do not edit this file unless you know what you are doing. Instead,
+// please use the HashOver administration panel to graphically adjust
+// the settings, or create/edit the settings JSON file.
+
+
+// Default and advanced HashOver settings
+class Settings extends Secrets
+{
+ // Primary settings
+ public $language = 'auto'; // UI language, for example 'en', 'de', etc. 'auto' to use system locale
+ public $theme = 'default'; // Comment Cascading Style Sheet (CSS)
+ public $usesModeration = false; // Whether comments must be approved before they appear to other visitors
+ public $pendsUserEdits = false; // Whether comments need to be approved again when edited
+ public $dataFormat = 'xml'; // Format comments will be stored in; options: xml, json, sql
+ public $defaultName = 'Anonymous'; // Default name to use when one isn't given
+ public $allowsImages = true; // Whether external image URLs wrapped in [img] tags are embedded
+ public $allowsLogin = true; // Whether users can login and logout (when false form cookies are still set)
+ public $allowsLikes = true; // Whether a "Like" link is displayed
+ public $allowsDislikes = false; // Whether a "Dislike" link is displayed; allowing Reddit-style voting
+ public $usesAjax = true; // Whether AJAX is used for posting, editing, and loading comments
+ public $collapsesInterface = false; // Whether the comment form, thread, and end links are all initially hidden
+ public $collapsesComments = true; // Whether to hide comments and display a link to show them
+ public $collapseLimit = 3; // Number of comments that aren't hidden
+ public $replyMode = 'thread'; // Whether to display replies as a 'thread' or as a 'stream'
+ public $streamDepth = 3; // In stream mode, the number of reply indentions to allow before the thread flattens
+ public $popularityThreshold = 5; // Minimum likes a comment needs to be popular
+ public $popularityLimit = 2; // Number of comments allowed to become popular
+ public $usesMarkdown = true; // Whether comments will be parsed for Markdown
+
+ // Date and Time settings
+ public $serverTimezone = 'America/Los_Angeles'; // Server timezone
+ public $usesUserTimezone = true; // Whether comment dates should use the user's timezone (JavaScript-mode)
+ public $usesShortDates = true; // Whether comment dates are shortened, for example "X days ago"
+ public $timeFormat = 'g:ia'; // Time format, use 'H:i' for 24-hour format (see: http://php.net/manual/en/function.date.php)
+ public $dateFormat = 'm/d/Y'; // Date format (see: http://php.net/manual/en/function.date.php)
+
+ // Field options, use true/false to enable/disable a field,
+ // use 'required' to require a field be properly filled
+ public $fieldOptions = array (
+ 'name' => true,
+ 'password' => false,
+ 'email' => true,
+ 'website' => false
+ );
+
+ // Behavior settings
+ public $displaysTitle = false; // Whether page title is shown or not
+ public $formPosition = 'top'; // Position for primary form; options: 'top' or 'bottom'
+ public $usesAutoLogin = true; // Whether a user's first comment automatically logs them in
+ public $showsReplyCount = false; // Whether to show reply count separately from total
+ public $countIncludesDeleted = true; // Whether comment counts should include deleted comments
+ public $iconMode = 'none'; // How to display avatar icons (either 'image', 'count' or 'none')
+ public $iconSize = 45; // Size of Gravatar icons in pixels
+ public $imageFormat = 'png'; // Format for icons and other images (use 'svg' for HDPI)
+ public $usesLabels = false; // Whether to display labels above inputs
+ public $usesCancelButtons = true; // Whether forms have "Cancel" buttons
+ public $appendsCss = false; // Whether to automatically add a CSS <link> element to the page <head>
+ public $appendsRss = false; // Whether a comment RSS feed link is displayed
+
+ // Technical settings
+ public $loginMethod = 'defaultLogin'; // Login method class for handling user login information
+ public $setsCookies = true; // Whether cookies are enabled
+ public $secureCookies = false; // Whether cookies set over secure HTTPS will only be transmitted over HTTPS
+ public $storesIpAddress = false; // Whether to store users' IP addresses
+ public $subscribesUser = true; // Whether to subscribe the user to e-mail notifications by default
+ public $allowsUserReplies = false; // Whether given e-mails are sent as reply-to address to users
+ public $noreplyEmail = 'noreply@example.com'; // E-mail used when no e-mail is given
+ public $spamDatabase = 'remote'; // Whether to use a remote or local spam database
+ public $spamCheckModes = 'php'; // Perform IP spam check in 'json' or 'php' mode, or 'both'
+ public $gravatarDefault = 'custom'; // Gravatar theme to use ('custom', 'identicon', 'monsterid', 'wavatar', or 'retro')
+ public $gravatarForce = false; // Whether to force the themed Gravatar images instead of an avatar image
+ public $minifiesJavascript = false; // Whether JavaScript output should be minified
+ public $minifyLevel = 4; // How much to minify JavaScript code, options: 1, 2, 3, 4
+ public $enabledApi = array ('all'); // An array of enabled API. 'all' = fully-enabled, empty array = fully disabled
+ public $latestMax = 10; // Maximum number of comments to save as latest comments
+ public $latestTrimWidth = 100; // Number of characters to trim latest comments to, 0 for no trim
+ public $userDeletionsUnlink = false; // Whether user deleted files are actually unlinked from the filesystem
+ public $allowLocalMetadata = false; // Whether default metadata should be collected while running on a local server
+
+ // Types of images allowed to be embedded in comments
+ public $imageTypes = array (
+ 'jpeg',
+ 'jpg',
+ 'png',
+ 'gif'
+ );
+
+ // External domains allowed to remotely load HashOver scripts
+ public $allowedDomains = array (
+ // '*.example.com',
+ // '*.example.org',
+ // '*.example.net'
+ );
+
+ // General database options
+ public $databaseType = 'sqlite'; // Type of database, sqlite or mysql
+ public $databaseName = 'hashover-threads'; // Database name
+
+ // SQL database options
+ public $databaseHost = 'localhost'; // Database host name
+ public $databaseUser = 'root'; // Database login user
+ public $databasePassword = 'password'; // Database login password
+ public $databaseCharset = 'utf8'; // Database character set
+
+ // Automated settings
+ public $isMobile = false;
+
+ // Technical settings placeholders
+ public $rootDirectory;
+ public $httpRoot;
+ public $httpBackend;
+ public $httpImages;
+ public $cookieExpiration;
+ public $domain;
+
+ public function __construct ()
+ {
+ // Theme path
+ $this->themePath = 'themes/' . $this->theme;
+
+ // Set server timezone
+ date_default_timezone_set ($this->serverTimezone);
+
+ // Set encoding
+ mb_internal_encoding ('UTF-8');
+
+ // Get parent directory
+ $root_directory = dirname (dirname (__DIR__));
+
+ // Get HTTP parent directory
+ $document_root = realpath ($_SERVER['DOCUMENT_ROOT']);
+ $http_directory = mb_substr ($root_directory, mb_strlen ($document_root));
+
+ // Replace backslashes with forward slashes on Windows
+ if (DIRECTORY_SEPARATOR === '\\') {
+ $http_directory = str_replace ('\\', '/', $http_directory);
+ }
+
+ // Determine HTTP or HTTPS
+ $protocol = ($this->isHTTPS () ? 'https' : 'http') . '://';
+
+ // Technical settings
+ $this->rootDirectory = $root_directory; // Root directory for script
+ $this->httpRoot = $http_directory; // Root directory for HTTP
+ $this->httpBackend = $http_directory . '/backend'; // Backend directory for HTTP
+ $this->httpImages = $http_directory . '/images'; // Image directory for HTTP
+ $this->cookieExpiration = time () + 60 * 60 * 24 * 30; // Cookie expiration date
+ $this->domain = $_SERVER['HTTP_HOST']; // Domain name for refer checking & notifications
+ $this->absolutePath = $protocol . $this->domain; // Absolute path or remote access
+
+ // Load JSON settings
+ $this->jsonSettings ();
+
+ // Synchronize settings
+ $this->syncSettings ();
+ }
+
+ function isHTTPS ()
+ {
+ // The connection is HTTPS if server says so
+ if (!empty ($_SERVER['HTTPS']) and $_SERVER['HTTPS'] !== 'off') {
+ return true;
+ }
+
+ // Assume the connection is HTTPS on standard SSL port
+ if ($_SERVER['SERVER_PORT'] == 443) {
+ return true;
+ }
+
+ return false;
+ }
+
+ // Returns a server-side absolute file path
+ public function getAbsolutePath ($file)
+ {
+ return $this->rootDirectory . '/' . trim ($file, '/');
+ }
+
+ // Returns a client-side path for a file within the HashOver root
+ public function getHttpPath ($file)
+ {
+ return $this->httpRoot . '/' . trim ($file, '/');
+ }
+
+ // Returns a client-side path for a file within the backend directory
+ public function getBackendPath ($file)
+ {
+ return $this->httpBackend . '/' . trim ($file, '/');
+ }
+
+ // Returns a client-side path for a file within the images directory
+ public function getImagePath ($filename)
+ {
+ $path = $this->httpImages . '/' . trim ($filename, '/');
+ $path .= '.' . $this->imageFormat;
+
+ return $path;
+ }
+
+ // Returns a client-side path for a file within the configured theme
+ public function getThemePath ($file, $http = true)
+ {
+ // Path to the requested file in the configured theme
+ $theme_file = $this->themePath . '/' . $file;
+
+ // Use the same file from the default theme if it doesn't exist
+ if (!file_exists ($this->getAbsolutePath ($theme_file))) {
+ $theme_file = 'themes/default/' . $file;
+ }
+
+ // Convert the theme file path for HTTP use if told to
+ if ($http !== false) {
+ $theme_file = $this->getHttpPath ($theme_file);
+ }
+
+ return $theme_file;
+ }
+
+ public function jsonSettings ()
+ {
+ // JSON settings file path
+ $path = $this->getAbsolutePath ('config/settings.json');
+
+ // Do nothing if the JSON settings file doesn't exist
+ if (!file_exists ($path)) {
+ return;
+ }
+
+ // Get JSON data
+ $data = @file_get_contents ($path);
+
+ // Load and decode JSON settings file
+ $json_settings = @json_decode ($data, true);
+
+ // Return void on failure
+ if ($json_settings === null) {
+ return;
+ }
+
+ // Loop through each setting
+ foreach ($json_settings as $key => $value) {
+ // Convert setting name to camelCase
+ $title_case_key = ucwords (str_replace ('-', ' ', strtolower ($key)));
+ $setting = lcfirst (str_replace (' ', '', $title_case_key));
+
+ // Check if the JSON setting property exists in the defaults
+ if (property_exists ($this, $setting)) {
+ // Check if the JSON value is the same type as the default
+ if (gettype ($value) === gettype ($this->{$setting})) {
+ // Override default setting
+ $this->{$setting} = $value;
+ }
+ }
+ }
+ }
+
+ // Synchronizes specific settings after remote changes
+ public function syncSettings ()
+ {
+ // Theme path
+ $this->themePath = 'themes/' . $this->theme;
+
+ // Disable likes and dislikes if cookies are disabled
+ if ($this->setsCookies === false) {
+ $this->allowsLikes = false;
+ $this->allowsDislikes = false;
+ }
+
+ // Setup default field options
+ foreach (array ('name', 'password', 'email', 'website') as $field) {
+ if (!isset ($this->fieldOptions[$field])) {
+ $this->fieldOptions[$field] = true;
+ }
+ }
+
+ // Disable password if name is disabled
+ if ($this->fieldOptions['name'] === false) {
+ $this->fieldOptions['password'] = false;
+ }
+
+ // Disable login if name or password is disabled
+ if ($this->fieldOptions['name'] === false
+ or $this->fieldOptions['password'] === false)
+ {
+ $this->allowsLogin = false;
+ }
+
+ // Disable autologin if login is disabled
+ if ($this->allowsLogin === false) {
+ $this->usesAutoLogin = false;
+ }
+
+ // Backend directory for HTTP
+ $this->httpBackend = $this->httpRoot . '/backend';
+
+ // Image directory for HTTP
+ $this->httpImages = $this->httpRoot . '/images';
+ }
+
+ // Check if a given API format is enabled
+ public function apiStatus ($api)
+ {
+ // Check if the given API is enabled
+ if (is_array ($this->enabledApi)) {
+ // Return enabled if all available APIs are enabled
+ if (in_array ('all', $this->enabledApi)) {
+ return 'enabled';
+ }
+
+ // Return enabled if the given API is enabled
+ if (in_array ($api, $this->enabledApi)) {
+ return 'enabled';
+ }
+ }
+
+ // Otherwise, assume API is disabled by default
+ return 'disabled';
+ }
+}
diff --git a/bootstrap/comments/backend/classes/setup.php b/bootstrap/comments/backend/classes/setup.php
new file mode 100644
index 0000000..c3576d4
--- /dev/null
+++ b/bootstrap/comments/backend/classes/setup.php
@@ -0,0 +1,446 @@
+<?php namespace HashOver;
+
+// Copyright (C) 2010-2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// HashOver is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// HashOver is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with HashOver. If not, see <http://www.gnu.org/licenses/>.
+
+
+class Setup extends Settings
+{
+ public $usage;
+ public $encryption;
+ public $remoteAccess = false;
+ public $pageURL;
+ public $pageTitle;
+ public $filePath;
+ public $threadName;
+ public $commentsDirectory;
+ public $pagesDirectory;
+ public $threadDirectory;
+ public $URLQueryList = array ();
+ public $URLQueries;
+
+ // Required extensions to check for
+ public $extensions = array (
+ 'date',
+ 'dom',
+ 'json',
+ 'mbstring',
+ 'openssl',
+ 'pcre',
+ 'PDO',
+ 'SimpleXML'
+ );
+
+ // Characters that aren't allowed in directory names
+ public $reservedCharacters = array (
+ '<',
+ '>',
+ ':',
+ '"',
+ '/',
+ '\\',
+ '|',
+ '?',
+ '&',
+ '!',
+ '*',
+ '.',
+ '=',
+ '_',
+ '+',
+ ' '
+ );
+
+ // HashOver-specific URL queries to be ignored
+ public $ignoredQueries = array (
+ 'hashover-reply',
+ 'hashover-edit'
+ );
+
+ // Default metadata
+ public $metadata = array (
+ 'title' => '',
+ 'url' => '',
+ 'status' => 'open',
+ 'latest' => array ()
+ );
+
+ public function __construct (array $usage)
+ {
+ parent::__construct ();
+
+ $this->usage = $usage;
+ $this->misc = new Misc ($usage['mode']);
+
+ // Check if PHP version is the minimum required
+ if (version_compare (PHP_VERSION, '5.3.3') < 0) {
+ $version_parts = explode ('-', PHP_VERSION);
+ $version = current ($version_parts);
+
+ throw new \Exception ('PHP ' . $version . ' is too old. Must be at least version 5.3.3.');
+ }
+
+ // Check for required extensions
+ $this->extensionsLoaded ($this->extensions);
+
+ // Comments directory path
+ $this->commentsDirectory = $this->getAbsolutePath ('comments');
+
+ // Comment threads directory path
+ $this->pagesDirectory = $this->commentsDirectory . '/threads';
+
+ // Throw exception if for Blowfish hashing support isn't detected
+ if ((defined ('CRYPT_BLOWFISH') and CRYPT_BLOWFISH) === false) {
+ throw new \Exception ('Failed to find CRYPT_BLOWFISH. Blowfish hashing support is required.');
+ }
+
+ // Throw exception if notification email is set to the default
+ if ($this->notificationEmail === 'example@example.com') {
+ throw new \Exception (sprintf (
+ 'You must use a UNIQUE notification e-mail in %s',
+ $this->getBackendPath ('classes/settings.php')
+ ));
+ }
+
+ // Throw exception if encryption key is set to the default
+ if ($this->encryptionKey === '8CharKey') {
+ throw new \Exception (sprintf (
+ 'You must use a UNIQUE encryption key in %s',
+ $this->getBackendPath ('classes/settings.php')
+ ));
+ }
+
+ // Throw exception if administrative password is set to the default
+ if ($this->adminPassword === 'password') {
+ throw new \Exception (sprintf (
+ 'You must use a UNIQUE admin password in %s',
+ $this->getBackendPath ('classes/settings.php')
+ ));
+ }
+
+ // Throw exception if the script wasn't requested by this server
+ if ($this->usage['mode'] !== 'php') {
+ if ($this->refererCheck () === false) {
+ throw new \Exception ('External use not allowed.');
+ }
+ }
+
+ // Instantiate encryption class
+ $this->encryption = new Encryption ($this->encryptionKey);
+
+ // Check if visitor is on mobile device
+ if (!empty ($_SERVER['HTTP_USER_AGENT'])) {
+ if (preg_match ('/(android|blackberry|phone|mobile|tablet)/i', $_SERVER['HTTP_USER_AGENT'])) {
+ // Adjust settings to accommodate
+ $this->isMobile = true;
+ $this->imageFormat = 'svg';
+ }
+ }
+ }
+
+ public function extensionsLoaded (array $extensions)
+ {
+ // Throw exceptions if an extension isn't loaded
+ foreach ($extensions as $extension) {
+ if (extension_loaded ($extension) === false) {
+ throw new \Exception ('Failed to detect required extension: ' . $extension . '.');
+ }
+ }
+ }
+
+ public function getRequest ($key, $default = false)
+ {
+ // Return default value if POST and GET are empty
+ if (empty ($_GET[$key]) and empty ($_POST[$key])) {
+ return $default;
+ }
+
+ // Attempt to obtain GET data
+ if (!empty ($_GET[$key])) {
+ $request = $_GET[$key];
+ }
+
+ // Attempt to obtain POST data
+ if (!empty ($_POST[$key])) {
+ $request = $_POST[$key];
+ }
+
+ // Strip escape slashes from POST or GET
+ if (get_magic_quotes_gpc ()) {
+ $request = stripslashes ($request);
+ }
+
+ return $request;
+ }
+
+ protected function getDomainWithPort ($url = '')
+ {
+ // Parse URL
+ $url = parse_url ($url);
+
+ if ($url === false or empty ($url['host'])) {
+ throw new \Exception ('Failed to obtain domain name.');
+ }
+
+ // If URL has a port, return domain with port
+ if (!empty ($url['port'])) {
+ return $url['host'] . ':' . $url['port'];
+ }
+
+ // Otherwise return domain without port
+ return $url['host'];
+ }
+
+ protected function setupRemoteAccess ()
+ {
+ $this->remoteAccess = true;
+ $this->httpRoot = $this->absolutePath . $this->httpRoot;
+ $this->syncSettings ();
+ }
+
+ protected function refererCheck ()
+ {
+ // No referer set
+ if (empty ($_SERVER['HTTP_REFERER'])) {
+ return true;
+ }
+
+ // Get HTTP referer domain with port
+ $domain = $this->getDomainWithPort ($_SERVER['HTTP_REFERER']);
+
+ // Check if the script was requested by this server
+ if ($domain === $this->domain) {
+ return true;
+ }
+
+ // Run through allowed domains
+ foreach ($this->allowedDomains as $allowed_domain) {
+ $sub_regex = '/^' . preg_quote ('\*\.') . '/S';
+ $safe_domain = preg_quote ($allowed_domain);
+ $domain_regex = preg_replace ($sub_regex, '(?:.*?\.)*', $safe_domain);
+ $domain_regex = '/^' . $domain_regex . '$/iS';
+
+ // Check if the script was requested from an allowed domain
+ if (preg_match ($domain_regex, $domain)) {
+ // If so, setup remote access
+ $this->setupRemoteAccess ();
+ return true;
+ }
+ }
+
+ // Check if the usage context is an API
+ if ($this->usage['context'] === 'api') {
+ // If so, setup remote access
+ $this->setupRemoteAccess ();
+ return true;
+ }
+
+ return false;
+ }
+
+ protected function getPageURL ()
+ {
+ // Attempt to obtain URL via GET or POST
+ $request = $this->getRequest ('url');
+
+ // Return on success
+ if ($request !== false) {
+ return $request;
+ }
+
+ // Attempt to obtain URL via HTTP referer
+ if (!empty ($_SERVER['HTTP_REFERER'])) {
+ return $_SERVER['HTTP_REFERER'];
+ }
+
+ // Error on failure
+ throw new \Exception ('Failed to obtain page URL.');
+ }
+
+ protected function sanitizeData ($data = '')
+ {
+ // Strip HTML tags from data
+ $data = strip_tags (html_entity_decode ($data, false, 'UTF-8'));
+
+ // Encode HTML characters in data
+ $data = htmlspecialchars ($data, false, 'UTF-8', false);
+
+ return $data;
+ }
+
+ protected function requestData ($data = '', $default = false)
+ {
+ // Attempt to obtain data via GET or POST
+ $request = $this->getRequest ($data, $default);
+
+ // Return on success
+ if ($request !== $default) {
+ $request = $this->sanitizeData ($request);
+ }
+
+ return $request;
+ }
+
+ public function setThreadName ($name = '')
+ {
+ // Requesting the title if told to
+ if ($name === 'request') {
+ $name = $this->requestData ('thread', $this->threadName);
+ }
+
+ // Replace reserved characters with dashes
+ $name = str_replace ($this->reservedCharacters, '-', $name);
+
+ // Remove multiple dashes
+ if (mb_strpos ($name, '--') !== false) {
+ $name = preg_replace ('/-{2,}/', '-', $name);
+ }
+
+ // Remove leading and trailing dashes
+ $name = trim ($name, '-');
+
+ // Final comment directory name
+ $this->threadDirectory = $this->pagesDirectory . '/' . $name;
+ $this->threadName = $name;
+ }
+
+ protected function getIgnoredQueries ()
+ {
+ // Ignored URL queries list file
+ $ignored_queries = $this->getAbsolutePath ('config/ignored-queries.json');
+
+ // Queries to be ignored
+ $queries = $this->ignoredQueries;
+
+ // Check if ignored URL queries list file exists
+ if (file_exists ($ignored_queries)) {
+ // If so, get ignored URL queries list
+ $data = @file_get_contents ($ignored_queries);
+
+ // Parse ignored URL queries list JSON
+ $json = @json_decode ($data, true);
+
+ // Check if file parsed successfully
+ if ($json !== null) {
+ // If so, merge ignored URL queries file with defaults
+ $queries = array_merge ($json, $queries);
+ }
+ }
+
+ return $queries;
+ }
+
+ public function setPageURL ($url = '')
+ {
+ // Set page URL
+ $this->pageURL = $url;
+
+ // Request page URL by default
+ if (empty ($url) or $url === 'request') {
+ $this->pageURL = $this->getPageURL ();
+ }
+
+ // Strip HTML tags from page URL
+ $this->pageURL = strip_tags (html_entity_decode ($this->pageURL, false, 'UTF-8'));
+
+ // Turn page URL into array
+ $url_parts = parse_url ($this->pageURL);
+
+ // Set initial path
+ if (empty ($url_parts['path']) or $url_parts['path'] === '/') {
+ $this->threadName = 'index';
+ $this->filePath = '/';
+ } else {
+ // Remove starting slash
+ $this->threadName = mb_substr ($url_parts['path'], 1);
+
+ // Set file path
+ $this->filePath = $url_parts['path'];
+ }
+
+ // Remove unwanted URL queries
+ if (!empty ($url_parts['query'])) {
+ $url_queries = explode ('&', $url_parts['query']);
+ $ignored_queries = $this->getIgnoredQueries ();
+
+ for ($q = 0, $ql = count ($url_queries); $q < $ql; $q++) {
+ if (!in_array ($url_queries[$q], $ignored_queries, true)) {
+ $equals = explode ('=', $url_queries[$q]);
+
+ if (!in_array ($equals[0], $ignored_queries, true)) {
+ $this->URLQueryList[] = $url_queries[$q];
+ }
+ }
+ }
+
+ $this->URLQueries = implode ('&', $this->URLQueryList);
+ $this->threadName .= '-' . $this->URLQueries;
+ }
+
+ // Encode HTML characters in page URL
+ $this->pageURL = htmlspecialchars ($this->pageURL, false, 'UTF-8', false);
+
+ // Final URL
+ if (!empty ($url_parts['scheme']) and !empty ($url_parts['host'])) {
+ $this->pageURL = $url_parts['scheme'] . '://';
+ $this->pageURL .= $url_parts['host'];
+ } else {
+ throw new \Exception ('URL needs a hostname and scheme.');
+ }
+
+ // Add optional port to URL
+ if (!empty ($url_parts['port'])) {
+ $this->pageURL .= ':' . $url_parts['port'];
+ }
+
+ // Add file path
+ $this->pageURL .= $this->filePath;
+
+ // Add option queries
+ if (!empty ($this->URLQueries)) {
+ $this->pageURL .= '?' . $this->URLQueries;
+ }
+
+ // Set thread directory name to page URL
+ $this->setThreadName ($this->threadName);
+ }
+
+ public function setPageTitle ($title = '')
+ {
+ // Requesting the title if told to
+ if ($title === 'request') {
+ $title = $this->requestData ('title', '');
+ }
+
+ // Sanitize page title
+ $title = $this->sanitizeData ($title);
+
+ // Set page title
+ $this->pageTitle = $title;
+ }
+
+ // Weak verification of an admin login
+ public function adminLogin ($hash)
+ {
+ return ($hash === hash ('ripemd160', $this->adminName . $this->adminPassword));
+ }
+
+ // Strict verification of an admin login
+ public function verifyAdmin ($password)
+ {
+ return $this->encryption->verifyHash ($this->adminPassword, $password);
+ }
+}
diff --git a/bootstrap/comments/backend/classes/sourcecode.php b/bootstrap/comments/backend/classes/sourcecode.php
new file mode 100644
index 0000000..3c90c5c
--- /dev/null
+++ b/bootstrap/comments/backend/classes/sourcecode.php
@@ -0,0 +1,474 @@
+<?php namespace HashOver;
+
+// Copyright (C) 2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// HashOver is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// HashOver is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with HashOver. If not, see <http://www.gnu.org/licenses/>.
+
+
+class SourceCode
+{
+ public $files = array (
+ array (
+ 'type' => 'Script',
+ 'path' => 'admin/views/blocklist/index.php'
+ ),
+ array (
+ 'type' => 'Script',
+ 'path' => 'admin/views/documentation/index.php'
+ ),
+ array (
+ 'type' => 'Script',
+ 'path' => 'admin/views/example/index.php'
+ ),
+ array (
+ 'type' => 'Script',
+ 'path' => 'admin/views/url-queries/index.php'
+ ),
+ array (
+ 'type' => 'Script',
+ 'path' => 'admin/views/login/index.php'
+ ),
+ array (
+ 'type' => 'Script',
+ 'path' => 'admin/views/moderation/index.php'
+ ),
+ array (
+ 'type' => 'Script',
+ 'path' => 'admin/views/moderation/threads.php'
+ ),
+ array (
+ 'type' => 'Script',
+ 'path' => 'admin/views/settings/index.php'
+ ),
+ array (
+ 'type' => 'Script',
+ 'path' => 'admin/views/updates/index.php'
+ ),
+ array (
+ 'type' => 'Script',
+ 'path' => 'admin/views/view-setup.php'
+ ),
+ array (
+ 'type' => 'Script',
+ 'path' => 'api/backend/count-link-ajax.php'
+ ),
+ array (
+ 'type' => 'Script',
+ 'path' => 'api/backend/latest-ajax.php'
+ ),
+ array (
+ 'type' => 'Script',
+ 'path' => 'api/count-link.php'
+ ),
+ array (
+ 'type' => 'Script',
+ 'path' => 'api/json.php'
+ ),
+ array (
+ 'type' => 'Script',
+ 'path' => 'api/latest.php'
+ ),
+ array (
+ 'type' => 'Script',
+ 'path' => 'api/rss.php'
+ ),
+ array (
+ 'type' => 'Class',
+ 'name' => 'Avatars',
+ 'path' => 'backend/classes/avatars.php'
+ ),
+ array (
+ 'type' => 'Class',
+ 'name' => 'CommentFiles',
+ 'path' => 'backend/classes/commentfiles.php'
+ ),
+ array (
+ 'type' => 'Class',
+ 'name' => 'CommentParser',
+ 'path' => 'backend/classes/commentparser.php'
+ ),
+ array (
+ 'type' => 'Class',
+ 'name' => 'CommentsUI',
+ 'path' => 'backend/classes/commentsui.php'
+ ),
+ array (
+ 'type' => 'Class',
+ 'name' => 'Cookies',
+ 'path' => 'backend/classes/cookies.php'
+ ),
+ array (
+ 'type' => 'Class',
+ 'name' => 'Database',
+ 'path' => 'backend/classes/database.php'
+ ),
+ array (
+ 'type' => 'Class',
+ 'name' => 'DataFiles',
+ 'path' => 'backend/classes/datafiles.php'
+ ),
+ array (
+ 'type' => 'Class',
+ 'name' => 'DefaultLogin',
+ 'path' => 'backend/classes/defaultlogin.php'
+ ),
+ array (
+ 'type' => 'Class',
+ 'name' => 'Encryption',
+ 'path' => 'backend/classes/encryption.php'
+ ),
+ array (
+ 'type' => 'Class',
+ 'name' => 'FormUI',
+ 'path' => 'backend/classes/formui.php'
+ ),
+ array (
+ 'type' => 'Class',
+ 'name' => 'HashOver',
+ 'path' => 'backend/classes/hashover.php'
+ ),
+ array (
+ 'type' => 'Class',
+ 'name' => 'HTMLTag',
+ 'path' => 'backend/classes/htmltag.php'
+ ),
+ array (
+ 'type' => 'Class',
+ 'name' => 'JavaScriptBuild',
+ 'path' => 'backend/classes/javascriptbuild.php'
+ ),
+ array (
+ 'type' => 'Class',
+ 'name' => 'JavaScriptMinifier',
+ 'path' => 'backend/classes/javascriptminifier.php'
+ ),
+ array (
+ 'type' => 'Class',
+ 'name' => 'Locale',
+ 'path' => 'backend/classes/locale.php'
+ ),
+ array (
+ 'type' => 'Class',
+ 'name' => 'Login',
+ 'path' => 'backend/classes/login.php'
+ ),
+ array (
+ 'type' => 'Class',
+ 'name' => 'Markdown',
+ 'path' => 'backend/classes/markdown.php'
+ ),
+ array (
+ 'type' => 'Class',
+ 'name' => 'Metadata',
+ 'path' => 'backend/classes/metadata.php'
+ ),
+ array (
+ 'type' => 'Class',
+ 'name' => 'Misc',
+ 'path' => 'backend/classes/misc.php'
+ ),
+ array (
+ 'type' => 'Class',
+ 'name' => 'ParseJSON',
+ 'path' => 'backend/classes/parsejson.php'
+ ),
+ array (
+ 'type' => 'Class',
+ 'name' => 'ParseSQL',
+ 'path' => 'backend/classes/parsesql.php'
+ ),
+ array (
+ 'type' => 'Class',
+ 'name' => 'ParseXML',
+ 'path' => 'backend/classes/parsexml.php'
+ ),
+ array (
+ 'type' => 'Class',
+ 'name' => 'PHPMode',
+ 'path' => 'backend/classes/phpmode.php'
+ ),
+ array (
+ 'type' => 'Class',
+ 'name' => 'PostData',
+ 'path' => 'backend/classes/postdata.php'
+ ),
+ array (
+ 'type' => 'Class',
+ 'name' => 'Settings',
+ 'path' => 'backend/classes/settings.php'
+ ),
+ array (
+ 'type' => 'Class',
+ 'name' => 'Setup',
+ 'path' => 'backend/classes/setup.php'
+ ),
+ array (
+ 'type' => 'Class',
+ 'name' => 'SourceSode',
+ 'path' => 'backend/classes/sourcecode.php'
+ ),
+ array (
+ 'type' => 'Class',
+ 'name' => 'SpamCheck',
+ 'path' => 'backend/classes/spamcheck.php'
+ ),
+ array (
+ 'type' => 'Class',
+ 'name' => 'Statistics',
+ 'path' => 'backend/classes/statistics.php'
+ ),
+ array (
+ 'type' => 'Class',
+ 'name' => 'Templater',
+ 'path' => 'backend/classes/templater.php'
+ ),
+ array (
+ 'type' => 'Class',
+ 'name' => 'Thread',
+ 'path' => 'backend/classes/thread.php'
+ ),
+ array (
+ 'type' => 'Class',
+ 'name' => 'WriteComments',
+ 'path' => 'backend/classes/writecomments.php'
+ ),
+ array (
+ 'type' => 'Script',
+ 'path' => 'backend/locales/da.php'
+ ),
+ array (
+ 'type' => 'Script',
+ 'path' => 'backend/locales/de.php'
+ ),
+ array (
+ 'type' => 'Script',
+ 'path' => 'backend/locales/el.php'
+ ),
+ array (
+ 'type' => 'Script',
+ 'path' => 'backend/locales/en.php'
+ ),
+ array (
+ 'type' => 'Script',
+ 'path' => 'backend/locales/es.php'
+ ),
+ array (
+ 'type' => 'Script',
+ 'path' => 'backend/locales/fa.php'
+ ),
+ array (
+ 'type' => 'Script',
+ 'path' => 'backend/locales/fr.php'
+ ),
+ array (
+ 'type' => 'Script',
+ 'path' => 'backend/locales/jp.php'
+ ),
+ array (
+ 'type' => 'Script',
+ 'path' => 'backend/locales/ko.php'
+ ),array (
+ 'type' => 'Script',
+ 'path' => 'backend/locales/lt.php'
+ ),
+ array (
+ 'type' => 'Script',
+ 'path' => 'backend/locales/nl.php'
+ ),
+ array (
+ 'type' => 'Script',
+ 'path' => 'backend/locales/pl.php'
+ ),
+ array (
+ 'type' => 'Script',
+ 'path' => 'backend/locales/pt-br.php'
+ ),
+ array (
+ 'type' => 'Script',
+ 'path' => 'backend/locales/ro.php'
+ ),
+ array (
+ 'type' => 'Script',
+ 'path' => 'backend/locales/tr.php'
+ ),
+ array (
+ 'type' => 'Script',
+ 'path' => 'backend/locales/zh-cn.php'
+ ),
+ array (
+ 'type' => 'Script',
+ 'path' => 'backend/comments-ajax.php'
+ ),
+ array (
+ 'type' => 'Script',
+ 'path' => 'backend/form-actions.php'
+ ),
+ array (
+ 'type' => 'Script',
+ 'path' => 'backend/javascript-setup.php'
+ ),
+ array (
+ 'type' => 'Script',
+ 'path' => 'backend/json-setup.php'
+ ),
+ array (
+ 'type' => 'Script',
+ 'path' => 'backend/like.php'
+ ),
+ array (
+ 'type' => 'Script',
+ 'path' => 'backend/load-comments.php'
+ ),
+ array (
+ 'type' => 'Script',
+ 'path' => 'backend/nocache-headers.php'
+ ),
+ array (
+ 'type' => 'Script',
+ 'path' => 'backend/php-setup.php'
+ ),
+ array (
+ 'type' => 'Script',
+ 'path' => 'backend/source-viewer.php'
+ ),
+ array (
+ 'type' => 'Script',
+ 'path' => 'backend/standard-setup.php'
+ ),
+ array (
+ 'type' => 'Script',
+ 'path' => 'comments.php'
+ )
+ );
+
+ // Checks if a given file is a HashOver file
+ protected function isHashOverFile ($path)
+ {
+ // Run through HashOver files array
+ foreach ($this->files as $file) {
+ // Check if the given file path matches the current file
+ if ($path === $file['path']) {
+ // If so, return true
+ return true;
+ }
+ }
+
+ // Otherwise return false
+ return false;
+ }
+
+ // Sets content type header based on user request
+ protected function setContentType ($file, $type)
+ {
+
+ // Switch between return types
+ switch ($type) {
+ // Display as plain text
+ case 'text': {
+ // Set content type header to plain text
+ header ('Content-type: text/plain; charset=UTF-8');
+ break;
+ }
+
+ // Display as HTML
+ case 'html': {
+ // Set content type header to HTML
+ header ('Content-type: text/html; charset=UTF-8');
+ break;
+ }
+
+ // Download source code
+ case 'download': {
+ // File name
+ $file_name = basename ($file);
+
+ // Set headers to trigger file download
+ header ('Content-type: application/octet-stream');
+ header ('Content-Disposition: attachment; filename="' . $file_name . '"');
+ header ('Content-Length: ' . filesize ($file));
+
+ break;
+ }
+
+ // Default to displaying an error
+ default: {
+ // Set content type header to plain text
+ header ('Content-type: text/plain; charset=UTF-8');
+ break;
+ }
+ }
+ }
+
+ // Format source code based on content type
+ protected function format ($name, $source, $type)
+ {
+ // Switch between return types
+ switch ($type) {
+ // Display as HTML
+ case 'html': {
+ // Conform HTML highlighting to coding standard
+ $source = str_replace ("\t", '/*_TAB_*/', $source);
+ $source = highlight_string ($source, true);
+ $source = str_replace ('/*_TAB_*/', "&#9;", $source);
+ $source = str_replace ('&nbsp;', ' ', $source);
+ $source = str_replace ("\n", '', $source);
+
+ // Return highlighted source code
+ return implode (PHP_EOL, array (
+ '<!DOCTYPE html>',
+ '<html lang="en" dir="ltr">',
+ "\t" . '<head>',
+ "\t\t" . '<title>' . $name . '</title>',
+ "\t" . '</head>',
+ "\t" . '<body>',
+ "\t\t" . '<pre>' . $source . '</pre>',
+ "\t" . '</body>',
+ '</html>'
+ ));
+ }
+
+ // Default to displaying as plain text
+ default: {
+ return $source;
+ }
+ }
+ }
+
+ // Display source code
+ public function display ($file, $type = 'text')
+ {
+ // Set content type header
+ $this->setContentType ($file, $type);
+
+ // Check if the given file is a known HashOver file
+ if ($this->isHashOverFile ($file) === true) {
+ // If so, load PHP file
+ $source = @file_get_contents ('../' . $file);
+ $name = basename ($file);
+
+ // Check if file read successfully
+ if ($source !== false) {
+ // If so, display formatted source code
+ echo $this->format ($name, $source, $type);
+ } else {
+ // If not, display error
+ echo 'Error! Failed to read HashOver file!';
+ }
+ } else {
+ // If not, display error
+ echo 'Error! Not a known HashOver file!';
+ }
+ }
+}
diff --git a/bootstrap/comments/backend/classes/spamcheck.php b/bootstrap/comments/backend/classes/spamcheck.php
new file mode 100644
index 0000000..0cad687
--- /dev/null
+++ b/bootstrap/comments/backend/classes/spamcheck.php
@@ -0,0 +1,168 @@
+<?php namespace HashOver;
+
+// Copyright (C) 2010-2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// HashOver is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// HashOver is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with HashOver. If not, see <http://www.gnu.org/licenses/>.
+
+
+class SpamCheck
+{
+ public $blocklist;
+ public $database;
+ public $error;
+
+ public function __construct (Setup $setup)
+ {
+ // JSON IP address blocklist file
+ $this->blocklist = $setup->getAbsolutePath ('config/blocklist.json');
+
+ // CSV spam database file
+ $this->database = $setup->getAbsolutePath ('spam-database.csv');
+ }
+
+ // Compare array of IP addresses to user's IP
+ public function checkIPs ($ips = array ())
+ {
+ // Do nothing if input isn't an array
+ if (!is_array ($ips)) {
+ return false;
+ }
+
+ // Run through each IP
+ for ($ip = count ($ips) - 1; $ip >= 0; $ip--) {
+ // Return true if they match
+ if ($ips[$ip] === $_SERVER['REMOTE_ADDR']) {
+ return true;
+ }
+ }
+
+ // Otherwise, return false
+ return false;
+ }
+
+ // Return false if visitor's IP address is in block list file
+ public function checkList ()
+ {
+ // Do nothing if blocklist file doesn't exist
+ if (!file_exists ($this->blocklist)) {
+ return false;
+ }
+
+ // Read blocklist file
+ $data = @file_get_contents ($this->blocklist);
+
+ // Parse blocklist file
+ $blocklist = @json_decode ($data, true);
+
+ // Check user's IP address against blocklist
+ if ($blocklist !== null) {
+ return $this->checkIPs ($blocklist);
+ }
+
+ return false;
+ }
+
+ // Get Stop Forum Spam remote spam database JSON
+ public function getStopForumSpamJSON ()
+ {
+ // Stop Forum Spam API URL
+ $url = 'http://www.stopforumspam.com/api?ip=' . $_SERVER['REMOTE_ADDR'] . '&f=json';
+
+ // Check if we have cURL
+ if (function_exists ('curl_init')) {
+ // If so, initiate cURL
+ $ch = curl_init ();
+ $options = array (CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => true);
+ curl_setopt_array ($ch, $options);
+
+ // Fetch response from Stop Forum Spam database check
+ $output = curl_exec ($ch);
+
+ // Close cURL
+ curl_close ($ch);
+ } else {
+ // If not, open file via URL if allowed
+ if (ini_get ('allow_url_fopen')) {
+ $output = @file_get_contents ($url);
+ }
+ }
+
+ // Parse response as JSON
+ if (!empty ($output)) {
+ $json = @json_decode ($output, true);
+
+ if ($json !== null) {
+ return $json;
+ }
+ }
+
+ return array ();
+ }
+
+ // Stop Forum Spam remote spam database check
+ public function remote ()
+ {
+ // Get Stop Forum Spam JSON
+ $spam_database = $this->getStopForumSpamJSON ();
+
+ // Set error message and return true if spam check failed
+ if (!isset ($spam_database['success'])) {
+ $this->error = 'Spam check failed!';
+ return true;
+ }
+
+ // Set error message and return true if response was invalid
+ if (!isset ($spam_database['ip']['appears'])) {
+ $this->error = 'Spam check received invalid JSON!';
+ return true;
+ }
+
+ // If spam check was successful
+ if ($spam_database['success'] === 1) {
+ // Return true if user's IP appears in the database
+ if ($spam_database['ip']['appears'] === 1) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ // Local CSV spam database check
+ public function local ()
+ {
+ // Do nothing if CSV spam database file doesn't exist
+ if (!file_exists ($this->database)) {
+ return false;
+ }
+
+ // Read CSV spam database file
+ $data = @file_get_contents ($this->database);
+
+ // Check if file read successfully
+ if ($data !== false) {
+ // If so, convert CSV database into array
+ $ips = explode (',', $data);
+
+ // And check user's IP address against CSV database
+ return $this->checkIPs ($ips);
+ } else {
+ // If not, set error message
+ $this->error = 'No local database found!';
+ }
+
+ return false;
+ }
+}
diff --git a/bootstrap/comments/backend/classes/statistics.php b/bootstrap/comments/backend/classes/statistics.php
new file mode 100644
index 0000000..e08d0de
--- /dev/null
+++ b/bootstrap/comments/backend/classes/statistics.php
@@ -0,0 +1,91 @@
+<?php namespace HashOver;
+
+// Copyright (C) 2010-2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// HashOver is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// HashOver is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with HashOver. If not, see <http://www.gnu.org/licenses/>.
+
+
+class Statistics
+{
+ public $mode;
+ public $executionStart;
+ public $executionEnd;
+ public $executionMicroTime;
+ public $executionTime;
+ public $scriptMemory;
+ public $systemMemory;
+
+ public function __construct ($mode = 'php')
+ {
+ $this->mode = $mode;
+ }
+
+ // Script execution starting time
+ public function executionStart ()
+ {
+ // Start time in seconds
+ $this->executionStart = microtime (true);
+ }
+
+ // Script execution ending time
+ public function executionEnd ()
+ {
+ // End time in seconds
+ $this->executionEnd = microtime (true);
+
+ // Difference between start time and end time in seconds
+ $this->executionMicroTime = $this->executionEnd - $this->executionStart;
+
+ // Unit to divided memory bytes by (1,024² for mibibytes)
+ $division_unit = pow (1024, 2);
+
+ // Memory the script consumed divided by division unit
+ $script_memory = round (memory_get_peak_usage () / $division_unit, 2);
+ $this->scriptMemory = $script_memory . ' MiB';
+
+ // Memory the system consumed divided by division unit
+ $system_memory = round (memory_get_peak_usage (true) / $division_unit, 2);
+ $this->systemMemory = $system_memory . ' MiB';
+
+ // Display execution time in millisecond(s)
+ if ($this->executionMicroTime < 1) {
+ $this->executionTime = round ($this->executionMicroTime * 1000, 5) . ' ms';
+ } else {
+ // Display execution time in seconds
+ $this->executionTime = round ($this->executionMicroTime, 5) . ' Second';
+
+ // Add plural to any execution time other than one
+ if ($this->executionMicroTime !== 1) {
+ $this->executionTime .= 's';
+ }
+ }
+
+ // Statistics inner-comment
+ $statistics = PHP_EOL . PHP_EOL;
+ $statistics .= "\t" . 'HashOver Statistics' . PHP_EOL . PHP_EOL;
+ $statistics .= "\t" . 'Execution Time : ' . $this->executionTime . PHP_EOL;
+ $statistics .= "\t" . 'Script Memory Peak : ' . $this->scriptMemory . PHP_EOL;
+ $statistics .= "\t" . 'System Memory Peak : ' . $this->systemMemory;
+ $statistics .= PHP_EOL . PHP_EOL;
+
+ // Return statistics as JavaScript comment
+ if ($this->mode !== 'php') {
+ return PHP_EOL . '/*' . $statistics . '*/';
+ }
+
+ // Return statistics as HTML comment by default
+ return PHP_EOL . '<!--' . $statistics . '-->';
+ }
+}
diff --git a/bootstrap/comments/backend/classes/templater.php b/bootstrap/comments/backend/classes/templater.php
new file mode 100644
index 0000000..7b66645
--- /dev/null
+++ b/bootstrap/comments/backend/classes/templater.php
@@ -0,0 +1,124 @@
+<?php namespace HashOver;
+
+// Copyright (C) 2015-2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// HashOver is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// HashOver is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with HashOver. If not, see <http://www.gnu.org/licenses/>.
+
+
+class Templater
+{
+ public $mode;
+ public $setup;
+ public $template;
+
+ protected $curlyRegex = '/\{([a-z]+):([a-z_-]+)\}/i';
+ protected $newlineSearch = array ("\r\n", "\r", "\n");
+ protected $newlineReplace = array ("\n", "\n", PHP_EOL);
+
+ public function __construct ($mode = 'php', Setup $setup)
+ {
+ $this->mode = $mode;
+ $this->setup = $setup;
+ }
+
+ public function loadFile ($file)
+ {
+ // Attempt to read template HTML file
+ $theme_html = @file_get_contents ($file);
+
+ // Check if template file read successfully
+ if ($theme_html !== false) {
+ // If so, return trimmed HTML template
+ return trim ($theme_html);
+ } else {
+ // If not, throw exception
+ throw new \Exception ('Failed to load template file.');
+ }
+ }
+
+ protected function curlyVariable ($key)
+ {
+ return '{{' . $key . '}}';
+ }
+
+ protected function fromTemplate ($key)
+ {
+ if ($this->mode !== 'php') {
+ return $this->curlyVariable ($key);
+ }
+
+ if (!empty ($this->template[$key])) {
+ return $this->template[$key];
+ }
+
+ return '';
+ }
+
+ protected function placeholder ($id)
+ {
+ $span_id = 'hashover-placeholder-' . $id;
+ $span_id .= '-' . $this->fromTemplate ('permalink');
+
+ $placeholder = new HTMLTag ('span', array (
+ 'id' => $span_id
+ ), false);
+
+ if (!empty ($this->template[$id])) {
+ $placeholder->innerHTML ($this->template[$id]);
+ }
+
+ return $placeholder->asHTML ();
+ }
+
+ protected function parser ($var)
+ {
+ if (empty ($var[1]) or empty ($var[2])) {
+ return '';
+ }
+
+ switch ($var[1]) {
+ case 'hashover': {
+ return $this->fromTemplate ($var[2]);
+ }
+
+ case 'placeholder': {
+ return $this->placeholder ($var[2]);
+ }
+ }
+ }
+
+ public function parseTemplate ($file, array $template = array ())
+ {
+ $this->template = $template;
+ $data = $this->loadFile ($file);
+ $template = preg_replace_callback ($this->curlyRegex, 'self::parser', $data);
+ $template = str_replace ($this->newlineSearch, $this->newlineReplace, $template);
+ $template = preg_replace ('/^[\s\n]+$/m', '', $template);
+
+ return $template;
+ }
+
+ public function parseTheme ($file, array $template = array ())
+ {
+ // Get the file path for the configured theme
+ $path = $this->setup->getThemePath ($file, false);
+ $path = $this->setup->getAbsolutePath ($path);
+
+ // Parse the theme HTML as template
+ $theme = $this->parseTemplate ($path, $template);
+
+ return $theme;
+ }
+}
diff --git a/bootstrap/comments/backend/classes/thread.php b/bootstrap/comments/backend/classes/thread.php
new file mode 100644
index 0000000..eed3024
--- /dev/null
+++ b/bootstrap/comments/backend/classes/thread.php
@@ -0,0 +1,228 @@
+<?php namespace HashOver;
+
+// Copyright (C) 2010-2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// HashOver is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// HashOver is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with HashOver. If not, see <http://www.gnu.org/licenses/>.
+
+
+class Thread
+{
+ public $setup;
+ public $data;
+ public $commentList = array ();
+ public $threadCount = array ();
+ public $primaryCount = 1;
+ public $totalCount = 1;
+ public $primaryDeletedCount = 0;
+ public $collapsedDeletedCount = 0;
+ public $totalDeletedCount = 0;
+
+ public function __construct (Setup $setup)
+ {
+ $this->setup = $setup;
+
+ // Instantiate necessary class data format class
+ $data_class = 'HashOver\\Parse' . strtoupper ($setup->dataFormat);
+ $this->data = new $data_class ($setup);
+ }
+
+ // Queries a list of comments
+ public function queryComments ()
+ {
+ // Query a list of comments
+ $comment_list = $this->data->query ();
+
+ // Organize comments if comments could be queried
+ if ($comment_list !== false) {
+ $this->commentList = $comment_list;
+ $this->organizeComments ();
+ }
+ }
+
+ // Counts a comment
+ public function countComment ($comment)
+ {
+ // Count replies
+ if (strpos ($comment, '-') !== false) {
+ $file_parts = explode ('-', $comment);
+ $thread = basename ($comment, '-' . end ($file_parts));
+
+ if (isset ($this->threadCount[$thread])) {
+ $this->threadCount[$thread]++;
+ } else {
+ $this->threadCount[$thread] = 1;
+ }
+ } else {
+ // Count top level comments
+ $this->primaryCount++;
+ }
+
+ // Count replies
+ if (isset ($this->threadCount[$comment])) {
+ $this->threadCount[$comment]++;
+ } else {
+ $this->threadCount[$comment] = 1;
+ }
+
+ // Count all other comments
+ $this->totalCount++;
+ }
+
+ // Explode a string, cast substrings to integers
+ protected function intExplode ($delimiter, $string)
+ {
+ $parts = explode ($delimiter, $string);
+ $ints = array ();
+
+ for ($i = 0, $il = count ($parts); $i < $il; $i++) {
+ $ints[] = (int)($parts[$i]);
+ }
+
+ return $ints;
+ }
+
+ // Counts a deleted comment
+ public function countDeleted ($comment)
+ {
+ // Count deleted replies
+ if (strpos ($comment, '-') === false) {
+ $this->primaryDeletedCount++;
+ }
+
+ // Get count from comment key
+ $comment_parts = $this->intExplode ('-', $comment);
+ $comment_count = array_sum ($comment_parts);
+
+ // Count collapsed deleted comments
+ if ($comment_count > $this->setup->collapseLimit) {
+ $this->collapsedDeletedCount++;
+ }
+
+ // Count all other deleted comments
+ $this->totalDeletedCount++;
+ }
+
+ // Check for missing comments
+ protected function findMissingComments ($key)
+ {
+ // Get integers from key
+ $key_parts = $this->intExplode ('-', $key);
+
+ // Initial comment tree
+ $comment_tree = '';
+
+ // Run through each key
+ foreach ($key_parts as $key => $reply) {
+ // Check for comments counting backward from the current
+ for ($i = 1; $i <= $reply; $i++) {
+ // Current level to check for
+ if ($key > 0) {
+ $current = $comment_tree . '-' . $i;
+ } else {
+ $current = $i;
+ }
+
+ // Check for the comment in the list
+ if (!isset ($this->commentList[$current])) {
+ // If it doesn't exist, mark comment as missing
+ $this->commentList[$current] = 'missing';
+
+ // Count the missing comment
+ $this->countComment ($current);
+ $this->countDeleted ($current);
+ }
+ }
+
+ // Add current reply number to tree
+ if ($key > 0) {
+ $comment_tree .= '-' . $reply;
+ } else {
+ $comment_tree = $reply;
+ }
+ }
+ }
+
+ // Organize comments
+ public function organizeComments ()
+ {
+ foreach ($this->commentList as $key) {
+ // Check for missing comments
+ $this->findMissingComments ($key);
+
+ // Count comment
+ $this->countComment ($key);
+ }
+
+ // Sort comments by their keys alphabetically in ascending order
+ uksort ($this->commentList, 'strnatcasecmp');
+ }
+
+ // Read comments
+ public function read ($start = 0, $end = null)
+ {
+ $comments = array ();
+ $limit_count = 0;
+ $allowed_count = 0;
+
+ foreach ($this->commentList as $i => $key) {
+ // Skip until starting point is reached
+ if ($limit_count < $start) {
+ $limit_count++;
+ continue;
+ }
+
+ // Stop at end point
+ if ($end !== null and $allowed_count >= $end) {
+ break;
+ }
+
+ // Skip deleted comments
+ if ($key === 'missing') {
+ $comments[$i]['status'] = 'missing';
+ continue;
+ }
+
+ // Read comment
+ $comment = $this->data->read ($key);
+
+ // See if it read successfully
+ if ($comment !== false) {
+ // If so, add the comment to output
+ $comments[$i] = $comment;
+
+ // And count deleted status comments
+ if (!empty ($comment['status'])
+ and $comment['status'] === 'deleted')
+ {
+ $this->countDeleted ($key);
+ }
+ } else {
+ // If not, set comment status as a read error
+ $comments[$i]['status'] = 'read-error';
+ continue;
+ }
+
+ $allowed_count++;
+ }
+
+ return $comments;
+ }
+
+ // Queries a list of comment threads
+ public function queryThreads ()
+ {
+ return $this->data->queryThreads ();
+ }
+}
diff --git a/bootstrap/comments/backend/classes/writecomments.php b/bootstrap/comments/backend/classes/writecomments.php
new file mode 100644
index 0000000..c391faf
--- /dev/null
+++ b/bootstrap/comments/backend/classes/writecomments.php
@@ -0,0 +1,871 @@
+<?php namespace HashOver;
+
+// Copyright (C) 2010-2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// HashOver is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// HashOver is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with HashOver. If not, see <http://www.gnu.org/licenses/>.
+
+
+class WriteComments extends PostData
+{
+ protected $setup;
+ protected $encryption;
+ protected $mode;
+ protected $thread;
+ protected $locale;
+ protected $cookies;
+ protected $login;
+ protected $misc;
+ protected $spamCheck;
+ protected $metadata;
+ protected $headers;
+ protected $userHeaders;
+ protected $kickbackURL;
+ protected $name = '';
+ protected $password = '';
+ protected $loginHash = '';
+ protected $email = '';
+ protected $website = '';
+ protected $commentData = array ();
+ protected $urls = array ();
+
+ // Fake inputs used as spam trap fields
+ protected $trapFields = array (
+ 'summary',
+ 'age',
+ 'lastname',
+ 'address',
+ 'zip'
+ );
+
+ // Characters to search for and replace with in comments
+ protected $dataSearch = array (
+ '\\',
+ '"',
+ '<',
+ '>',
+ "\r\n",
+ "\r",
+ "\n",
+ ' '
+ );
+
+ // Character replacements
+ protected $dataReplace = array (
+ '&#92;',
+ '&quot;',
+ '&lt;',
+ '&gt;',
+ PHP_EOL,
+ PHP_EOL,
+ PHP_EOL,
+ '&nbsp; '
+ );
+
+ // HTML tags to allow in comments
+ protected $htmlTagSearch = array (
+ 'b',
+ 'big',
+ 'blockquote',
+ 'code',
+ 'em',
+ 'i',
+ 'li',
+ 'ol',
+ 'pre',
+ 's',
+ 'small',
+ 'strong',
+ 'sub',
+ 'sup',
+ 'u',
+ 'ul'
+ );
+
+ // HTML tags to automatically close
+ public $closeTags = array (
+ 'b',
+ 'big',
+ 'blockquote',
+ 'em',
+ 'i',
+ 'li',
+ 'ol',
+ 'pre',
+ 's',
+ 'small',
+ 'strong',
+ 'sub',
+ 'sup',
+ 'u',
+ 'ul'
+ );
+
+ // Unprotected fields to update when editing a comment
+ protected $editableFields = array (
+ 'body',
+ 'name',
+ 'notifications',
+ 'website'
+ );
+
+ // Password protected fields
+ protected $protectedFields = array (
+ 'password',
+ 'login_id',
+ 'email',
+ 'encryption',
+ 'email_hash'
+ );
+
+ // Possible comment status options
+ protected $statuses = array (
+ 'approved',
+ 'pending',
+ 'deleted'
+ );
+
+ public function __construct (Setup $setup, Thread $thread)
+ {
+ parent::__construct ();
+
+ $this->setup = $setup;
+ $this->encryption = $setup->encryption;
+ $this->mode = $setup->usage['mode'];
+ $this->thread = $thread;
+ $this->locale = new Locale ($setup);
+ $this->cookies = new Cookies ($setup);
+ $this->login = new Login ($setup);
+ $this->misc = new Misc ($this->mode);
+ $this->spamCheck = new SpamCheck ($setup);
+ $this->metadata = new Metadata ($setup, $thread);
+
+ // Setup initial login data
+ $this->setupLogin ();
+
+ // Default email headers
+ $this->setHeaders ($setup->noreplyEmail);
+
+ // Check if we have an HTTP referer
+ if (!empty ($_SERVER['HTTP_REFERER'])) {
+ // If so, use it as the kickback URL
+ $this->kickbackURL = $_SERVER['HTTP_REFERER'];
+ } else {
+ // Check if posting from remote domain
+ if ($this->remoteAccess === true) {
+ // If so, use absolute path
+ $this->kickbackURL = $setup->pageURL;
+ } else {
+ // If not, use relative path
+ $this->kickbackURL = $setup->filePath;
+ }
+
+ // Add URL queries to kickback URL
+ if (!empty ($setup->URLQueries)) {
+ $this->kickbackURL .= '?' . $setup->URLQueries;
+ }
+ }
+ }
+
+ // Encodes HTML entities
+ protected function encodeHTML ($value)
+ {
+ return htmlentities ($value, ENT_COMPAT, 'UTF-8', false);
+ }
+
+ // Set mail headers
+ protected function setHeaders ($email, $user = true)
+ {
+ $this->headers = 'Content-Type: text/plain; charset=UTF-8' . "\r\n";
+ $this->headers .= 'From: ' . $email . "\r\n";
+ $this->headers .= 'Reply-To: ' . $email;
+
+ // Set commenter's headers to new value as well
+ if ($user === true) {
+ $this->userHeaders = $this->headers;
+ }
+ }
+
+ protected function kickback ($text = '', $error = false, $anchor = 'comments')
+ {
+ $message_type = ($error) ? 'error' : 'message';
+
+ // Return error as JSON if request is AJAX
+ if ($this->viaAJAX === true) {
+ if (!empty ($text)) {
+ echo $this->misc->jsonData (array (
+ 'message' => $text,
+ 'type' => $message_type
+ ));
+ }
+
+ return;
+ }
+
+ // Set cookie to specified message or error
+ if (!empty ($text)) {
+ $this->cookies->set ($message_type, $text);
+ }
+
+ // Set header to redirect user to previous page
+ header ('Location: ' . $this->kickbackURL . '#' . $anchor);
+ }
+
+ // Confirm that attempted actions are to existing comments
+ protected function verifyFile ($file)
+ {
+ // Attempt to get file
+ $comment_file = $this->setup->getRequest ($file);
+
+ // Check if file is set
+ if ($comment_file !== false) {
+ // Cast file to string
+ $comment_file =(string) ($comment_file);
+
+ // Return true if POST file is in comment list
+ if (in_array ($comment_file, $this->thread->commentList, true)) {
+ return $comment_file;
+ }
+
+ // Set cookies to indicate failure
+ if ($this->viaAJAX !== true) {
+ $this->cookies->setFailedOn ('comment', $this->replyTo, false);
+ }
+ }
+
+ // Throw exception as error message
+ throw new \Exception ($this->locale->text['comment-needed']);
+ }
+
+ protected function checkForSpam ()
+ {
+ // Check trap fields
+ foreach ($this->trapFields as $name) {
+ if ($this->setup->getRequest ($name)) {
+ // Block for filing trap fields
+ throw new \Exception ('You are blocked!');
+ }
+ }
+
+ // Check user's IP address against local blocklist
+ if ($this->spamCheck->checkList () === true) {
+ throw new \Exception ('You are blocked!');
+ }
+
+ // Whether to check for spam in current mode
+ if ($this->setup->spamCheckModes === 'both'
+ or $this->setup->spamCheckModes === $this->mode)
+ {
+ // Check user's IP address against local or remote database
+ if ($this->spamCheck->{$this->setup->spamDatabase}() === true) {
+ throw new \Exception ('You are blocked!');
+ }
+
+ // Throw any error message as exception
+ if (!empty ($this->spamCheck->error)) {
+ throw new \Exception ($this->spamCheck->error);
+ }
+ }
+
+ return true;
+ }
+
+ // Set cookies
+ public function login ($kickback = true)
+ {
+ try {
+ // Log the user in
+ if ($this->setup->allowsLogin !== false) {
+ $this->login->setLogin ();
+ }
+
+ // Kick visitor back if told to
+ if ($kickback !== false) {
+ $this->kickback ($this->locale->text['logged-in']);
+ }
+
+ } catch (\Exception $error) {
+ // Kick visitor back with exception if told to
+ if ($kickback !== false) {
+ $this->kickback ($error->getMessage (), true);
+ return true;
+ }
+
+ // Otherwise, throw exception as-is
+ throw $error;
+ }
+
+ return true;
+ }
+
+ // Expire cookies
+ public function logout ()
+ {
+ // Log the user out
+ $this->login->clearLogin ();
+
+ // Kick visitor back
+ $this->kickback ($this->locale->text['logged-out']);
+
+ return true;
+ }
+
+ // Setup necessary login data
+ protected function setupLogin ()
+ {
+ $this->name = $this->encodeHTML ($this->login->name);
+ $this->password = $this->encodeHTML ($this->login->password);
+ $this->loginHash = $this->encodeHTML ($this->login->loginHash);
+ $this->email = $this->encodeHTML ($this->login->email);
+ $this->website = $this->encodeHTML ($this->login->website);
+ }
+
+ // User comment authentication
+ protected function commentAuthentication ()
+ {
+ // Verify file exists
+ $file = $this->verifyFile ('file');
+
+ // Read original comment
+ $comment = $this->thread->data->read ($file);
+
+ // Authentication data
+ $auth = array (
+ // Assume no authorization by default
+ 'authorized' => false,
+ 'user-owned' => false,
+
+ // Original comment
+ 'comment' => $comment
+ );
+
+ // Return authorization data if we fail to get comment
+ if ($comment === false) {
+ return $auth;
+ }
+
+ // Check if we have both required passwords
+ if (!empty ($this->postData['password']) and !empty ($comment['password'])) {
+ // If so, get the user input password
+ $user_password = $this->encodeHTML ($this->postData['password']);
+
+ // Get the comment password
+ $comment_password = $comment['password'];
+
+ // Attempt to compare the two passwords
+ if ($this->encryption->verifyHash ($user_password, $comment_password) === true) {
+ $auth['user-owned'] = true;
+ $auth['authorized'] = true;
+ }
+ }
+
+ // Admin is always authorized after strict verification
+ if ($this->setup->verifyAdmin ($this->password) === true) {
+ $auth['authorized'] = true;
+ }
+
+ return $auth;
+ }
+
+ // Delete comment
+ public function deleteComment ()
+ {
+ try {
+ // Authenticate user password
+ $auth = $this->commentAuthentication ();
+
+ // Check if user is authorized
+ if ($auth['authorized'] === true) {
+ // Strict verification of an admin login
+ $user_is_admin = $this->setup->verifyAdmin ($this->password);
+
+ // Unlink comment file indicator
+ $user_deletions_unlink = ($this->setup->userDeletionsUnlink === true);
+ $unlink_comment = ($user_deletions_unlink or $user_is_admin);
+
+ // If so, delete the comment file
+ if ($this->thread->data->delete ($this->file, $unlink_comment)) {
+ // Remove comment from latest comments metadata
+ $this->metadata->removeFromLatest ($this->file);
+
+ // And kick visitor back with comment deletion message
+ $this->kickback ($this->locale->text['comment-deleted']);
+
+ return true;
+ }
+ }
+
+ // Otherwise sleep for 5 seconds
+ sleep (5);
+
+ // Then kick visitor back with comment posting error
+ $this->kickback ($this->locale->text['post-fail'], true);
+
+ } catch (\Exception $error) {
+ // On exception kick visitor back with error
+ $this->kickback ($error->getMessage (), true);
+ }
+
+ return false;
+ }
+
+ // Closes all allowed HTML tags
+ public function tagCloser ($tags, $html)
+ {
+ for ($tc = 0, $tcl = count ($tags); $tc < $tcl; $tc++) {
+ // Count opening and closing tags
+ $open_tags = mb_substr_count ($html, '<' . $tags[$tc] . '>');
+ $close_tags = mb_substr_count ($html, '</' . $tags[$tc] . '>');
+
+ // Check if opening and closing tags aren't equal
+ if ($open_tags !== $close_tags) {
+ // Add closing tags to end of comment
+ while ($open_tags > $close_tags) {
+ $html .= '</' . $tags[$tc] . '>';
+ $close_tags++;
+ }
+
+ // Remove closing tags for unopened tags
+ while ($close_tags > $open_tags) {
+ $html = preg_replace ('/<\/' . $tags[$tc] . '>/iS', '', $html, 1);
+ $close_tags--;
+ }
+ }
+ }
+
+ return $html;
+ }
+
+ // Extract URLs for later injection
+ protected function urlExtractor ($groups)
+ {
+ $link_number = count ($this->urls);
+ $this->urls[] = $groups[1];
+
+ return 'URL[' . $link_number . ']';
+ }
+
+ // Escape all HTML tags excluding allowed tags
+ public function htmlSelectiveEscape ($code)
+ {
+ // Escape all HTML tags
+ $code = str_ireplace ($this->dataSearch, $this->dataReplace, $code);
+
+ // Unescape allowed HTML tags
+ foreach ($this->htmlTagSearch as $tag) {
+ $escaped_tags = array ('&lt;' . $tag . '&gt;', '&lt;/' . $tag . '&gt;');
+ $text_tags = array ('<' . $tag . '>', '</' . $tag . '>');
+ $code = str_ireplace ($escaped_tags, $text_tags, $code);
+ }
+
+ return $code;
+ }
+
+ // Escapes HTML inside of <code> tags and markdown code blocks
+ protected function codeEscaper ($groups)
+ {
+ return $groups[1] . htmlspecialchars ($groups[2], null, null, false) . $groups[3];
+ }
+
+ // Setup and test for necessary comment data
+ protected function setupCommentData ($editing = false)
+ {
+ // Default status
+ $new_status = 'approved';
+
+ // Check if required fields have values
+ $this->login->validateFields ();
+
+ // Post fails when comment is empty
+ if (empty ($this->postData['comment'])) {
+ // Set cookies to indicate failure
+ if ($this->viaAJAX !== true) {
+ $this->cookies->setFailedOn ('comment', $this->replyTo);
+ }
+
+ // Set reply cookie
+ if (!empty ($this->replyTo)) {
+ // Throw exception about reply requirement
+ throw new \Exception ($this->locale->text['reply-needed']);
+ }
+
+ // Throw exception about comment requirement
+ throw new \Exception ($this->locale->text['comment-needed']);
+ }
+
+ // Strictly verify if the user is logged in as admin
+ if ($this->setup->verifyAdmin ($this->password) === true) {
+ // If so, check if status is set in POST data is set
+ if (!empty ($this->postData['status'])) {
+ // If so, check if status is allowed
+ if (in_array ($this->postData['status'], $this->statuses, true)) {
+ // If so, use it
+ $this->commentData['status'] = $this->postData['status'];
+ }
+ }
+ } else {
+ // Check if setup is for a comment edit
+ if ($editing === true) {
+ // If so, set status to "pending" if moderation of user edits is enabled
+ if (($this->setup->usesModeration and $this->setup->pendsUserEdits) === true) {
+ $this->commentData['status'] = 'pending';
+ }
+ } else {
+ // If not, set status to "pending" if moderation is enabled
+ if ($this->setup->usesModeration === true) {
+ $this->commentData['status'] = 'pending';
+ }
+ }
+ }
+
+ // Check if setup is for a comment edit
+ if ($editing === true) {
+ // If so, mimic normal user login
+ $this->login->prepareCredentials ();
+ $this->login->updateCredentials ();
+ } else {
+ // If not, setup initial login information
+ if ($this->login->userIsLoggedIn !== true) {
+ $this->login->setCredentials ();
+ }
+ }
+
+ // Setup login data
+ $this->setupLogin ();
+
+ // Set mail headers to user's e-mail address
+ if (!empty ($this->email)) {
+ $this->setHeaders ($this->email, false);
+ }
+
+ // Trim leading and trailing white space
+ $clean_code = $this->postData['comment'];
+
+ // Extract URLs from comment
+ $clean_code = preg_replace_callback ('/((http|https|ftp):\/\/[a-z0-9-@:;%_\+.~#?&\/=]+)/i', 'self::urlExtractor', $clean_code);
+
+ // Escape all HTML tags excluding allowed tags
+ $clean_code = $this->htmlSelectiveEscape ($clean_code);
+
+ // Collapse multiple newlines to three maximum
+ $clean_code = preg_replace ('/' . PHP_EOL . '{3,}/', str_repeat (PHP_EOL, 3), $clean_code);
+
+ // Close <code> tags
+ $clean_code = $this->tagCloser (array ('code'), $clean_code);
+
+ // Escape HTML inside of <code> tags and markdown code blocks
+ $clean_code = preg_replace_callback ('/(<code>)(.*?)(<\/code>)/is', 'self::codeEscaper', $clean_code);
+ $clean_code = preg_replace_callback ('/(```)(.*?)(```)/is', 'self::codeEscaper', $clean_code);
+
+ // Close remaining tags
+ $clean_code = $this->tagCloser ($this->closeTags, $clean_code);
+
+ // Inject original URLs back into comment
+ $clean_code = preg_replace_callback ('/URL\[([0-9]+)\]/', function ($groups) {
+ $url_key = $groups[1];
+ $url = $this->urls[$url_key];
+
+ return $url . ' ';
+ }, $clean_code);
+
+ // Store clean code
+ $this->commentData['body'] = $clean_code;
+
+ // Store posting date
+ $this->commentData['date'] = date (DATE_ISO8601);
+
+ // Check if name is enabled and isn't empty
+ if ($this->setup->fieldOptions['name'] !== false) {
+ // Store name
+ if (!empty ($this->name)) {
+ $this->commentData['name'] = $this->name;
+ }
+ }
+
+ // Store password and login ID if a password is given
+ if ($this->setup->fieldOptions['password'] !== false) {
+ if (!empty ($this->password)) {
+ $this->commentData['password'] = $this->password;
+ }
+ }
+
+ // Store login ID if login hash is non-empty
+ if (!empty ($this->loginHash)) {
+ $this->commentData['login_id'] = $this->loginHash;
+ }
+
+ // Store e-mail if one is given
+ if ($this->setup->fieldOptions['email'] !== false) {
+ if (!empty ($this->email)) {
+ $encryption_keys = $this->encryption->encrypt ($this->email);
+ $this->commentData['email'] = $encryption_keys['encrypted'];
+ $this->commentData['encryption'] = $encryption_keys['keys'];
+ $this->commentData['email_hash'] = md5 (mb_strtolower ($this->email));
+
+ // Set e-mail subscription if one is given
+ $this->commentData['notifications'] = $this->setup->getRequest ('subscribe') ? 'yes' : 'no';
+ }
+ }
+
+ // Store website URL if one is given
+ if ($this->setup->fieldOptions['website'] !== false) {
+ if (!empty ($this->website)) {
+ $this->commentData['website'] = $this->website;
+ }
+ }
+
+ // Store user IP address if setup to and one is given
+ if ($this->setup->storesIpAddress === true) {
+ if (!empty ($_SERVER['REMOTE_ADDR'])) {
+ $this->commentData['ipaddr'] = $this->misc->makeXSSsafe ($_SERVER['REMOTE_ADDR']);
+ }
+ }
+
+ return true;
+ }
+
+ public function editComment ()
+ {
+ try {
+ // Authenticate user password
+ $auth = $this->commentAuthentication ();
+
+ // Check if user is authorized
+ if ($auth['authorized'] === true) {
+ // Login normal user with edited credentials
+ if ($this->login->userIsAdmin === false) {
+ $this->login (false);
+ }
+
+ // Set initial fields for update
+ $update_fields = $this->editableFields;
+
+ // Setup necessary comment data
+ $this->setupCommentData (true);
+
+ // Add status to editable fields if a new status is set
+ if (!empty ($this->commentData['status'])) {
+ $update_fields[] = 'status';
+ }
+
+ // Only set protected fields for update if passwords match
+ if ($auth['user-owned'] === true) {
+ $update_fields = array_merge ($update_fields, $this->protectedFields);
+ }
+
+ // Update login information and comment
+ foreach ($update_fields as $key) {
+ if (!empty ($this->commentData[$key])) {
+ $auth['comment'][$key] = $this->commentData[$key];
+ } else {
+ unset ($auth['comment'][$key]);
+ }
+ }
+
+ // Attempt to write edited comment
+ if ($this->thread->data->save ($this->file, $auth['comment'], true)) {
+ // If successful, check if request is via AJAX
+ if ($this->viaAJAX === true) {
+ // If so, return the comment data
+ return array (
+ 'file' => $this->file,
+ 'comment' => $auth['comment']
+ );
+ }
+
+ // Otherwise kick visitor back to posted comment
+ $this->kickback ('', false, 'hashover-c' . str_replace ('-', 'r', $this->file));
+
+ return true;
+ }
+ }
+
+ // Otherwise sleep for 5 seconds
+ sleep (5);
+
+ // Then kick visitor back with comment posting error
+ $this->kickback ($this->locale->text['post-fail'], true);
+
+ } catch (\Exception $error) {
+ // On exception kick visitor back with error
+ $this->kickback ($error->getMessage (), true);
+ }
+
+ return false;
+ }
+
+ protected function indentedWordwrap ($text)
+ {
+ if (PHP_EOL !== "\r\n") {
+ $text = str_replace (PHP_EOL, "\r\n", $text);
+ }
+
+ $text = wordwrap ($text, 66, "\r\n", true);
+ $paragraphs = explode ("\r\n\r\n", $text);
+ $paragraphs = str_replace ("\r\n", "\r\n ", $paragraphs);
+
+ array_walk ($paragraphs, function (&$paragraph) {
+ $paragraph = ' ' . $paragraph;
+ });
+
+ return implode ("\r\n\r\n", $paragraphs);
+ }
+
+ protected function sendNotification ($from, $comment, $reply = '', $permalink, $email, $header)
+ {
+ $subject = $this->setup->domain . ' - New ';
+ $subject .= !empty ($reply) ? 'Reply' : 'Comment';
+
+ // Message body to original poster
+ $message = 'From ' . $from . ":\r\n\r\n";
+ $message .= $comment . "\r\n\r\n";
+ $message .= 'In reply to:' . "\r\n\r\n" . $reply . "\r\n\r\n" . '----' . "\r\n\r\n";
+ $message .= 'Permalink: ' . $this->setup->pageURL . '#' . $permalink . "\r\n\r\n";
+ $message .= 'Page: ' . $this->setup->pageURL;
+
+ // Send e-mail
+ mail ($email, $subject, $message, $header);
+ }
+
+ protected function writeComment ($comment_file)
+ {
+ // Write comment to file
+ if ($this->thread->data->save ($comment_file, $this->commentData)) {
+ // Add comment to latest comments metadata
+ $this->metadata->addLatestComment ($comment_file);
+
+ // Send notification e-mails
+ $permalink = 'hashover-c' . str_replace ('-', 'r', $comment_file);
+ $from_line = !empty ($this->name) ? $this->name : $this->setup->defaultName;
+ $mail_comment = html_entity_decode (strip_tags ($this->commentData['body']), ENT_COMPAT, 'UTF-8');
+ $mail_comment = $this->indentedWordwrap ($mail_comment);
+ $webmaster_reply = '';
+
+ // Notify commenter of reply
+ if (!empty ($this->replyTo)) {
+ $reply_comment = $this->thread->data->read ($this->replyTo);
+ $reply_body = html_entity_decode (strip_tags ($reply_comment['body']), ENT_COMPAT, 'UTF-8');
+ $reply_body = $this->indentedWordwrap ($reply_body);
+ $reply_name = !empty ($reply_comment['name']) ? $reply_comment['name'] : $this->setup->defaultName;
+ $webmaster_reply = 'In reply to ' . $reply_name . ':' . "\r\n\r\n" . $reply_body . "\r\n\r\n";
+
+ if (!empty ($reply_comment['email']) and !empty ($reply_comment['encryption'])) {
+ $reply_email = $this->encryption->decrypt ($reply_comment['email'], $reply_comment['encryption']);
+
+ if ($reply_email !== $this->email
+ and !empty ($reply_comment['notifications'])
+ and $reply_comment['notifications'] === 'yes')
+ {
+ if ($this->setup->allowsUserReplies === true) {
+ $this->userHeaders = $this->headers;
+
+ // Add user's e-mail address to "From" line
+ if (!empty ($this->email)) {
+ $from_line .= ' <' . $this->email . '>';
+ }
+ }
+
+ // Message body to original poster
+ $reply_message = 'From ' . $from_line . ":\r\n\r\n";
+ $reply_message .= $mail_comment . "\r\n\r\n";
+ $reply_message .= 'In reply to:' . "\r\n\r\n" . $reply_body . "\r\n\r\n" . '----' . "\r\n\r\n";
+ $reply_message .= 'Permalink: ' . $this->setup->pageURL . '#' . $permalink . "\r\n\r\n";
+ $reply_message .= 'Page: ' . $this->setup->pageURL;
+
+ // Send
+ mail ($reply_email, $this->setup->domain . ' - New Reply', $reply_message, $this->userHeaders);
+ }
+ }
+ }
+
+ // Notify webmaster via e-mail
+ if ($this->email !== $this->setup->notificationEmail) {
+ // Add user's e-mail address to "From" line
+ if (!empty ($this->email)) {
+ $from_line .= ' <' . $this->email . '>';
+ }
+
+ $webmaster_message = 'From ' . $from_line . ":\r\n\r\n";
+ $webmaster_message .= $mail_comment . "\r\n\r\n";
+ $webmaster_message .= $webmaster_reply . '----' . "\r\n\r\n";
+ $webmaster_message .= 'Permalink: ' . $this->setup->pageURL . '#' . $permalink . "\r\n\r\n";
+ $webmaster_message .= 'Page: ' . $this->setup->pageURL;
+
+ // Send
+ mail ($this->setup->notificationEmail, 'New Comment', $webmaster_message, $this->headers);
+ }
+
+ // Set/update user login cookie
+ if ($this->setup->usesAutoLogin !== false
+ and $this->login->userIsLoggedIn !== true)
+ {
+ $this->login (false);
+ }
+
+ // Return the comment data on success via AJAX
+ if ($this->viaAJAX === true) {
+ // Increase comment count(s)
+ $this->thread->countComment ($comment_file);
+
+ return array (
+ 'file' => $comment_file,
+ 'comment' => $this->commentData
+ );
+ }
+
+ // Kick visitor back to comment
+ $this->kickback ('', false, $permalink);
+
+ return true;
+ }
+
+ // Kick visitor back with an error on failure
+ $this->kickback ($this->locale->text['post-fail'], true);
+ return false;
+ }
+
+ public function postComment ()
+ {
+ try {
+ // Test for necessary comment data
+ $this->setupCommentData ();
+
+ // Set comment file name
+ if (isset ($this->replyTo)) {
+ // Verify file exists
+ $this->verifyFile ('reply-to');
+
+ // Rename file for reply
+ $comment_file = $this->replyTo . '-' . $this->thread->threadCount[$this->replyTo];
+ } else {
+ $comment_file = $this->thread->primaryCount;
+ }
+
+ // Check if comment is SPAM
+ $this->checkForSpam ();
+
+ // Check if comment thread exists
+ $this->thread->data->checkThread ();
+
+ // Write the comment file
+ return $this->writeComment ($comment_file);
+
+ } catch (\Exception $error) {
+ // On exception kick visitor back with error
+ $this->kickback ($error->getMessage (), true);
+ }
+
+ return false;
+ }
+}
diff --git a/bootstrap/comments/backend/comments-ajax.php b/bootstrap/comments/backend/comments-ajax.php
new file mode 100644
index 0000000..1f987a9
--- /dev/null
+++ b/bootstrap/comments/backend/comments-ajax.php
@@ -0,0 +1,208 @@
+<?php namespace HashOver;
+
+// Copyright (C) 2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// HashOver is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// HashOver is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with HashOver. If not, see <http://www.gnu.org/licenses/>.
+
+
+// Check if request is for JSONP
+if (isset ($_GET['jsonp'])) {
+ // If so, setup HashOver for JavaScript
+ require ('javascript-setup.php');
+} else {
+ // If not, setup HashOver for JSON
+ require ('json-setup.php');
+}
+
+try {
+ // Instantiate HashOver class
+ $hashover = new \HashOver ('json');
+ $hashover->setup->setPageURL ('request');
+ $hashover->setup->setPageTitle ('request');
+ $hashover->setup->setThreadName ('request');
+ $hashover->initiate ();
+ $hashover->parsePrimary ();
+ $hashover->parsePopular ();
+ $hashover->finalize ();
+
+ // Page, setup, and comment data array
+ $data = array ();
+
+ // Check if we're preparing HashOver
+ if ($hashover->setup->getRequest ('prepare') !== false) {
+ // Set/update default page metadata
+ $hashover->defaultMetadata ();
+
+ // Add locales to data
+ $data['locale'] = array (
+ 'cancel' => $hashover->locale->text['cancel'],
+ 'date-time' => $hashover->locale->text['date-time'],
+ 'dislike-comment' => $hashover->locale->text['dislike-comment'],
+ 'disliked-comment' => $hashover->locale->text['disliked-comment'],
+ 'disliked' => $hashover->locale->text['disliked'],
+ 'dislike' => $hashover->locale->text['dislike'],
+ 'external-image-tip' => $hashover->locale->text['external-image-tip'],
+ 'field-needed' => $hashover->locale->text['field-needed'],
+ 'like-comment' => $hashover->locale->text['like-comment'],
+ 'liked-comment' => $hashover->locale->text['liked-comment'],
+ 'liked' => $hashover->locale->text['liked'],
+ 'like' => $hashover->locale->text['like'],
+ 'today' => $hashover->locale->text['date-today'],
+ 'unlike' => $hashover->locale->text['unlike'],
+ 'commenter-tip' => $hashover->locale->text['commenter-tip'],
+ 'subscribed-tip' => $hashover->locale->text['subscribed-tip'],
+ 'unsubscribed-tip' => $hashover->locale->text['unsubscribed-tip'],
+ 'replies' => $hashover->locale->text['replies'],
+ 'reply' => $hashover->locale->text['reply'],
+ 'no-email-warning' => $hashover->locale->text['no-email-warning'],
+ 'invalid-email' => $hashover->locale->text['invalid-email'],
+ 'reply-needed' => $hashover->locale->text['reply-needed'],
+ 'comment-needed' => $hashover->locale->text['comment-needed'],
+ 'delete-comment' => $hashover->locale->text['delete-comment'],
+ 'loading' => $hashover->locale->text['loading'],
+ 'click-to-close' => $hashover->locale->text['click-to-close'],
+ 'email' => $hashover->locale->text['email'],
+ 'name' => $hashover->locale->text['name'],
+ 'password' => $hashover->locale->text['password'],
+ 'website' => $hashover->locale->text['website'],
+ 'day-names' => $hashover->locale->text['date-day-names'],
+ 'month-names' => $hashover->locale->text['date-month-names']
+ );
+
+ // Add setup information to data
+ $data['setup'] = array (
+ 'server-eol' => PHP_EOL,
+ 'collapse-limit' => $hashover->setup->collapseLimit,
+ 'default-name' => $hashover->setup->defaultName,
+ 'user-is-logged-in' => $hashover->login->userIsLoggedIn,
+ 'user-is-admin' => $hashover->login->userIsAdmin,
+ 'http-root' => $hashover->setup->httpRoot,
+ 'http-backend' => $hashover->setup->httpBackend,
+ 'allows-dislikes' => $hashover->setup->allowsDislikes,
+ 'allows-likes' => $hashover->setup->allowsLikes,
+ 'time-format' => $hashover->setup->timeFormat,
+ 'image-extensions' => $hashover->setup->imageTypes,
+ 'image-placeholder' => $hashover->setup->getImagePath ('place-holder'),
+ 'stream-mode' => ($hashover->setup->replyMode === 'stream'),
+ 'stream-depth' => $hashover->setup->streamDepth,
+ 'theme-css' => $hashover->setup->getThemePath ('comments.css'),
+ 'device-type' => ($hashover->setup->isMobile === true) ? 'mobile' : 'desktop',
+ 'collapses-interface' => $hashover->setup->collapsesInterface,
+ 'collapses-comments' => $hashover->setup->collapsesComments,
+ 'uses-user-timezone' => $hashover->setup->usesUserTimezone,
+ 'uses-short-dates' => $hashover->setup->usesShortDates,
+ 'allows-images' => $hashover->setup->allowsImages,
+ 'uses-markdown' => $hashover->setup->usesMarkdown,
+ 'uses-cancel-buttons' => $hashover->setup->usesCancelButtons,
+ 'uses-auto-login' => $hashover->setup->usesAutoLogin,
+ 'uses-ajax' => $hashover->setup->usesAjax,
+ 'allows-login' => $hashover->setup->allowsLogin,
+ 'field-options' => $hashover->setup->fieldOptions
+ );
+
+ // Add UI HTML to data
+ $data['ui'] = array (
+ 'user-avatar' => $hashover->ui->userAvatar (),
+ 'name-link' => $hashover->ui->nameElement ('a'),
+ 'name-span' => $hashover->ui->nameElement ('span'),
+ 'parent-link' => $hashover->ui->parentThreadLink (),
+ 'edit-link' => $hashover->ui->formLink ('{{href}}', 'edit'),
+ 'reply-link' => $hashover->ui->formLink ('{{href}}', 'reply'),
+ 'like-link' => $hashover->ui->likeLink ('like'),
+ 'dislike-link' => $hashover->ui->likeLink ('dislike'),
+ 'like-count' => $hashover->ui->likeCount ('likes'),
+ 'dislike-count' => $hashover->ui->likeCount ('dislikes'),
+ 'name-wrapper' => $hashover->ui->nameWrapper (),
+ 'date-link' => $hashover->ui->dateLink (),
+ 'comment-wrapper' => $hashover->ui->commentWrapper (),
+ 'theme' => $hashover->templater->parseTheme ('comments.html'),
+ 'reply-form' => $hashover->ui->replyForm (),
+ 'edit-form' => $hashover->ui->editForm ()
+ );
+ }
+
+ // HashOver instance information
+ $data['instance'] = array (
+ 'primary-count' => $hashover->thread->primaryCount - 1,
+ 'total-count' => $hashover->thread->totalCount - 1,
+ 'page-url' => $hashover->setup->pageURL,
+ 'page-title' => $hashover->setup->pageTitle,
+ 'thread-name' => $hashover->setup->threadName,
+ 'file-path' => $hashover->setup->filePath,
+ 'initial-html' => $hashover->ui->initialHTML (false),
+ 'comments' => $hashover->comments
+ );
+
+ // Count according to `$showsReplyCount` setting
+ $show_number_comments = $hashover->getCommentCount ('show-number-comments');
+
+ // Add locales for UI uncollapse button
+ if ($hashover->setup->collapsesInterface !== false) {
+ $data['instance']['show-number-comments'] = $show_number_comments;
+ $data['instance']['post-comment-on'] = $hashover->ui->postCommentOn;
+ }
+
+ // Text for "Show X Other Comment(s)" link
+ if ($hashover->setup->collapsesComments !== false) {
+ // Check if at least 1 comment is to be shown
+ if ($hashover->setup->collapseLimit >= 1) {
+ // If so, use the "Show X Other Comments" locale
+ $more_link_locale = $hashover->locale->text['show-other-comments'];
+
+ // Shorter variables
+ $total_count = $hashover->thread->totalCount;
+ $collapse_limit = $hashover->setup->collapseLimit;
+
+ // Get number of comments after collapse limit
+ $other_count = ($total_count - 1) - $collapse_limit;
+
+ // Subtract deleted comment counts
+ if ($hashover->setup->countIncludesDeleted === false) {
+ $other_count -= $hashover->thread->collapsedDeletedCount;
+ }
+
+ // Decide if count is pluralized
+ $more_link_plural = ($other_count !== 1) ? 1 : 0;
+ $more_link_text = $more_link_locale[$more_link_plural];
+
+ // And inject the count into the locale string
+ $more_link_text = sprintf ($more_link_text, $other_count);
+ } else {
+ // If not, show count according to `$showsReplyCount` setting
+ $more_link_text = $show_number_comments;
+ }
+
+ // Add "Show X Other Comment(s)" link to instance
+ $data['instance']['more-link-text'] = $more_link_text;
+ }
+
+ // Generate statistics
+ $hashover->statistics->executionEnd ();
+
+ // HashOver statistics
+ $data['statistics'] = array (
+ 'execution-time' => $hashover->statistics->executionTime,
+ 'script-memory' => $hashover->statistics->scriptMemory,
+ 'system-memory' => $hashover->statistics->systemMemory
+ );
+
+ // Return JSON or JSONP function call
+ echo $hashover->misc->jsonData ($data);
+
+} catch (\Exception $error) {
+ $misc = new Misc ('json');
+ $message = $error->getMessage ();
+ $misc->displayError ($message);
+}
diff --git a/bootstrap/comments/backend/form-actions.php b/bootstrap/comments/backend/form-actions.php
new file mode 100644
index 0000000..5bed763
--- /dev/null
+++ b/bootstrap/comments/backend/form-actions.php
@@ -0,0 +1,114 @@
+<?php namespace HashOver;
+
+// Copyright (C) 2015-2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// HashOver is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// HashOver is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with HashOver. If not, see <http://www.gnu.org/licenses/>.
+
+
+// Check if request is for JSONP
+if (isset ($_GET['jsonp'])) {
+ // If so, setup HashOver for JavaScript
+ require ('javascript-setup.php');
+ $mode = 'javascript';
+ $postget = $_GET;
+} else {
+ // If not, setup HashOver for JSON
+ require ('json-setup.php');
+ $mode = 'json';
+ $postget = $_POST;
+}
+
+try {
+ // Instantiate HashOver class
+ $hashover = new \HashOver ($mode);
+ $hashover->setup->setPageURL ('request');
+ $hashover->setup->setPageTitle ('request');
+ $hashover->setup->setThreadName ('request');
+ $hashover->initiate ();
+ $hashover->finalize ();
+
+ // Instantiate class for writing and editing comments
+ $write_comments = new WriteComments (
+ $hashover->setup,
+ $hashover->thread
+ );
+
+ // Various POST data actions
+ $post_actions = array (
+ 'login',
+ 'logout',
+ 'post',
+ 'edit',
+ 'delete'
+ );
+
+ // Execute an action (write/edit/login/etc)
+ foreach ($post_actions as $action) {
+ if (empty ($postget[$action])) {
+ continue;
+ }
+
+ switch ($action) {
+ case 'login': {
+ $write_comments->login ();
+ break;
+ }
+
+ case 'logout': {
+ $write_comments->logout ();
+ break;
+ }
+
+ case 'post': {
+ $data = $write_comments->postComment ();
+ $hashover->defaultMetadata ();
+ break;
+ }
+
+ case 'edit': {
+ $data = $write_comments->editComment ();
+ break;
+ }
+
+ case 'delete': {
+ $write_comments->deleteComment ();
+ break;
+ }
+ }
+
+ break;
+ }
+
+ // Returns comment being saved as JSON
+ if (isset ($postget['ajax']) and isset ($data) and is_array ($data)) {
+ // Slit file into parts
+ $file = $data['file'];
+ $key_parts = explode ('-', $file);
+
+ // Parsed comment data
+ $comment = $data['comment'];
+ $parsed = $hashover->commentParser->parse ($comment, $file, $key_parts);
+
+ // Return JSON or JSONP function call
+ echo $hashover->misc->jsonData (array (
+ 'comment' => $parsed,
+ 'count' => $hashover->getCommentCount ()
+ ));
+ }
+} catch (\Exception $error) {
+ $misc = new Misc ($mode);
+ $message = $error->getMessage ();
+ $misc->displayError ($message);
+}
diff --git a/bootstrap/comments/backend/javascript-setup.php b/bootstrap/comments/backend/javascript-setup.php
new file mode 100644
index 0000000..6350d6f
--- /dev/null
+++ b/bootstrap/comments/backend/javascript-setup.php
@@ -0,0 +1,44 @@
+<?php namespace HashOver;
+
+// Copyright (C) 2017-2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// HashOver is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// HashOver is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with HashOver. If not, see <http://www.gnu.org/licenses/>.
+
+
+// Tell browser output is JavaScript
+header ('Content-Type: application/javascript');
+
+// Do some standard HashOver setup work
+require ('nocache-headers.php');
+require ('standard-setup.php');
+
+// Autoload class files
+spl_autoload_register (function ($uri) {
+ $uri = str_replace ('\\', '/', strtolower ($uri));
+ $class_name = basename ($uri);
+ $error = '"' . $class_name . '.php" file could not be included!';
+
+ // Check if class file could be included
+ if (!@include ('classes/' . $class_name . '.php')) {
+ // If not, construct JavaScript code to display an error
+ $js_error = 'var hashover = document.getElementById (\'hashover\') || document.body;' . PHP_EOL;
+ $js_error .= 'var error = \'<p><b>HashOver</b>: ' . $error . '</p>\';' . PHP_EOL . PHP_EOL;
+ $js_error .= 'hashover.innerHTML += error;';
+
+ // Display JavaScript code
+ echo $js_error;
+ exit;
+ }
+});
diff --git a/bootstrap/comments/backend/json-setup.php b/bootstrap/comments/backend/json-setup.php
new file mode 100644
index 0000000..129e26f
--- /dev/null
+++ b/bootstrap/comments/backend/json-setup.php
@@ -0,0 +1,41 @@
+<?php namespace HashOver;
+
+// Copyright (C) 2017-2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// HashOver is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// HashOver is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with HashOver. If not, see <http://www.gnu.org/licenses/>.
+
+
+// Tell browser output is JSON
+header ('Content-Type: application/json');
+
+// Do some standard HashOver setup work
+require ('nocache-headers.php');
+require ('standard-setup.php');
+
+// Autoload class files
+spl_autoload_register (function ($uri) {
+ $uri = str_replace ('\\', '/', strtolower ($uri));
+ $class_name = basename ($uri);
+
+ // Check if class file could be included
+ if (!@include ('classes/' . $class_name . '.php')) {
+ // If not, return JSON code to display an error
+ echo json_encode (array (
+ 'error' => '"' . $class_name . '.php" file could not be included!'
+ ));
+
+ exit;
+ }
+});
diff --git a/bootstrap/comments/backend/like.php b/bootstrap/comments/backend/like.php
new file mode 100644
index 0000000..e808bd7
--- /dev/null
+++ b/bootstrap/comments/backend/like.php
@@ -0,0 +1,176 @@
+<?php namespace HashOver;
+
+// Copyright (C) 2010-2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// HashOver is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// HashOver is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with HashOver. If not, see <http://www.gnu.org/licenses/>.
+//
+//--------------------
+//
+// Script Description:
+//
+// This script reads a given comment file, retrieves the like count,
+// increases the count by one, then writes the file. Assuming the
+// visitor hasn't already liked the given comment before and the
+// visitor isn't the comment's original poster.
+
+
+// Check if request is for JSONP
+if (isset ($_GET['jsonp'])) {
+ // If so, setup HashOver for JavaScript
+ require ('javascript-setup.php');
+} else {
+ // If not, setup HashOver for JSON
+ require ('json-setup.php');
+}
+
+// Sets cookie indicating what comment was liked
+function set_like (&$hashover, $like_hash, $set, &$likes)
+{
+ $hashover->cookies->set ($like_hash, $set, mktime (0, 0, 0, 11, 26, 3468));
+ $likes = $likes + 1;
+}
+
+// Decreases a like/dislike count
+function like_decrease (&$likes)
+{
+ if ($likes > 0) {
+ $likes = $likes - 1;
+ }
+}
+
+// Likes or dislikes a comment
+function liker ($action, $like_hash, &$hashover, &$comment)
+{
+ // Get the comment array key based on given action
+ $key = ($action === 'like') ? 'likes' : 'dislikes';
+ $set = ($action === 'like') ? 'liked' : 'disliked';
+
+ // Get like cookie
+ $like_cookie = $hashover->cookies->getValue ($like_hash);
+
+ // Check that a like/dislike cookie is not already set
+ if ($like_cookie === null) {
+ // If so, set the cookie and increase the like/dislike count
+ set_like ($hashover, $like_hash, $set, $comment[$key]);
+ } else {
+ // If not, we're unliking/un-disliking the comment
+ $opposite_key = ($action === 'like') ? 'dislikes' : 'likes';
+ $opposite_set = ($action === 'like') ? 'disliked' : 'liked';
+
+ // Check if the user has liked the comment
+ if ($like_cookie === $set) {
+ // If so, expire the like cookie
+ $hashover->cookies->expireCookie ($like_hash);
+
+ // And decrease the like count
+ like_decrease ($comment[$key]);
+ }
+
+ // Check if the user has disliked the comment
+ if ($like_cookie === $opposite_set) {
+ // If so, expire the dislike cookie
+ set_like ($hashover, $like_hash, $set, $comment[$key]);
+
+ // And decrease the dislike count
+ like_decrease ($comment[$opposite_key]);
+ }
+ }
+}
+
+function get_json_response ($hashover, $key, $action)
+{
+ // JSON data
+ $data = array ();
+
+ // Store references to some long variables
+ $storageMode =& $hashover->thread->data->storageMode;
+ $thread = $hashover->setup->threadName;
+
+ // Sanitize file path
+ $file = str_replace ('../', '', $key);
+
+ // Read comment
+ $comment = $hashover->thread->data->read ($file, $thread);
+
+ // Return error message if failed to read comment
+ if ($comment === false) {
+ return array ('error' => 'Failed to read file: "' . $file . '"');
+ }
+
+ // Check if liker isn't poster via login ID comparision
+ if ($hashover->login->userIsLoggedIn and !empty ($comment['login_id'])) {
+ if ($hashover->cookies->getValue ('login') === $comment['login_id']) {
+ // Return error message if liker posted the comment
+ return array ('message' => 'Practice altruism!');
+ }
+ }
+
+ // Name of the cookie used to indicate liked comments
+ $like_hash = md5 ($hashover->setup->domain . $thread . '/' . $key);
+
+ // Action: like or dislike
+ $action = ($action !== 'dislike') ? 'like' : 'dislike';
+
+ // Like or dislike the comment
+ liker ($action, $like_hash, $hashover, $comment);
+
+ // Attempt to save file with updated like count
+ if ($hashover->thread->data->save ($file, $comment, true, $thread)) {
+ // If successful, add number of likes to JSON
+ if (isset ($comment['likes'])) {
+ $data['likes'] = $comment['likes'];
+ }
+
+ // And add dislikes to JSON as well
+ if (isset ($comment['dislikes'])) {
+ $data['dislikes'] = $comment['dislikes'];
+ }
+ } else {
+ // If failed, add error message to JSON
+ $data['error'] = 'Failed to save comment file!';
+ }
+
+ return $data;
+}
+
+try {
+ // Instanciate HashOver class
+ $hashover = new \HashOver ('json');
+
+ // Get required POST/GET data
+ $url = $hashover->setup->getRequest ('url', null);
+ $key = $hashover->setup->getRequest ('comment', null);
+ $action = $hashover->setup->getRequest ('action', null);
+
+ // Return error if we're missing necessary post data
+ if (($url and $key and $action) === null) {
+ return array ('error' => 'No action.');
+ }
+
+ // Continue HashOver setup
+ $hashover->setup->setPageURL ($url);
+ $hashover->initiate ();
+
+ // Display JSON response
+ $data = get_json_response ($hashover, $key, $action);
+
+ // Return JSON or JSONP function call
+ echo $hashover->misc->jsonData ($data);
+
+} catch (\Exception $error) {
+ $misc = new Misc ('json');
+ $message = $error->getMessage ();
+ $misc->displayError ($message);
+}
diff --git a/bootstrap/comments/backend/load-comments.php b/bootstrap/comments/backend/load-comments.php
new file mode 100644
index 0000000..918ca12
--- /dev/null
+++ b/bootstrap/comments/backend/load-comments.php
@@ -0,0 +1,71 @@
+<?php namespace HashOver;
+
+// Copyright (C) 2010-2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// HashOver is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// HashOver is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with HashOver. If not, see <http://www.gnu.org/licenses/>.
+
+
+// Check if request is for JSONP
+if (isset ($_GET['jsonp'])) {
+ // If so, setup HashOver for JavaScript
+ require ('javascript-setup.php');
+} else {
+ // If not, setup HashOver for JSON
+ require ('json-setup.php');
+}
+
+try {
+ // Instantiate HashOver class
+ $hashover = new \HashOver ('json');
+ $hashover->setup->setPageURL ('request');
+ $hashover->setup->setPageTitle ('request');
+ $hashover->setup->setThreadName ('request');
+ $hashover->setup->collapsesComments = false;
+ $hashover->initiate ();
+
+ // Setup where to start reading comments
+ $start = $hashover->setup->getRequest ('start', 0);
+
+ // Check for comments
+ if ($hashover->thread->totalCount > 1) {
+ // Parse primary comments
+ // TODO: Use starting point
+ $hashover->parsePrimary (0);
+
+ // Display as JSON data
+ $data = $hashover->comments;
+
+ // Generate statistics
+ $hashover->statistics->executionEnd ();
+
+ // HashOver statistics
+ $data['statistics'] = array (
+ 'execution-time' => $hashover->statistics->executionTime,
+ 'script-memory' => $hashover->statistics->scriptMemory,
+ 'system-memory' => $hashover->statistics->systemMemory
+ );
+ } else {
+ // Return no comments message
+ $data = array ('No comments.');
+ }
+
+ // Return JSON or JSONP function call
+ echo $hashover->misc->jsonData ($data);
+
+} catch (\Exception $error) {
+ $misc = new Misc ('json');
+ $message = $error->getMessage ();
+ $misc->displayError ($message);
+}
diff --git a/bootstrap/comments/backend/locales/da.php b/bootstrap/comments/backend/locales/da.php
new file mode 100644
index 0000000..8e97915
--- /dev/null
+++ b/bootstrap/comments/backend/locales/da.php
@@ -0,0 +1,206 @@
+<?php
+
+// Copyright (C) 2015-2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// I, Jacob Barkdull, hereby release this work into the public domain.
+// This applies worldwide. If this is not legally possible, I grant any
+// entity the right to use this work for any purpose, without any
+// conditions, unless such conditions are required by law.
+
+
+// Danish text for forms, buttons, links, and tooltips
+$locale = array (
+ 'comment-form' => 'Skriv kommentar her...',
+ 'reply-form' => 'Skriv svar her...',
+ 'comment-formatting' => 'Formatering',
+ 'accepted-format' => 'Akseptabel %s',
+ 'accepted-html' => '&lt;b&gt;, &lt;strong&gt;, &lt;u&gt;, &lt;i&gt;, &lt;em&gt;, &lt;s&gt;, &lt;big&gt;, &lt;small&gt;, &lt;sup&gt;, &lt;sub&gt;, &lt;pre&gt;, &lt;ul&gt;, &lt;ol&gt;, &lt;li&gt;, &lt;blockquote&gt;, &lt;code&gt; undslipper HTML, URL\'er blir til links automatisk, og [img]URL her[/img] vil vise et eksternt billede.',
+ 'accepted-markdown' => '**Fed**, _understregning_, *kursiv*, ~~gennemstregning~~, `fremhævning`, ```kode``` undslipper HTML. HTML og Markdown kan anvendes sammen i din kommentar.',
+ 'post-button' => 'Send Kommentar',
+ 'login' => 'Login',
+ 'login-tip' => 'Login (ikke påkrævet)',
+ 'logout' => 'Logout',
+ 'be-first-name' => 'Ingen kommentarer endnu.',
+ 'pending-name' => 'Venter...',
+ 'deleted-name' => 'Slettet...',
+ 'error-name' => 'Fejl...',
+ 'be-first-note' => 'Vær den første til at kommentere!',
+ 'pending-note' => 'Denne kommentar venter på godkendelse.',
+ 'deleted-note' => 'Denne kommentar er slettet.',
+ 'error-note' => 'Et eller andet gik galt. Kunne ikke hente kommentaren.',
+ 'options' => 'Valgmuligheder',
+ 'cancel' => 'Fortryd',
+ 'reply-to-comment' => 'Svar på kommentar',
+ 'edit-your-comment' => 'Rediger din kommentar',
+ 'optional' => 'Valgfri',
+ 'required' => 'Påkrævet',
+ 'name' => 'Navn',
+ 'name-tip' => 'Navn (%s)',
+ 'password' => 'Pasord',
+ 'password-tip' => 'Pasord (%s, gør det muligt for dig, at redigere eller slette denne kommentar)',
+ 'confirm-password' => 'Bekræft Pasord',
+ 'email' => 'Email Addresse',
+ 'email-tip' => 'Email Addresse (%s, for varsling via email)',
+ 'website' => 'Websted',
+ 'website-tip' => 'Websted (%s)',
+ 'logged-in' => 'Du er blevet logget ind!',
+ 'logged-out' => 'Du er blevet logget ud!',
+ 'comment-needed' => 'Du fik aldrig skrevet en kommentar. Prøv igen.',
+ 'reply-needed' => 'Du fik aldrig skrevet et svar. Prøv igen.',
+ 'field-needed' => 'Feltet "%s" er påkrævet.',
+ 'post-fail' => 'Fejl! Du har ikke de nødvendige rettigheder.',
+ 'comment-deleted' => 'Kommentar Slettet!',
+ 'post-reply' => 'Send Svar',
+ 'delete' => 'Slet',
+ 'permanently-delete' => 'Slet Permanent',
+ 'subscribe' => 'Påmind mig om svar',
+ 'subscribe-tip' => 'Tilmeld email varsling',
+ 'edit-comment' => 'Rediger kommentar',
+ 'status' => 'Status',
+ 'status-approved' => 'Godkendt',
+ 'status-pending' => 'Afventer godkendelse',
+ 'status-deleted' => 'Markerede slettet',
+ 'save' => 'Gem',
+ 'no-email-warning' => 'Du vil ikke modtage varslinger af svar til din kommentar uden at oplyse en email.',
+ 'invalid-email' => 'Email addressen du skrev er ugyldig.',
+ 'delete-comment' => 'Er du sikker på, at du vil slette denne kommentar?',
+ 'post-comment-on' => array ('Skriv en kommentar', 'Skriv en kommentar til "%s"'),
+ 'popular-comments' => array ('Populære Kommentarer', 'Populære Kommentarer'),
+ 'showing-comments' => array ('Viser %d Kommentar', 'Viser %d Kommentarer'),
+ 'count-link' => array ('%d Kommentar', '%d Kommentarer'),
+ 'count-replies' => array ('%d med svar', '%d med svar'),
+ 'sort' => 'Sorter',
+ 'sort-ascending' => 'I rækkefølge',
+ 'sort-descending' => 'I omvendt rækkefølge',
+ 'sort-by-date' => 'Nyeste først',
+ 'sort-by-likes' => 'Flest likes',
+ 'sort-by-replies' => 'Flest svar',
+ 'sort-by-discussion' => 'Efter diskussion',
+ 'sort-by-popularity' => 'Efter popularitet',
+ 'sort-by-name' => 'Efter navn',
+ 'sort-threads' => 'Tråde',
+ 'thread' => 'I svar til %s',
+ 'thread-tip' => 'Hop til toppen af tråden',
+ 'comments' => 'Kommentarer',
+ 'replies' => 'Svar',
+ 'edit' => 'Rediger',
+ 'reply' => 'Svar',
+ 'like' => array ('Synes om', 'Synes om'),
+ 'liked' => 'Synes godt om',
+ 'unlike' => 'Synes ikke om',
+ 'like-comment' => '\'Synes godt om\' denne kommentar',
+ 'liked-comment' => 'Synes ikke godt om denne kommentar',
+ 'dislike' => array ('Synes ikke om', 'Synes ikke om'),
+ 'disliked' => 'Syntes ikke om',
+ 'dislike-comment' => '\'Synes ikke om\' denne kommentar',
+ 'disliked-comment' => 'Du \'Syntes ikke om\' denne kommentar',
+ 'commenter-tip' => 'Du vil ikke blive varslet via email',
+ 'subscribed-tip' => 'vil blive varslet via email',
+ 'unsubscribed-tip' => 'er ikke tilmeldt email varsling',
+ 'show-other-comments' => array ('Vis %d Yderlig Kommentar', 'Vis %d Yderligere Kommentarer'),
+ 'show-number-comments' => array ('Vis %d Kommentar', 'Vis %d Kommentar'),
+ 'date-time' => '%s \p\å %s',
+ 'date-years' => array ('%d år siden', '%d år siden'),
+ 'date-months' => array ('%d måned siden', '%d måneder siden'),
+ 'date-days' => array ('%d dag siden', '%d dage siden'),
+ 'date-today' => '%s i dag',
+ 'date-day-names' => array ('Søndag', 'Mandag', 'tirsdag', 'onsdag', 'torsdag', 'Fredag', 'lørdag'),
+ 'date-month-names' => array ('Januar', 'Februar', 'Marts', 'April', 'Kan', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'December'),
+ 'untitled' => 'Unavngivet',
+ 'external-image-tip' => 'Klik for at se ekstern billede',
+ 'loading' => 'Henter...',
+ 'click-to-close' => 'Klik for at lukke',
+ 'hashover-comments' => 'HashOver Kommentarer',
+ 'rss-feed' => 'RSS Feed',
+ 'source-code' => 'Kilde Kode',
+
+ 'source-code-sub' => 'HashOver server-side kildekoden viewer',
+ 'type' => 'Type',
+ 'path' => 'Sti',
+ 'view-as' => 'Se som',
+ 'text' => 'Tekst',
+ 'download' => 'Hent',
+
+ 'documentation' => 'Dokumentation',
+ 'coming-soon' => 'Kommer snart',
+ 'example' => 'Eksempel',
+ 'back' => 'Tilbage',
+ 'value' => 'Værdi',
+
+ 'successful-save' => 'Vellykket gemt!',
+ 'failed-to-save' => 'Kunne ikke gemme! Kontroller tilladelser.',
+ 'permissions-info' => 'Give "%s" tilladelser 0755 og ejerskab til "%s"-brugeren.',
+
+ 'admin' => 'Admin',
+ 'moderation' => 'Moderation',
+ 'block-ip-addresses' => 'Bloker IP-adresser',
+ 'filter-url-queries' => 'Filter URL Queries',
+ 'check-for-updates' => 'Check for Updates',
+ 'settings' => 'Indstillinger',
+
+ 'admin-required' => 'Du skal være logget som administrator',
+
+ 'blocklist-title' => 'IP-adresseliste',
+ 'blocklist-sub' => 'Blokér specifikke IP-adresser',
+ 'blocklist-ip-tip' => 'IP-adresse eller tom for at fjerne',
+
+ 'url-queries-title' => 'Ignorerede URL forespørgsler',
+ 'url-queries-sub' => 'Filtrer hvilke webadressespørgsmål der skal ignoreres',
+ 'url-queries-name-tip' => 'Navne navn eller tom for at fjerne',
+ 'url-queries-value-tip' => 'Forespørgselsværdi eller tom for enhver værdi',
+
+ 'settings-sub' => 'Skift forskellige indstillinger',
+ 'moderation-sub' => 'Indlæg, redigere, godkende og slet kommentarer',
+
+ 'setting-language' => 'Sprog',
+ 'setting-theme' => 'Tema',
+ 'setting-uses-moderation' => 'Kommentarer indsendt af normale brugere kræver moderering',
+ 'setting-pends-user-edits' => 'Kommentarer redigeret af normale brugere kræver yderligere moderering',
+ 'setting-data-format' => 'Kommentar dataformat',
+ 'setting-default-name' => 'Standardkommentatornavn',
+ 'setting-allows-images' => 'Tillad visning af billeder i kommentarer',
+ 'setting-allows-login' => 'Tillad brugere at logge ind',
+ 'setting-allows-likes' => 'Tillad brugere at kunne lide kommentarer',
+ 'setting-allows-dislikes' => 'Tillad brugere at ikke lide kommentarer',
+ 'setting-uses-ajax' => 'Aktiver asynkron JavaScript-funktioner',
+ 'setting-collapses-interface' => 'Skjul hele HashOver brugergrænseflade',
+ 'setting-collapses-comments' => 'Skjul et konfigurerbart antal kommentarer',
+ 'setting-collapse-limit' => 'Antal kommentarer til sammenbrud',
+ 'setting-reply-mode' => 'Visningstilstand for kommenterede tråde',
+ 'setting-stream-depth' => 'Antal svarindrykninger før strømmen er fladt',
+ 'setting-popularity-threshold' => 'Netto antal synes om, at en kommentar skal være populær',
+ 'setting-popularity-limit' => 'Antal populære kommentarer, der skal vises',
+ 'setting-uses-markdown' => 'Aktiver Markdown support',
+ 'setting-server-timezone' => 'Server tidzone',
+ 'setting-uses-user-timezone' => 'Vis datoer / tider i brugerens tidszone (JavaScript-tilstand)',
+ 'setting-uses-short-dates' => 'Aktiver kortere datoer / tider (eksempel "1 dag siden")',
+ 'setting-time-format' => 'Tidsformat, brug "H:i" til 24-timers format',
+ 'setting-date-format' => 'Datoformat',
+ 'setting-displays-title' => 'Aktiver visning af sidetitel',
+ 'setting-form-position' => 'Position for primær kommentarformular',
+ 'setting-uses-auto-login' => 'Log automatisk ind brugere, når de sender kommentarer',
+ 'setting-shows-reply-count' => 'Vis svar tæller adskilt fra total tæller',
+ 'setting-count-includes-deleted' => 'Inkluder slettede kommentarer i kommentar tæller',
+ 'setting-icon-mode' => 'Avatarikonets visningstilstand',
+ 'setting-icon-size' => 'Avatarikonstørrelse',
+ 'setting-image-format' => 'Formater for ikoner og andre billeder',
+ 'setting-uses-labels' => 'Vis etiketter over input',
+ 'setting-uses-cancel-buttons' => 'Om formularer har annullere knapper',
+ 'setting-appends-css' => 'Tilføj automatisk HashOver CSS til side',
+ 'setting-appends-rss' => 'Tilføj HashOver RSS Feed links til side',
+ 'setting-login-method' => 'Bruger login system',
+ 'setting-sets-cookies' => 'Aktiver cookies',
+ 'setting-secure-cookies' => 'Brug kun HTTPS-kun cookies',
+ 'setting-stores-ip-address' => 'Aktiver opbevaring af brugerens IP-adresser',
+ 'setting-subscribes-user' => 'Abonner brugeren til at e-mail-meddelelser som standard',
+ 'setting-allows-user-replies' => 'Angiv brugerens e-mail som "Svar til" i svarmeddelelser',
+ 'setting-noreply-email' => 'E-mail-adresse, der bruges, når der ikke gives e-mail',
+ 'setting-spam-batabase' => 'SPAM database placering',
+ 'setting-spam-check-modes' => 'Tilstande til at udføre SPAM check under',
+ 'setting-gravatar-force' => 'Brug tema Gravatar billeder i stedet for avatars',
+ 'setting-gravatar-default' => 'Standard Gravatar tema at bruge',
+ 'setting-minifies-javascript' => 'Aktiver JavaScript-minificering',
+ 'setting-minify-level' => 'JavaScript-minusniveau',
+ 'setting-allow-local-metadata' => 'Tillad side metadata at blive opdateret fra localhost'
+);
diff --git a/bootstrap/comments/backend/locales/de.php b/bootstrap/comments/backend/locales/de.php
new file mode 100644
index 0000000..95f1f7f
--- /dev/null
+++ b/bootstrap/comments/backend/locales/de.php
@@ -0,0 +1,206 @@
+<?php
+
+// Copyright (C) 2015-2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// I, Jacob Barkdull, hereby release this work into the public domain.
+// This applies worldwide. If this is not legally possible, I grant any
+// entity the right to use this work for any purpose, without any
+// conditions, unless such conditions are required by law.
+
+
+// German text for forms, buttons, links, and tooltips
+$locale = array (
+ 'comment-form' => 'Kommentar hier eingeben...',
+ 'reply-form' => 'Antwort hier eingeben...',
+ 'comment-formatting' => 'Formatierung',
+ 'accepted-format' => 'Akzeptiertes %s',
+ 'accepted-html' => '&lt;b&gt;, &lt;strong&gt;, &lt;u&gt;, &lt;i&gt;, &lt;em&gt;, &lt;s&gt;, &lt;big&gt;, &lt;small&gt;, &lt;sup&gt;, &lt;sub&gt;, &lt;pre&gt;, &lt;ul&gt;, &lt;ol&gt;, &lt;li&gt;, &lt;blockquote&gt;, &lt;code&gt; maskieren HTML, URLs werden automatisch zu Links umgewandelt und mit [img]Bild-URL[/img] werden externe Bilder angezeigt.',
+ 'accepted-markdown' => '**Fett**, _unterstrichen_, *kursiv*, ~~durchgestrichen~~, `markieren` und ```code``` maskieren HTML. HTML und Markdown können zusammen verwendet werden.',
+ 'post-button' => 'Kommentar hinzufügen',
+ 'login' => 'Anmelden',
+ 'login-tip' => 'Anmelden (optional)',
+ 'logout' => 'Abmelden',
+ 'be-first-name' => 'Noch keine Kommentare.',
+ 'pending-name' => 'Wartend...',
+ 'deleted-name' => 'Gelöscht...',
+ 'error-name' => 'Fehler...',
+ 'be-first-note' => 'Schreib den ersten Kommentar!',
+ 'pending-note' => 'Dieser Kommentar wartet der Zustimmung.',
+ 'deleted-note' => 'Dieser Kommentar wurde gelöscht.',
+ 'error-note' => 'Etwas ist schief gelaufen. Konnte diesen Kommentar nicht abrufen.',
+ 'options' => 'Optionen',
+ 'cancel' => 'Abbrechen',
+ 'reply-to-comment' => 'Auf Kommentar antworten',
+ 'edit-your-comment' => 'Kommentar bearbeiten',
+ 'optional' => 'Optional',
+ 'required' => 'Erforderlich',
+ 'name' => 'Name',
+ 'name-tip' => 'Name (%s)',
+ 'password' => 'Passwort',
+ 'password-tip' => 'Passwort (%s, erlaubt das Bearbeiten oder Löschen dieses Kommentars)',
+ 'confirm-password' => 'Passwort bestätigen',
+ 'email' => 'E-Mail-Adresse',
+ 'email-tip' => 'E-Mail-Adresse (%s, für E-Mail-Benachrichtigungen)',
+ 'website' => 'Webseite',
+ 'website-tip' => 'Webseite (%s)',
+ 'logged-in' => 'Du hast dich erfolgreich angemeldet!',
+ 'logged-out' => 'Du hast dich erfolgreich abgemeldet!',
+ 'comment-needed' => 'Du hast den Kommentar falsch eingegeben. Bitte versuche es erneut.',
+ 'reply-needed' => 'Du hast die Antwort falsch eingegeben. Bitte versuche es erneut.',
+ 'field-needed' => '"%s" ist erforderlich.',
+ 'post-fail' => 'Ups, du hast nicht die erforderlichen Rechte!',
+ 'comment-deleted' => 'Kommentar gelöscht!',
+ 'post-reply' => 'Antwort absenden',
+ 'delete' => 'Löschen',
+ 'permanently-delete' => 'Permanent Löschen',
+ 'subscribe' => 'Benachrichtige mich über Antworten',
+ 'subscribe-tip' => 'E-Mail Benachrichtigung abonnieren',
+ 'edit-comment' => 'Kommentar bearbeiten',
+ 'status' => 'Status',
+ 'status-approved' => 'Genehmigt',
+ 'status-pending' => 'Genehmigung ausstehend',
+ 'status-deleted' => 'Markierte gelöscht',
+ 'save' => 'Speichern',
+ 'no-email-warning' => 'Du wirst bei neuen Kommentaren keine Benachrichtigung erhalten, wenn du keine E-Mail angibst.',
+ 'invalid-email' => 'Die von dir eingegebene E-Mail-Adresse ist ungültig.',
+ 'delete-comment' => 'Möchtest du diesen Kommentar wirklich löschen?',
+ 'post-comment-on' => array ('Kommentar hinzufügen', 'Kommentar hinzufügen zu "%s"'),
+ 'popular-comments' => array ('Beliebtester Kommentar', 'Beliebteste Kommentare'),
+ 'showing-comments' => array ('Anzeigen von %d Kommentar', 'Anzeigen von %d Kommentaren'),
+ 'count-link' => array ('%d Kommentar', '%d Kommentare'),
+ 'count-replies' => array ('%d inklusive Antwort', '%d inklusive Antworten'),
+ 'sort' => 'Sortieren',
+ 'sort-ascending' => 'Aufsteigend',
+ 'sort-descending' => 'Absteigend',
+ 'sort-by-date' => 'Neueste zuerst',
+ 'sort-by-likes' => 'Nach Likes',
+ 'sort-by-replies' => 'Nach Antworten',
+ 'sort-by-discussion' => 'Nach Diskussionen',
+ 'sort-by-popularity' => 'Nach Popularität',
+ 'sort-by-name' => 'Nach Kommentator',
+ 'sort-threads' => 'Threads',
+ 'thread' => 'Als Antwort auf %s',
+ 'thread-tip' => 'Spring zum Anfang des Threads',
+ 'comments' => 'Kommentare',
+ 'replies' => 'Antworten',
+ 'edit' => 'Bearbeiten',
+ 'reply' => 'Antworten',
+ 'like' => array ('Like', 'Likes'),
+ 'liked' => 'Liked',
+ 'unlike' => 'Unlike',
+ 'like-comment' => '\'Like\' diesen Kommentar',
+ 'liked-comment' => '\'Unlike\' diesen Kommentar',
+ 'dislike' => array ('Dislike', 'Dislikes'),
+ 'disliked' => 'Disliked',
+ 'dislike-comment' => '\'Dislike\' diesen Kommentar',
+ 'disliked-comment' => '\'Disliked\' diesen Kommentar',
+ 'commenter-tip' => 'Du wirst nicht per E-Mail benachrichtigt',
+ 'subscribed-tip' => 'wirst per E-Mail benachrichtigt',
+ 'unsubscribed-tip' => 'hat E-Mail-Benachrichtigungen nicht abonniert',
+ 'show-other-comments' => array ('Zeige %d anderen Kommentar', 'Zeige %d weitere Kommentare'),
+ 'show-number-comments' => array ('Zeige %d Kommentar', 'Zeige %d Kommentare'),
+ 'date-time' => '%s \u\m %s \U\h\r',
+ 'date-years' => array ('Vor %d Jahr', 'Vor %d Jahren'),
+ 'date-months' => array ('Vor %d Monat', 'Vor %d Monaten'),
+ 'date-days' => array ('Vor %d Tag', 'Vor %d Tagen'),
+ 'date-today' => '%s Heute',
+ 'date-day-names' => array ('Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'),
+ 'date-month-names' => array ('Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'),
+ 'untitled' => 'Ohne Titel',
+ 'external-image-tip' => 'Klicken um externes Bild anzusehen',
+ 'loading' => 'Laden...',
+ 'click-to-close' => 'Klicken um zu schließen',
+ 'hashover-comments' => 'HashOver Kommentare',
+ 'rss-feed' => 'RSS-Feed',
+ 'source-code' => 'Quellcode',
+
+ 'source-code-sub' => 'HashOver serverseitiger Quellcode-Viewer',
+ 'type' => 'Dateityp',
+ 'path' => 'Dateipfad',
+ 'view-as' => 'Ansicht als',
+ 'text' => 'Text',
+ 'download' => 'Herunterladen',
+
+ 'documentation' => 'Dokumentation',
+ 'coming-soon' => 'Demnächst',
+ 'example' => 'Beispiel',
+ 'back' => 'Zurück',
+ 'value' => 'Wert',
+
+ 'successful-save' => 'Erfolgreich gespeichert!',
+ 'failed-to-save' => 'Fehler beim Speichern! Überprüfen Sie die Berechtigungen.',
+ 'permissions-info' => 'Geben Sie "%s" -Berechtigungen 0755 und Eigentumsrechte für den Benutzer "%s".',
+
+ 'admin' => 'Admin',
+ 'moderation' => 'Moderation',
+ 'block-ip-addresses' => 'IP-Adressen blockieren',
+ 'filter-url-queries' => 'URL-Anfragen filtern',
+ 'check-for-updates' => 'Nach Updates suchen',
+ 'settings' => 'Einstellungen',
+
+ 'admin-required' => 'Sie müssen als Admin angemeldet sein',
+
+ 'blocklist-title' => 'IP Address Blocklist',
+ 'blocklist-sub' => 'Bestimmte IP-Adressen blockieren',
+ 'blocklist-ip-tip' => 'IP-Adresse oder leer zum Entfernen',
+
+ 'url-queries-title' => 'Ignorierte URL-Anfragen',
+ 'url-queries-sub' => 'Filter welche URL-Anfragen ignoriert werden sollen',
+ 'url-queries-name-tip' => 'Namen abfragen oder leer zum Entfernen',
+ 'url-queries-value-tip' => 'Abfrage Wert oder leer für irgendeinen Wert',
+
+ 'settings-sub' => 'Verschiedene Einstellungen ändern',
+ 'moderation-sub' => 'Kommentare posten, bearbeiten, genehmigen und löschen',
+
+ 'setting-language' => 'Sprache',
+ 'setting-theme' => 'Thema',
+ 'setting-uses-moderation' => 'Kommentare von normalen Benutzern erfordern Moderation',
+ 'setting-pends-user-edits' => 'Von normalen Benutzern bearbeitete Kommentare erfordern zusätzliche Moderation',
+ 'setting-data-format' => 'Kommentar Datenformat',
+ 'setting-default-name' => 'Name des Standardbenenners',
+ 'setting-allows-images' => 'Anzeige von Bildern in Kommentaren erlauben',
+ 'setting-allows-login' => 'Nutzern erlauben sich einzuloggen',
+ 'setting-allows-likes' => 'Benutzern erlauben, Kommentare zu mögen',
+ 'setting-allows-dislikes' => 'Benutzern erlauben, Kommentare nicht zu mögen',
+ 'setting-uses-ajax' => 'Asynchrone JavaScript-Funktionen aktivieren',
+ 'setting-collapses-interface' => 'Gesamte HashOver Benutzeroberfläche ausblenden',
+ 'setting-collapses-comments' => 'Eine konfigurierbare Anzahl von Kommentaren reduzieren',
+ 'setting-collapse-limit' => 'Anzahl der zu minimierenden Kommentare',
+ 'setting-reply-mode' => 'Anzeigemodus für Kommentar-Threads',
+ 'setting-stream-depth' => 'Anzahl der Antworteingaben, bevor der Stream abgeflacht ist',
+ 'setting-popularity-threshold' => 'Nettozahl der Likes, die ein Kommentar haben muss, um beliebt zu sein',
+ 'setting-popularity-limit' => 'Anzahl der anzuzeigenden beliebten Kommentare',
+ 'setting-uses-markdown' => 'Markdown-Unterstützung aktivieren',
+ 'setting-server-timezone' => 'Server Zeitzone',
+ 'setting-uses-user-timezone' => 'Zeige Datum / Uhrzeit in der Zeitzone des Benutzers (JavaScript-Modus)',
+ 'setting-uses-short-dates' => 'Kürzere Daten (Beispiel "vor 1 Tag") aktivieren',
+ 'setting-time-format' => 'Zeitformat, verwende "H:i" für 24-Stunden-Format',
+ 'setting-date-format' => 'Datumsformat',
+ 'setting-displays-title' => 'Anzeige des Seitentitels aktivieren',
+ 'setting-form-position' => 'Position für primäres Kommentarformular',
+ 'setting-uses-auto-login' => 'Benutzer automatisch anmelden, wenn sie Kommentare posten',
+ 'setting-shows-reply-count' => 'Antwortanzahl getrennt von Gesamtanzahl anzeigen',
+ 'setting-count-includes-deleted' => 'Gelöschte Kommentare in Kommentar zählen',
+ 'setting-icon-mode' => 'Avatar icon display mode',
+ 'setting-icon-size' => 'Avatar icon size',
+ 'setting-image-format' => 'Format für Icons und andere Bilder',
+ 'setting-uses-labels' => 'Beschriftungen über Eingaben anzeigen',
+ 'setting-uses-cancel-buttons' => 'Ob Formulare Abbruchtasten haben',
+ 'setting-appends-css' => 'HashOver CSS automatisch zur Seite hinzufügen',
+ 'setting-appends-rss' => 'HashOver RSS Feed Links zur Seite hinzufügen',
+ 'setting-login-method' => 'Benutzer-Login-System',
+ 'setting-sets-cookies' => 'Cookies aktivieren',
+ 'setting-secure-cookies' => 'Sichere HTTPS-Cookies verwenden',
+ 'setting-stores-ip-address' => 'Speichern von Benutzer-IP-Adressen aktivieren',
+ 'setting-subscribes-user' => 'Abonnieren Sie den Benutzer standardmäßig auf E-Mail-Benachrichtigungen',
+ 'setting-allows-user-replies' => 'Benutzer-E-Mail als "Antwort an" in den Antwortbenachrichtigungen festlegen',
+ 'setting-noreply-email' => 'E-Mail-Adresse wird verwendet, wenn keine E-Mail angegeben wird',
+ 'setting-spam-batabase' => 'SPAM Datenbankstandort',
+ 'setting-spam-check-modes' => 'Modi zum SPAM-Check unter',
+ 'setting-gravatar-force' => 'Benutze thematische Gravatar-Bilder anstelle von Avataren',
+ 'setting-gravatar-default' => 'Standard Gravatar Thema zu verwenden',
+ 'setting-minifies-javascript' => 'JavaScript minification aktivieren',
+ 'setting-minify-level' => 'JavaScript Minimierungsstufe',
+ 'setting-allow-local-metadata' => 'Erlaube Seiten-Metadaten von localhost'
+);
diff --git a/bootstrap/comments/backend/locales/el.php b/bootstrap/comments/backend/locales/el.php
new file mode 100644
index 0000000..295ef5b
--- /dev/null
+++ b/bootstrap/comments/backend/locales/el.php
@@ -0,0 +1,206 @@
+<?php
+
+// Copyright (C) 2015-2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// I, Jacob Barkdull, hereby release this work into the public domain.
+// This applies worldwide. If this is not legally possible, I grant any
+// entity the right to use this work for any purpose, without any
+// conditions, unless such conditions are required by law.
+
+
+// Greek text for forms, buttons, links, and tooltips
+$locale = array (
+ 'comment-form' => 'Πληκτρολογήστε σχόλιο εδώ...',
+ 'reply-form' => 'Πληκτρολογήστε απάντηση εδώ...',
+ 'comment-formatting' => 'Μορφοποίηση',
+ 'accepted-format' => 'Αποδεκτή %s',
+ 'accepted-html' => '&lt;b&gt;, &lt;strong&gt;, &lt;u&gt;, &lt;i&gt;, &lt;em&gt;, &lt;s&gt;, &lt;big&gt;, &lt;small&gt;, &lt;sup&gt;, &lt;sub&gt;, &lt;pre&gt;, &lt;ul&gt;, &lt;ol&gt;, &lt;li&gt;, &lt;blockquote&gt;, &lt;code&gt; διαφυγές HTML, τα URLs γίνονται αυτόματα σύνδεσμοι, και το [img]URL εδώ[/img] θα προβάλει μια εξωτερική εικόνα.',
+ 'accepted-markdown' => '**Bold**, _underline_, *italic*, ~~strikethrough~~, `highlight`, ```code``` διαφυγές HTML. HTML και Markdown μπορούν να χρησιμοποιηθούν μαζί στο σχόλιό σας.',
+ 'post-button' => 'Αφήστε Σχόλιο',
+ 'login' => 'Σύνδεση',
+ 'login-tip' => 'Σύνδεση (προαιρετική)',
+ 'logout' => 'Αποσύνδεση',
+ 'be-first-name' => 'Δεν υπάρχουν σχόλια.',
+ 'pending-name' => 'Εκκρεμεί...',
+ 'deleted-name' => 'Διεγραμμένο...',
+ 'error-name' => 'Σφάλμα...',
+ 'be-first-note' => 'Σχολιάστε πρώτοι!',
+ 'pending-note' => 'Αυτό το σχόλιο αναμένει έγκριση.',
+ 'deleted-note' => 'Αυτό το σχόλιο έχει διαγραφεί.',
+ 'error-note' => 'Κάτι πήγε στραβά. Δεν ήταν δυνατή η ανάκτηση αυτού του σχολίου.',
+ 'options' => 'Επιλογές',
+ 'cancel' => 'Ακύρωση',
+ 'reply-to-comment' => 'Απαντήστε στο σχόλιο',
+ 'edit-your-comment' => 'Επεξεργαστείτε το σχόλιό σας',
+ 'optional' => 'Προαιρετικά',
+ 'required' => 'Απαιτείται',
+ 'name' => 'Όνομα',
+ 'name-tip' => 'Όνομα (%s)',
+ 'password' => 'Κωδικός',
+ 'password-tip' => 'Κωδικός (%s, σας επιτρέπει να επεξεργαστείτε ή να διαγράψετε αυτό το σχόλιο)',
+ 'confirm-password' => 'Επιβεβαίωση Κωδικού',
+ 'email' => 'Διεύθυνση E-mail',
+ 'email-tip' => 'Διεύθυνση E-mail (%s, για e-mail ειδοποιήσεις)',
+ 'website' => 'Ιστοσελίδα',
+ 'website-tip' => 'Ιστοσελίδα (%s)',
+ 'logged-in' => 'Έχετε συνδεθεί με επιτυχία!',
+ 'logged-out' => 'Έχετε αποσυνδεθεί με επιτυχία!',
+ 'comment-needed' => 'Δεν καταφέρατε να εισαγάγετε κατάλληλο σχόλιο. Παρακαλώ προσπαθήστε ξανα.',
+ 'reply-needed' => 'Δεν καταφέρατε να εισαγάγετε κατάλληλη απάντηση. Παρακαλώ προσπαθήστε ξανα.',
+ 'field-needed' => 'The πεδίο "%s" απαιτείται.',
+ 'post-fail' => 'Αποτυχία! Δεν έχετε επαρκή δικαιώματα.',
+ 'comment-deleted' => 'Το σχόλιο διαγράφηκε!',
+ 'post-reply' => 'Αφήστε Απάντηση',
+ 'delete' => 'Διαγραφή',
+ 'permanently-delete' => 'Διαγράψτε μόνιμα',
+ 'subscribe' => 'Ειδοποίησέ με για απαντήσεις',
+ 'subscribe-tip' => 'Εγγραφείτε σε ειδοποιήσεις e-mail',
+ 'edit-comment' => 'Επεξεργασία σχολίου',
+ 'status' => 'Κατάσταση',
+ 'status-approved' => 'Εγκρίθηκε',
+ 'status-pending' => 'Αναμένει έγκριση',
+ 'status-deleted' => 'Σημειώθηκε ως διεγραμμένο',
+ 'save' => 'Αποθήκευση',
+ 'no-email-warning' => 'Δε θα λάβετε ειδοποίηση για απαντήσεις στο σχόλιό σας χωρίς την παροχή κάποιου e-mail.',
+ 'invalid-email' => 'Η διεύθυνση e-mail που εισαγάγατε δεν είναι έγκυρη.',
+ 'delete-comment' => 'Θέλετε σίγουρα να διαγράψετε αυτό το σχόλιο;',
+ 'post-comment-on' => array ('Σχολιάστε', 'Σχολιάστε το "%s"'),
+ 'popular-comments' => array ('Δημοφιλέστερο Σχόλιο', 'Δημοφιλέστερα Σχόλια'),
+ 'showing-comments' => array ('Προβολή %d Σχολίου', 'Προβολή %d Σχολίων'),
+ 'count-link' => array ('%d Σχόλιο', '%d Σχόλια'),
+ 'count-replies' => array ('%d με την απάντηση', '%d με τις απαντήσεις'),
+ 'sort' => 'Ταξινόμηση',
+ 'sort-ascending' => 'Σε σειρά',
+ 'sort-descending' => 'Σε αντίστροφη σειρά',
+ 'sort-by-date' => 'Νεότερο πρώτο',
+ 'sort-by-likes' => 'Κατά likes',
+ 'sort-by-replies' => 'Κατά απαντήσεις',
+ 'sort-by-discussion' => 'Κατά συζήτηση',
+ 'sort-by-popularity' => 'Κατά δημοφιλία',
+ 'sort-by-name' => 'Κατά σχολιαστή',
+ 'sort-threads' => 'Νήματα',
+ 'thread' => 'Ως απάντηση σε %s',
+ 'thread-tip' => 'Μεταφορά στην αρχή του νήματος',
+ 'comments' => 'Σχόλια',
+ 'replies' => 'Απαντήσεις',
+ 'edit' => 'Επεξεργασία',
+ 'reply' => 'Απάντηση',
+ 'like' => array ('Like', 'Likes'),
+ 'liked' => 'Liked',
+ 'unlike' => 'Unlike',
+ 'like-comment' => '\'Like\' αυτό το σχόλιο',
+ 'liked-comment' => 'Unlike αυτό το σχόλιο',
+ 'dislike' => array ('Dislike', 'Dislikes'),
+ 'disliked' => 'Disliked',
+ 'dislike-comment' => '\'Dislike\' αυτό το σχόλιο',
+ 'disliked-comment' => 'Κάνατε \'Dislike\' αυτό το σχόλιο',
+ 'commenter-tip' => 'Δε θα ειδοποιηθείτε μέσω e-mail',
+ 'subscribed-tip' => 'δε θα ειδοποιηθεί μέσω e-mail',
+ 'unsubscribed-tip' => 'δεν έχει εγγραφεί σε e-mail ειδοποιήσεις',
+ 'show-other-comments' => array ('Προβολή %d Άλλου Σχολίου', 'Προβολή %d Άλλων Σχολίων'),
+ 'show-number-comments' => array ('Προβολή %d Σχολίου', 'Προβολή %d Σχολίων'),
+ 'date-time' => '%s \a\t %s',
+ 'date-years' => array ('%d χρόνο πριν', '%d χρόνια πριν'),
+ 'date-months' => array ('%d μήνα πριν', '%d μήνες πριν'),
+ 'date-days' => array ('%d ημέρα πριν', '%d ημέρες πριν'),
+ 'date-today' => '%s σήμερα',
+ 'date-day-names' => array ('Κυριακή', 'Δευτέρα', 'Τρίτη', 'Τετάρτη', 'Πέμπτη', 'Παρασκευή', 'Σάββατο'),
+ 'date-month-names' => array ('Ιανουάριος', 'Φεβρουάριος', 'Μάρτιος', 'Απρίλιος', 'Μάιος', 'Ιούνιος', 'Ιούλιος', 'Αύγουστος', 'Σεπτέμβριος', 'Οκτώβριος', 'Νοέμβριος', 'Δεκέμβριος'),
+ 'untitled' => 'Χωρίς τίτλο',
+ 'external-image-tip' => 'Κλικ για να δείτε την εικόνα',
+ 'loading' => 'Φόρτωση...',
+ 'click-to-close' => 'Κλικ για σχόλιο',
+ 'hashover-comments' => 'HashOver Comments',
+ 'rss-feed' => 'Ροή RSS',
+ 'source-code' => 'Πηγαίος Κώδικας',
+
+ 'source-code-sub' => 'Προβολή πηγαίου κώδικα HashOver εντός του εξυπηρετητή',
+ 'type' => 'Τύπος',
+ 'path' => 'Διαδρομή',
+ 'view-as' => 'Προβολή Ως',
+ 'text' => 'Κείμενο',
+ 'download' => 'Λήψη',
+
+ 'documentation' => 'Τεκμηρίωση',
+ 'coming-soon' => 'Έρχεται σύντομα',
+ 'example' => 'Παράδειγμα',
+ 'back' => 'Πίσω',
+ 'value' => 'Τιμή',
+
+ 'successful-save' => 'Αποθηκεύτηκε με επιτυχία!',
+ 'failed-to-save' => 'Αποτυχία αποθήκευσης! Ελέγξτε τα δικαιώματα.',
+ 'permissions-info' => 'Δώστε στο "%s" δικαιώματα 0755 και ιδιοκτησία στον "%s" χρήστη.',
+
+ 'admin' => 'Διαχειριστής',
+ 'moderation' => 'Διαχείριση',
+ 'block-ip-addresses' => 'Φραγή IP Διευθύνσεων',
+ 'filter-url-queries' => 'Φιλτράρισμα URL Ερωτημάτων',
+ 'check-for-updates' => 'Έλεγχος για Ενημερώσεις',
+ 'settings' => 'Ρυθμίσεις',
+
+ 'admin-required' => 'Πρέπει να συνδεθείτε ως διαχειριστής',
+
+ 'blocklist-title' => 'Λίστα Φραγής IP Διευθύνσεων',
+ 'blocklist-sub' => 'Φραγή συγκεκριμένων IP διευθύνσεων',
+ 'blocklist-ip-tip' => 'IP Διεύθυνση ή κενό για να αφαιρεθεί',
+
+ 'url-queries-title' => 'Αγνοημένα URL Ερωτήματα',
+ 'url-queries-sub' => 'Φιλτράρετε ποια URL ερωτήματα να αγνοηθούν',
+ 'url-queries-name-tip' => 'Όνομα ερωτήματος ή κενό για να αφαιρεθεί',
+ 'url-queries-value-tip' => 'Τιμή ερωτήματος ή κενό για οποιαδήποτε τιμή',
+
+ 'settings-sub' => 'Αλλάξτε διάφορες ρυθμίσεις',
+ 'moderation-sub' => 'Αποστολή, επεξεργασία, έγκριση και διαγραφή σχολίων',
+
+ 'setting-language' => 'Γλώσσα',
+ 'setting-theme' => 'Θέμα',
+ 'setting-uses-moderation' => 'Τα σχόλια από απλούς χρήστες απαιτούν έγκριση',
+ 'setting-pends-user-edits' => 'Τα επεξεργασμένα σχόλια από απλούς χρήστες απαιτούν πρόσθετη διαχείριση',
+ 'setting-data-format' => 'Τύπος δεδομένων σχολίων',
+ 'setting-default-name' => 'Προεπιλεγμένο όνομα σχολιαστή',
+ 'setting-allows-images' => 'Να επιτρέπεται η προβολή εικόνων στα σχόλια',
+ 'setting-allows-login' => 'Να επιτρέπεται η σύνδεση στους χρήστες',
+ 'setting-allows-likes' => 'Να επιτρέπεται στους χρήστες να κάνουν like',
+ 'setting-allows-dislikes' => 'Να επιτρέπεται στους χρήστες να κάνουν dislike',
+ 'setting-uses-ajax' => 'Ενεργοποίηση ασύγχρονων χαρακτηριστικών JavaScript',
+ 'setting-collapses-interface' => 'Σύμπτυξη ολόκληρης της διεπαφής HashOver',
+ 'setting-collapses-comments' => 'Σύμπτυξη προσαρμόσιμου αριθμού σχολίων',
+ 'setting-collapse-limit' => 'Αριθμός σχολίων για σύμπτυξη',
+ 'setting-reply-mode' => 'Τύπος προβολής νημάτων σχολίων',
+ 'setting-stream-depth' => 'Αριθμός εσοχών απαντήσεων πριν η ροή γίνει επίπεδη',
+ 'setting-popularity-threshold' => 'Καθαρός αριθμός likes που χρειάζονται ώστε κάποιο σχόιο να γίνει δημοφιλές',
+ 'setting-popularity-limit' => 'Αριθμός δημοφιλών σχολίων προς προβολή',
+ 'setting-uses-markdown' => 'Ενεργοποίηση υποστήριξης Markdown',
+ 'setting-server-timezone' => 'Ζώνη ώρας εξυπηρετητή',
+ 'setting-uses-user-timezone' => 'Προβολή ημερομηνίας/ώρας στη ζώνη ώρας του χρήστη (JavaScript-mode)',
+ 'setting-uses-short-dates' => 'Ενεργοποίηση σύντομης ημερομηνίας/ώρας (παράδειγμα "1 ημέρα πριν")',
+ 'setting-time-format' => 'Μορφή ώρας, χρησιμοποιηστε "H:i" για 24ωρη μορφή',
+ 'setting-date-format' => 'Μορφή ημερομηνίας',
+ 'setting-displays-title' => 'Ενεργοποιήστε την προβολή του τίτλου σελίδας',
+ 'setting-form-position' => 'Θέση της κύριας φόρμας σχολίων',
+ 'setting-uses-auto-login' => 'Σύνδεσε αυτόματα τους χρήστες όταν αφήνουν σχόλια',
+ 'setting-shows-reply-count' => 'Προβολή αριθμού απαντήσεων ξεχωριστά από το σύνολο',
+ 'setting-count-includes-deleted' => 'Περίλαβε τα διεγραμμένα σχόλια στα σύνολα',
+ 'setting-icon-mode' => 'Τύπος προβολής εικονιδίου avatar',
+ 'setting-icon-size' => 'Μέγεθος εικονιδίου avatar',
+ 'setting-image-format' => 'Μορφή εικονιδιών και λοιπών εικόνων',
+ 'setting-uses-labels' => 'Προβολή επιγραφών πάνω από τα πεδία εισαγωγής κειμένου',
+ 'setting-uses-cancel-buttons' => 'Αν οι φόρμες θα έχουν κουμπιά ακύρωσης',
+ 'setting-appends-css' => 'Αυτόματη προσθήκη του HashOver CSS στη σελίδα',
+ 'setting-appends-rss' => 'Προσθήκη συνδέσμων HashOver RSS Ροής στη σελίδα',
+ 'setting-login-method' => 'Σύστημα σύνδεσης χρηστών',
+ 'setting-sets-cookies' => 'Ενεργοποίηση cookies',
+ 'setting-secure-cookies' => 'Χρήση ασφαλών HTTPS-only cookies',
+ 'setting-stores-ip-address' => 'Ενεργοποίηση αποθήκευσης των IP διευθύνσεων χρηστών',
+ 'setting-subscribes-user' => 'Οι χρήστες εγγράφονται στα μηνύματα ηλεκτρονικού ταχυδρομείου ειδοποιήσεων από προεπιλογή',
+ 'setting-allows-user-replies' => 'Όρισε το e-mail του χρήστη ως "Reply-To" σε ειδοποιήσεις απαντήσεων',
+ 'setting-noreply-email' => 'Διεύθυνση e-mail που θα χρησιμοποιείται όταν δεν παρέχεται e-mail',
+ 'setting-spam-batabase' => 'Τοποθεσία βάσης δεδομένων SPAM',
+ 'setting-spam-check-modes' => 'Τύποι υπό τους οποίους να γίνεται έλεγχος SPAM',
+ 'setting-gravatar-force' => 'Χρησιμοποίησε Gravatar εικόνες αντί για avatars',
+ 'setting-gravatar-default' => 'Προεπιλεγμένο Gravatar θέμα προς χρήση',
+ 'setting-minifies-javascript' => 'Ενεργοποίηση σύμπτυξης JavaScript',
+ 'setting-minify-level' => 'Επίπεδο σύμπτυξης JavaScript',
+ 'setting-allow-local-metadata' => 'Επίτρεψε την ενημέρωση των μεταδεδομένων σελίδων από το localhost'
+);
diff --git a/bootstrap/comments/backend/locales/en.php b/bootstrap/comments/backend/locales/en.php
new file mode 100644
index 0000000..fd5ac7f
--- /dev/null
+++ b/bootstrap/comments/backend/locales/en.php
@@ -0,0 +1,206 @@
+<?php
+
+// Copyright (C) 2015-2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// I, Jacob Barkdull, hereby release this work into the public domain.
+// This applies worldwide. If this is not legally possible, I grant any
+// entity the right to use this work for any purpose, without any
+// conditions, unless such conditions are required by law.
+
+
+// English text for forms, buttons, links, and tooltips
+$locale = array (
+ 'comment-form' => 'Type your comment here...',
+ 'reply-form' => 'Type your reply here...',
+ 'comment-formatting' => '',
+ 'accepted-format' => 'Accepted %s',
+ 'accepted-html' => '&lt;b&gt;, &lt;strong&gt;, &lt;u&gt;, &lt;i&gt;, &lt;em&gt;, &lt;s&gt;, &lt;big&gt;, &lt;small&gt;, &lt;sup&gt;, &lt;sub&gt;, &lt;pre&gt;, &lt;ul&gt;, &lt;ol&gt;, &lt;li&gt;, &lt;blockquote&gt;, &lt;code&gt; escapes HTML, URLs automagically become links, and [img]URL here[/img] will display an external image.',
+ 'accepted-markdown' => '**Bold**, _underline_, *italic*, ~~strikethrough~~, `highlight`, ```code``` escapes HTML. HTML and Markdown may be used together in your comment.',
+ 'post-button' => 'Post Comment',
+ 'login' => 'Login',
+ 'login-tip' => 'Login (optional)',
+ 'logout' => 'Logout',
+ 'be-first-name' => 'No comments yet.',
+ 'pending-name' => 'Pending...',
+ 'deleted-name' => 'Deleted...',
+ 'error-name' => 'Error...',
+ 'be-first-note' => 'Be the first to comment!',
+ 'pending-note' => 'This comment is pending approval.',
+ 'deleted-note' => 'This comment has been deleted.',
+ 'error-note' => 'Something went wrong. Could not retrieve this comment.',
+ 'options' => 'Options',
+ 'cancel' => 'Cancel',
+ 'reply-to-comment' => 'Reply to comment',
+ 'edit-your-comment' => 'Edit your comment',
+ 'optional' => 'Optional',
+ 'required' => 'Required',
+ 'name' => 'Name',
+ 'name-tip' => 'Name (%s)',
+ 'password' => 'Password',
+ 'password-tip' => 'Password (%s, allows you to edit or delete this comment)',
+ 'confirm-password' => 'Confirm Password',
+ 'email' => 'Email Address',
+ 'email-tip' => 'Email Address (%s, for e-mail notifications)',
+ 'website' => 'Website',
+ 'website-tip' => 'Website (%s)',
+ 'logged-in' => 'You have been successfully logged in!',
+ 'logged-out' => 'You have been successfully logged out!',
+ 'comment-needed' => 'You failed to enter a proper comment. Please try again.',
+ 'reply-needed' => 'You failed to enter a proper reply. Please try again.',
+ 'field-needed' => 'The "%s" field is required.',
+ 'post-fail' => 'Failure! You lack sufficient permission.',
+ 'comment-deleted' => 'Comment Deleted!',
+ 'post-reply' => 'Post Reply',
+ 'delete' => 'Delete',
+ 'permanently-delete' => 'Permanently Delete',
+ 'subscribe' => 'Notify me of replies',
+ 'subscribe-tip' => 'Subscribe to e-mail notifications',
+ 'edit-comment' => 'Edit comment',
+ 'status' => 'Status',
+ 'status-approved' => 'Approved',
+ 'status-pending' => 'Pending approval',
+ 'status-deleted' => 'Marked deleted',
+ 'save' => 'Save',
+ 'no-email-warning' => 'You will not receive notification of replies to your comment without supplying an e-mail.',
+ 'invalid-email' => 'The e-mail address you entered is invalid.',
+ 'delete-comment' => 'Are you sure you want to delete this comment?',
+ 'post-comment-on' => array ('Post a Comment', 'Post a comment on "%s"'),
+ 'popular-comments' => array ('Most Popular Comment', 'Most Popular Comments'),
+ 'showing-comments' => array ('Showing %d Comment', 'Showing %d Comments'),
+ 'count-link' => array ('%d Comment', '%d Comments'),
+ 'count-replies' => array ('%d counting reply', '%d counting replies'),
+ 'sort' => 'Sort',
+ 'sort-ascending' => 'In order',
+ 'sort-descending' => 'In reverse order',
+ 'sort-by-date' => 'Newest first',
+ 'sort-by-likes' => 'By likes',
+ 'sort-by-replies' => 'By replies',
+ 'sort-by-discussion' => 'By discussion',
+ 'sort-by-popularity' => 'By popularity',
+ 'sort-by-name' => 'By commenter',
+ 'sort-threads' => 'Threads',
+ 'thread' => 'In reply to %s',
+ 'thread-tip' => 'Jump to top of thread',
+ 'comments' => 'Comments',
+ 'replies' => 'Replies',
+ 'edit' => 'Edit',
+ 'reply' => 'Reply',
+ 'like' => array ('Like', 'Likes'),
+ 'liked' => 'Liked',
+ 'unlike' => 'Unlike',
+ 'like-comment' => '\'Like\' this comment',
+ 'liked-comment' => 'Unlike this comment',
+ 'dislike' => array ('Dislike', 'Dislikes'),
+ 'disliked' => 'Disliked',
+ 'dislike-comment' => '\'Dislike\' this comment',
+ 'disliked-comment' => 'You \'Disliked\' this comment',
+ 'commenter-tip' => 'You will not be notified via e-mail',
+ 'subscribed-tip' => 'will be notified via e-mail',
+ 'unsubscribed-tip' => 'is not subscribed to e-mail notifications',
+ 'show-other-comments' => array ('Show %d Other Comment', 'Show %d Other Comments'),
+ 'show-number-comments' => array ('Show %d Comment', 'Show %d Comments'),
+ 'date-time' => '%s \a\t %s',
+ 'date-years' => array ('%d year ago', '%d years ago'),
+ 'date-months' => array ('%d month ago', '%d months ago'),
+ 'date-days' => array ('%d day ago', '%d days ago'),
+ 'date-today' => '%s today',
+ 'date-day-names' => array ('Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'),
+ 'date-month-names' => array ('January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'),
+ 'untitled' => 'Untitled',
+ 'external-image-tip' => 'Click to view external image',
+ 'loading' => 'Loading...',
+ 'click-to-close' => 'Click to close',
+ 'hashover-comments' => 'HashOver Comments',
+ 'rss-feed' => 'RSS Feed',
+ 'source-code' => 'Source Code',
+
+ 'source-code-sub' => 'HashOver server-side source code viewer',
+ 'type' => 'Type',
+ 'path' => 'Path',
+ 'view-as' => 'View As',
+ 'text' => 'Text',
+ 'download' => 'Download',
+
+ 'documentation' => 'Documentation',
+ 'coming-soon' => 'Coming soon',
+ 'example' => 'Example',
+ 'back' => 'Back',
+ 'value' => 'Value',
+
+ 'successful-save' => 'Successfully saved!',
+ 'failed-to-save' => 'Failed to save! Check permissions.',
+ 'permissions-info' => 'Give "%s" permissions 0755 and ownership to the "%s" user.',
+
+ 'admin' => 'Admin',
+ 'moderation' => 'Moderation',
+ 'block-ip-addresses' => 'Block IP Addresses',
+ 'filter-url-queries' => 'Filter URL Queries',
+ 'check-for-updates' => 'Check for Updates',
+ 'settings' => 'Settings',
+
+ 'admin-required' => 'You must be logged as admin',
+
+ 'blocklist-title' => 'IP Address Blocklist',
+ 'blocklist-sub' => 'Block specific IP addresses',
+ 'blocklist-ip-tip' => 'IP Address or blank to remove',
+
+ 'url-queries-title' => 'Ignored URL Queries',
+ 'url-queries-sub' => 'Filter which URL queries should be ignored',
+ 'url-queries-name-tip' => 'Query name or blank to remove',
+ 'url-queries-value-tip' => 'Query value or blank for any value',
+
+ 'settings-sub' => 'Change various settings',
+ 'moderation-sub' => 'Post, edit, approve, and delete comments',
+
+ 'setting-language' => 'Language',
+ 'setting-theme' => 'Theme',
+ 'setting-uses-moderation' => 'Comments posted by normal users require moderation',
+ 'setting-pends-user-edits' => 'Comments edited by normal users require additional moderation',
+ 'setting-data-format' => 'Comment data format',
+ 'setting-default-name' => 'Default commenter name',
+ 'setting-allows-images' => 'Allow display of images in comments',
+ 'setting-allows-login' => 'Allow users to login',
+ 'setting-allows-likes' => 'Allow users to like comments',
+ 'setting-allows-dislikes' => 'Allow users to dislike comments',
+ 'setting-uses-ajax' => 'Enable asynchronous JavaScript features',
+ 'setting-collapses-interface' => 'Collapse entire HashOver user interface',
+ 'setting-collapses-comments' => 'Collapse a configurable number of comments',
+ 'setting-collapse-limit' => 'Number of comments to collapse',
+ 'setting-reply-mode' => 'Display mode of comment threads',
+ 'setting-stream-depth' => 'Number of reply indentions before stream is flattened',
+ 'setting-popularity-threshold' => 'Net number of likes a comment needs to be popular',
+ 'setting-popularity-limit' => 'Number of popular comments to display',
+ 'setting-uses-markdown' => 'Enable Markdown support',
+ 'setting-server-timezone' => 'Server timezone',
+ 'setting-uses-user-timezone' => 'Display dates/times in user\'s timezone (JavaScript-mode)',
+ 'setting-uses-short-dates' => 'Enable shorter dates/times (example "1 day ago")',
+ 'setting-time-format' => 'Time format, use "H:i" for 24-hour format',
+ 'setting-date-format' => 'Date format',
+ 'setting-displays-title' => 'Enable display of page title',
+ 'setting-form-position' => 'Position for primary comment form',
+ 'setting-uses-auto-login' => 'Automatically log users in when they post comments',
+ 'setting-shows-reply-count' => 'Display reply count separately from total count',
+ 'setting-count-includes-deleted' => 'Include deleted comments in comment counts',
+ 'setting-icon-mode' => 'Avatar icon display mode',
+ 'setting-icon-size' => 'Avatar icon size',
+ 'setting-image-format' => 'Format for icons and other images',
+ 'setting-uses-labels' => 'Display labels above inputs',
+ 'setting-uses-cancel-buttons' => 'Whether forms have cancel buttons',
+ 'setting-appends-css' => 'Automatically add HashOver CSS to page',
+ 'setting-appends-rss' => 'Add HashOver RSS Feed links to page',
+ 'setting-login-method' => 'User login system',
+ 'setting-sets-cookies' => 'Enable cookies',
+ 'setting-secure-cookies' => 'Use secure HTTPS-only cookies',
+ 'setting-stores-ip-address' => 'Enable storage of user IP addresses',
+ 'setting-subscribes-user' => 'Subscribe the user to e-mail notifications by default',
+ 'setting-allows-user-replies' => 'Set user e-mail as "Reply-To" in reply notifications',
+ 'setting-noreply-email' => 'E-mail address used when no e-mail is given',
+ 'setting-spam-batabase' => 'SPAM database location',
+ 'setting-spam-check-modes' => 'Modes to perform SPAM check under',
+ 'setting-gravatar-force' => 'Use themed Gravatar images instead of avatars',
+ 'setting-gravatar-default' => 'Default Gravatar theme to use',
+ 'setting-minifies-javascript' => 'Enable JavaScript minification',
+ 'setting-minify-level' => 'JavaScript minification level',
+ 'setting-allow-local-metadata' => 'Allow page metadata to be updated from localhost'
+);
diff --git a/bootstrap/comments/backend/locales/es.php b/bootstrap/comments/backend/locales/es.php
new file mode 100644
index 0000000..86b15ad
--- /dev/null
+++ b/bootstrap/comments/backend/locales/es.php
@@ -0,0 +1,206 @@
+<?php
+
+// Copyright (C) 2015-2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// I, Jacob Barkdull, hereby release this work into the public domain.
+// This applies worldwide. If this is not legally possible, I grant any
+// entity the right to use this work for any purpose, without any
+// conditions, unless such conditions are required by law.
+
+
+// Spanish text for forms, buttons, links, and tooltips
+$locale = array (
+ 'comment-form' => 'Escribe un comentario aquí...',
+ 'reply-form' => 'Escribe tu respuesta aquí...',
+ 'comment-formatting' => 'Formateo',
+ 'accepted-format' => '%s aceptado',
+ 'accepted-html' => '&lt;b&gt;, &lt;strong&gt;, &lt;u&gt;, &lt;i&gt;, &lt;em&gt;, &lt;s&gt;, &lt;big&gt;, &lt;small&gt;, &lt;sup&gt;, &lt;sub&gt;, &lt;pre&gt;, &lt;ul&gt;, &lt;ol&gt;, &lt;li&gt;, &lt;blockquote&gt;, &lt;code&gt; escapa HTML, las URLs se convierten automágicamente en enlaces, y [img]URL aquí[/img] mostrará una imagen externa.',
+ 'accepted-markdown' => '**Negrita**, _subrayado_, *cursiva*, ~~tachado~~, `resaltado`, ```código``` escapa HTML. Puedes usar HTML y Markdown juntos en tu comentario.',
+ 'post-button' => 'Publicar comentario',
+ 'login' => 'Iniciar sesión',
+ 'login-tip' => 'Iniciar sesión (opcional)',
+ 'logout' => 'Cerrar sesión',
+ 'be-first-name' => 'Sin comentarios aún.',
+ 'pending-name' => 'Pendiente...',
+ 'deleted-name' => 'Eliminado...',
+ 'error-name' => 'Error...',
+ 'be-first-note' => '¡Sé el primero en comentar!',
+ 'pending-note' => 'Este comentario está a la espera de ser aprobado.',
+ 'deleted-note' => 'Este comentario ha sido eliminado.',
+ 'error-note' => 'Algo salió mal. No se pudo recuperar este comentario.',
+ 'options' => 'Opciones',
+ 'cancel' => 'Cancelar',
+ 'reply-to-comment' => 'Responder al comentario',
+ 'edit-your-comment' => 'Editar tu comentario',
+ 'optional' => 'Opcional',
+ 'required' => 'Obligatorio',
+ 'name' => 'Nombre',
+ 'name-tip' => 'Nombre (%s)',
+ 'password' => 'Contraseña',
+ 'password-tip' => 'Contraseña (%s, te permite editar o eliminar tu comentario)',
+ 'confirm-password' => 'Confirmar contraseña',
+ 'email' => 'Correo electrónico',
+ 'email-tip' => 'Dirección de correo electrónico (para notificaciones, %s)',
+ 'website' => 'Sitio web',
+ 'website-tip' => 'Sitio web (%s)',
+ 'logged-in' => '¡Iniciaste sesión correctamente!',
+ 'logged-out' => '¡Cerraste sesión correctamente!',
+ 'comment-needed' => 'No hiciste ningún comentario válido. Vuelve a intentarlo.',
+ 'reply-needed' => 'No hiciste ninguna respuesta válida. Vuelve a intentarlo.',
+ 'field-needed' => 'El campo «%s» es obligatorio.',
+ 'post-fail' => '¡Error! No tienes permisos suficientes.',
+ 'comment-deleted' => '¡Comentario eliminado!',
+ 'post-reply' => 'Responder',
+ 'delete' => 'Borrar',
+ 'permanently-delete' => 'Borrar permanentemente',
+ 'subscribe' => 'Notificarme si hay respuestas',
+ 'subscribe-tip' => 'Suscribirse a las notificaciones por correo electrónico',
+ 'edit-comment' => 'Editar comentario',
+ 'status' => 'Estado',
+ 'status-approved' => 'Aprobado',
+ 'status-pending' => 'Esperando ser aprobado',
+ 'status-deleted' => 'Marcado como eliminado',
+ 'save' => 'Guardar',
+ 'no-email-warning' => 'No se te notificarán las respuestas a tu comentario si no escribes una dirección de correo electrónico.',
+ 'invalid-email' => 'La dirección de correo electrónico que has introducido no es válida.',
+ 'delete-comment' => '¿Seguro que quieres eliminar este comentario?',
+ 'post-comment-on' => array ('Publicar un comentario', 'Publicar un comentario en "%s"'),
+ 'popular-comments' => array ('Comentario más popular', 'Comentarios más populares'),
+ 'showing-comments' => array ('Mostrando un comentario', 'Mostrando %d comentarios'),
+ 'count-link' => array ('Un comentario', '%d comentarios'),
+ 'count-replies' => array ('Una respuesta', '%d respuestas'),
+ 'sort' => 'Ordenar',
+ 'sort-ascending' => 'En orden',
+ 'sort-descending' => 'En orden inverso',
+ 'sort-by-date' => 'El más reciente primero',
+ 'sort-by-likes' => 'Por «me gustas»',
+ 'sort-by-replies' => 'Por respuestas',
+ 'sort-by-discussion' => 'Por discusión',
+ 'sort-by-popularity' => 'Por popularidad',
+ 'sort-by-name' => 'Por autor del comentario',
+ 'sort-threads' => 'Hilos',
+ 'thread' => 'En respuesta a %s',
+ 'thread-tip' => 'Ir al inicio del hilo',
+ 'comments' => 'Comentarios',
+ 'replies' => 'Respuestas',
+ 'edit' => 'Editar',
+ 'reply' => 'Responder',
+ 'like' => array ('Me gusta', 'Me gusta'),
+ 'liked' => 'Me gusta',
+ 'unlike' => 'Ya no me gusta',
+ 'like-comment' => 'Marcar con «me gusta» este comentario',
+ 'liked-comment' => 'Quitar «me gusta»',
+ 'dislike' => array ('No me gusta', 'No me gusta'),
+ 'disliked' => 'No me gusta',
+ 'dislike-comment' => 'Marcar con «no me gusta» este comentario',
+ 'disliked-comment' => 'Quitar «no me gusta» de este comentario',
+ 'commenter-tip' => 'No se te notificará por correo electrónico',
+ 'subscribed-tip' => 'Se te notificará por correo electrónico',
+ 'unsubscribed-tip' => 'No estás suscrito a las notificaciones por correo electrónico',
+ 'show-other-comments' => array ('Mostrar un comentario más', 'Mostrar %d comentarios más'),
+ 'show-number-comments' => array ('Mostrar un comentario', 'Mostrar %d comentarios'),
+ 'date-time' => '%s \a \l\a\s %s',
+ 'date-years' => array ('Hace un año', 'Hace %d años'),
+ 'date-months' => array ('Hace un mes', 'Hace %d meses'),
+ 'date-days' => array ('Hace un día', 'Hace %d días'),
+ 'date-today' => 'Hoy a las %s',
+ 'date-day-names' => array ('domingo', 'lunes', 'martes', 'miércoles', 'jueves', 'viernes', 'sábado'),
+ 'date-month-names' => array ('enero', 'febrero', 'marzo', 'abril', 'aayo', 'junio', 'julio', 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre'),
+ 'untitled' => 'Sin título',
+ 'external-image-tip' => 'Clic para mostrar la imagen externa',
+ 'loading' => 'Cargando...',
+ 'click-to-close' => 'Clic para cerrar',
+ 'hashover-comments' => 'Comentarios de HashOver',
+ 'rss-feed' => 'Fuente RSS',
+ 'source-code' => 'Código fuente',
+
+ 'source-code-sub' => 'Visor de código fuente del lado del servidor HashOver',
+ 'type' => 'Tipo',
+ 'path' => 'Ruta',
+ 'view-as' => 'Ver como',
+ 'text' => 'Texto',
+ 'download' => 'Descargar',
+
+ 'documentation' => 'Documentación',
+ 'coming-soon' => 'Próximamente',
+ 'example' => 'Ejemplo',
+ 'back' => 'Atrás',
+ 'value' => 'Valor',
+
+ 'successful-save' => '¡Guardado con éxito!',
+ 'failed-to-save' => '¡Error al guardar! Compruebe los permisos.',
+ 'permissions-info' => 'Dar "%s" permisos 0755 y propiedad al usuario "%s".',
+
+ 'admin' => 'Admin',
+ 'moderation' => 'Moderación',
+ 'block-ip-addresses' => 'Bloquear direcciones IP',
+ 'filter-url-queries' => 'Filtrar consultas URL',
+ 'check-for-updates' => 'Buscar actualizaciones',
+ 'settings' => 'Configuración',
+
+ 'admin-required' => 'Debe estar registrado como administrador',
+
+ 'blocklist-title' => 'Lista de direcciones IP Lista de bloqueo',
+ 'blocklist-sub' => 'Bloquear direcciones IP específicas',
+ 'blocklist-ip-tip' => 'Dirección IP o espacio en blanco para eliminar',
+
+ 'url-queries-title' => 'Consultas URL ignoradas',
+ 'url-queries-sub' => 'Filtra qué consultas URL se deben ignorar',
+ 'url-queries-name-tip' => 'Nombre de la consulta o espacio en blanco para eliminar',
+ 'url-queries-value-tip' => 'Valor de consulta o espacio en blanco para cualquier valor',
+
+ 'settings-sub' => 'Cambiar varias configuraciones',
+ 'moderation-sub' => 'Publicar, editar, aprobar y eliminar comentarios',
+
+ 'setting-language' => 'Idioma',
+ 'setting-theme' => 'Tema',
+ 'setting-uses-moderation' => 'Los comentarios publicados por usuarios normales requieren moderación',
+ 'setting-pends-user-edits' => 'Los comentarios editados por usuarios normales requieren moderación adicional',
+ 'setting-data-format' => 'Formato de datos de comentarios',
+ 'setting-default-name' => 'Nombre de comentarista predeterminado',
+ 'setting-allows-images' => 'Permitir visualización de imágenes en comentarios',
+ 'setting-allows-login' => 'Permitir a los usuarios iniciar sesión',
+ 'setting-allows-likes' => 'Permitir a los usuarios que me gusten los comentarios',
+ 'setting-allows-dislikes' => 'Permitir que los usuarios no les gusten los comentarios',
+ 'setting-uses-ajax' => 'Habilitar funciones de JavaScript asíncronas',
+ 'setting-collapses-interface' => 'Contraer toda la interfaz de usuario de HashOver',
+ 'setting-collapses-comments' => 'Contraer una cantidad configurable de comentarios',
+ 'setting-collapse-limit' => 'Número de comentarios para colapsar',
+ 'setting-reply-mode' => 'Modo de visualización de los hilos de comentario',
+ 'setting-stream-depth' => 'Número de sangrías de respuesta antes de que la transmisión se aplana',
+ 'setting-popularity-threshold' => 'Cantidad neta de "Me gusta" que un comentario debe ser popular',
+ 'setting-popularity-limit' => 'Número de comentarios populares para mostrar',
+ 'setting-uses-markdown' => 'Habilitar compatibilidad con Markdown',
+ 'setting-server-timezone' => 'Zona horaria del servidor',
+ 'setting-uses-user-timezone' => 'Mostrar fechas / horas en la zona horaria del usuario (modo JavaScript)',
+ 'setting-uses-short-dates' => 'Habilitar fechas / horas más cortas (ejemplo "1 día atrás")',
+ 'setting-time-format' => 'Formato de tiempo, use "H:i" para el formato de 24 horas',
+ 'setting-date-format' => 'Formato de fecha',
+ 'setting-displays-title' => 'Habilita la visualización del título de la página',
+ 'setting-form-position' => 'Posición para el formulario de comentario primario',
+ 'setting-uses-auto-login' => 'Registra usuarios automáticamente cuando publican comentarios',
+ 'setting-shows-reply-count' => 'Mostrar recuento de respuestas por separado del recuento total',
+ 'setting-count-includes-deleted' => 'Incluir comentarios eliminados en conteos de comentarios',
+ 'setting-icon-mode' => 'Modo de visualización del icono de Avatar',
+ 'setting-icon-size' => 'Tamaño del icono de Avatar',
+ 'setting-image-format' => 'Formato para iconos y otras imágenes',
+ 'setting-uses-labels' => 'Mostrar etiquetas encima de las entradas',
+ 'setting-uses-cancel-buttons' => 'Si las formas tienen botones de cancelación',
+ 'setting-appends-css' => 'Agregar automáticamente CSS de HashOver a la página',
+ 'setting-appends-rss' => 'Agregar enlaces de Feed RSS de HashOver a la página',
+ 'setting-login-method' => 'Sistema de inicio de sesión de usuario',
+ 'setting-sets-cookies' => 'Habilitar cookies',
+ 'setting-secure-cookies' => 'Usar cookies seguras HTTPS-only',
+ 'setting-stores-ip-address' => 'Habilitar el almacenamiento de direcciones IP de usuario',
+ 'setting-subscribes-user' => 'Suscribir al usuario a notificaciones por correo electrónico de forma predeterminada',
+ 'setting-allows-user-replies' => 'Establecer el correo electrónico del usuario como "Responder a" en las notificaciones de respuesta',
+ 'setting-noreply-email' => 'Dirección de correo electrónico utilizada cuando no se proporciona un correo electrónico',
+ 'setting-spam-batabase' => 'Ubicación de la base de datos SPAM',
+ 'setting-spam-check-modes' => 'Modos para realizar la comprobación de SPAM debajo de',
+ 'setting-gravatar-force' => 'Usa imágenes temáticas de Gravatar en lugar de avatares',
+ 'setting-gravatar-default' => 'Tema de Gravatar predeterminado para usar',
+ 'setting-minifies-javascript' => 'Habilitar la minificación de JavaScript',
+ 'setting-minify-level' => 'Nivel de minificación de JavaScript',
+ 'setting-allow-local-metadata' => 'Permitir que los metadatos de la página se actualicen desde localhost'
+);
diff --git a/bootstrap/comments/backend/locales/fa.php b/bootstrap/comments/backend/locales/fa.php
new file mode 100644
index 0000000..d3da784
--- /dev/null
+++ b/bootstrap/comments/backend/locales/fa.php
@@ -0,0 +1,206 @@
+<?php
+
+// Copyright (C) 2015-2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// I, Jacob Barkdull, hereby release this work into the public domain.
+// This applies worldwide. If this is not legally possible, I grant any
+// entity the right to use this work for any purpose, without any
+// conditions, unless such conditions are required by law.
+
+
+// Persian text for forms, buttons, links, and tooltips
+$locale = array (
+ 'comment-form' => 'دیدگاه خود را یادداشت کنید...',
+ 'reply-form' => 'پاسخ خود را یادداشت کنید...',
+ 'comment-formatting' => 'قالب متن',
+ 'accepted-format' => 'فرمت پذیرش %s',
+ 'accepted-html' => '&lt;b&gt;, &lt;strong&gt;, &lt;u&gt;, &lt;i&gt;, &lt;em&gt;, &lt;s&gt;, &lt;big&gt;, &lt;small&gt;, &lt;sup&gt;, &lt;sub&gt;, &lt;pre&gt;, &lt;ul&gt;, &lt;ol&gt;, &lt;li&gt;, &lt;blockquote&gt;, &lt;code&gt; escapes HTML, URLs automagically become links, and [img]URL here[/img] will display an external image.',
+ 'accepted-markdown' => '**Bold**, _underline_, *italic*, ~~strikethrough~~, `highlight`, ```code``` escapes HTML. HTML and Markdown may be used together in your comment.',
+ 'post-button' => 'ثبت دیدگاه',
+ 'login' => 'ورود',
+ 'login-tip' => 'ورود اختیاری است!',
+ 'logout' => 'خارج شوید',
+ 'be-first-name' => 'هنوز دیدگاهی ثبت نشده است.',
+ 'pending-name' => 'انتظار...',
+ 'deleted-name' => 'حذف شده...',
+ 'error-name' => 'خطا...',
+ 'be-first-note' => 'اولین باشید!',
+ 'pending-note' => 'این دیدگاه در انتظار تایید است.',
+ 'deleted-note' => 'این دیدگاه بنابه دلایلی حذف شده است.',
+ 'error-note' => 'مشکلی در سیستم به وجود آمده است. دریافت پیام ممکن نیست.',
+ 'options' => 'اختیارات',
+ 'cancel' => 'لغو',
+ 'reply-to-comment' => 'پاسخ به این دیدگاه',
+ 'edit-your-comment' => 'دیدگاه خود را ویرایش کنید',
+ 'optional' => 'اختیاری',
+ 'required' => 'لازم',
+ 'name' => 'نام',
+ 'name-tip' => 'نام (%s)',
+ 'password' => 'رمز عبور',
+ 'password-tip' => 'رمز ورود (%s, با وارد کردن رمز عبور می‌توانید دیدگاه خود را حذف یا ویرایش کنید!)',
+ 'confirm-password' => 'تایید رمز عبور',
+ 'email' => 'آدرس ایمیل',
+ 'email-tip' => 'آدرس ایمیل (%s, جهت اطلاع یافتن از پاسخ‌ها)',
+ 'website' => 'وب‌سایت',
+ 'website-tip' => 'وب‌سایت (%s)',
+ 'logged-in' => 'شما با موفقیت وارد شدید!',
+ 'logged-out' => 'شما با موفقیت خارج شدید!',
+ 'comment-needed' => 'ابتدا دیدگاه خود را نوشته! و دوباره سعی کنید.',
+ 'reply-needed' => 'ابتدا پاسخ خود را نوشته! و دوباره سعی کنید.',
+ 'field-needed' => '"%s" نمی‌تواند خالی باشد!',
+ 'post-fail' => 'متاسفانه شما دسترسی لازم را ندارید.',
+ 'comment-deleted' => 'دیدگاه حذف شد!',
+ 'post-reply' => 'درج پاسخ',
+ 'delete' => 'حذف',
+ 'permanently-delete' => 'حذف دائمی',
+ 'subscribe' => 'از پاسخ‌ها باخبرم کن',
+ 'subscribe-tip' => 'اشتراک در خبرنامه دیدگاه‌ها',
+ 'edit-comment' => 'ویرایش دیدگاه',
+ 'status' => 'وضعیت',
+ 'status-approved' => 'تایید شده',
+ 'status-pending' => 'در انتظار تایید',
+ 'status-deleted' => 'حذف',
+ 'save' => 'ذذخیره',
+ 'no-email-warning' => 'اگر ایمیل خود را وارد نکنید دریافت پاسخ‌ها از طریق ایمیل ممکن نخواهد بود.',
+ 'invalid-email' => 'ایمیل وارد شده صحیح نیست!',
+ 'delete-comment' => 'واقعا می‌خواهید این دیدگاه را حذف کنید?',
+ 'post-comment-on' => array ('با دیدگاه ارزشمند خود ما را را در ارائه هرچه بهتر خدمات یاری نمایید!', 'نوشتن دیدگاه با عنوان "%s"'),
+ 'popular-comments' => array ('دیدگاه‌های معروف', 'دیدگاه‌های معروف'),
+ 'showing-comments' => array ('نمایش دیدگاه‌ها (%d)', 'نمایش دیدگاه‌ها (%d)'),
+ 'count-link' => array ('%d دیدگاه', '%d دیدگاه'),
+ 'count-replies' => array ('%d پاسخ‌ها', '%d پاسخ‌ها'),
+ 'sort' => 'چینش بر اساس',
+ 'sort-ascending' => 'ترتیب ثبت',
+ 'sort-descending' => 'عکس ترتیب ثبت',
+ 'sort-by-date' => 'جدیدترین‌ها',
+ 'sort-by-likes' => 'پسندیدن',
+ 'sort-by-replies' => 'پاسخ‌ها',
+ 'sort-by-discussion' => 'گفتگوها',
+ 'sort-by-popularity' => 'معروفیت',
+ 'sort-by-name' => 'دیدگاه دهنده',
+ 'sort-threads' => 'دیدگاه‌ها',
+ 'thread' => 'در پاسخ به %s',
+ 'thread-tip' => 'برو به اولین دیدگاه',
+ 'comments' => 'دیدگاه‌ها',
+ 'replies' => 'پاسخ‌ها',
+ 'edit' => 'ویرایش',
+ 'reply' => 'پاسخ',
+ 'like' => array ('پسند', 'پسند'),
+ 'liked' => 'پسند شد',
+ 'unlike' => 'ناپسند',
+ 'like-comment' => 'این دیدگاه را می‌پسندم',
+ 'liked-comment' => 'این دیدگاه را نمی‌پسندم',
+ 'dislike' => array ('ناپسند', 'ناپسند'),
+ 'disliked' => 'نپسندیدم',
+ 'dislike-comment' => 'این دیدگاه را نمی‌پسندم',
+ 'disliked-comment' => 'این دیدگاه را نپسندیدم',
+ 'commenter-tip' => 'از طریق ایمیل باخبر می‌شود.',
+ 'subscribed-tip' => 'از طریق ایمیل باخبر می‌شود',
+ 'unsubscribed-tip' => 'از طریق ایمیل باخبر نمی‌شود.',
+ 'show-other-comments' => array ('نمایش دیگر دیدگاه‌ها (%d) ', 'نمایش دیگر دیدگاه‌ها (%d)'),
+ 'show-number-comments' => array ('نمایش دیدگاه‌ها(%d)', 'نمایش دیدگاه‌ها(%d)'),
+ 'date-time' => '%s \a\t %s',
+ 'date-years' => array ('%d سال قبل', '%d سال قبل'),
+ 'date-months' => array ('%d ماه قبل', '%d ماه قبل'),
+ 'date-days' => array ('%d روز قبل', '%d روز قبل'),
+ 'date-today' => '%s امروز',
+ 'date-day-names' => array ('یکشنبه', 'دوشنبه', 'سه‌شنبه', 'چهارشنبه', 'پمج‌شنبه', 'جمعه', 'شنبه'),
+ 'date-month-names' => array ('ژانویه', 'فوریه', 'مارس', 'آوریل', 'مه', 'ژوئن', 'ژوئیه', 'اوت', 'سپتامبر', 'اکتبر', 'نوامبر', 'دسامبر'),
+ 'untitled' => 'بدون عنوان',
+ 'external-image-tip' => 'برای مشاهده عکس کلیک کنید.',
+ 'loading' => 'در حال بازگذاری...',
+ 'click-to-close' => 'ببندید',
+ 'hashover-comments' => 'HashOver Comments',
+ 'rss-feed' => 'RSS Feed',
+ 'source-code' => 'Source Code',
+
+ 'source-code-sub' => 'بازخورد کد منبع جانبی سرور HashOver',
+ 'type' => 'تایپ کنید',
+ 'path' => 'مسیر',
+ 'view-as' => 'نمایش به صورت',
+ 'text' => 'متن',
+ 'download' => 'دانلود',
+
+ 'documentation' => 'مستندات',
+ 'coming-soon' => 'به زودی',
+ 'example' => 'مثال',
+ 'back' => 'بازگشت',
+ 'value' => 'ارزش',
+
+ 'successful-save' => 'موفقیت آمیز!',
+ 'failed-to-save' => 'ذخیره نشد بررسی مجوز. ',
+ 'permissions-info' => 'اجازه"%s" مجوز 0755 و مالکیت به کاربر"%s".',
+
+ 'admin' => 'مدیریت',
+ 'moderation' => 'مديريت',
+ 'block-ip-addresses' => 'بلوک آدرس های IP',
+ 'filter-url-queries' => 'جستجوگر URL فیلتر',
+ 'check-for-updates' => 'بررسی برای به روز رسانی',
+ 'settings' => 'تنظیمات',
+
+ 'admin-required' => 'شما باید به عنوان مدیر وارد شوید',
+
+ 'blocklist-title' => 'لیست آدرس IP آدرس',
+ 'blocklist-sub' => 'بلوک آدرس های خاص IP',
+ 'blocklist-ip-tip' => 'آدرس IP یا خالی برای حذف',
+
+ 'url-queries-title' => 'نظرسنجی های URL نادیده گرفته شده',
+ 'url-queries-sub' => 'فیلتر کردن نمایشهای URL باید نادیده گرفته شود',
+ 'url-queries-name-tip' => 'نام پرس و جو یا خالی برای حذف',
+ 'url-queries-value-tip' => 'مقدار درخواست یا خالی برای هر مقدار',
+
+ 'settings-sub' => 'تغییر تنظیمات مختلف',
+ 'moderation-sub' => 'ارسال، ویرایش، تایید و حذف نظرات',
+
+ 'setting-language' => 'زبان',
+ 'setting-theme' => 'تم',
+ 'setting-uses-moderation' => 'نظرات ارسال شده توسط کاربران عادی نیاز به نظارت دارند',
+ 'setting-pends-user-edits' => 'نظرات ویرایش شده توسط کاربران عادی نیاز به نظارت بیشتری دارند',
+ 'setting-data-format' => 'فرمت داده اظهار نظر',
+ 'setting-default-name' => 'نام پیش فرض نظر دهنده',
+ 'setting-allows-images' => 'اجازه نمایش تصاویر در نظرات',
+ 'setting-allows-login' => 'اجازه کاربران برای ورود',
+ 'setting-allows-likes' => 'اجازه دادن به کاربران برای دوستداشتن نظر',
+ 'setting-allows-dislikes' => 'اجازه دادن به کاربران برای نادیده گرفتن نظرات',
+ 'setting-uses-ajax' => 'فعال کردن ویژگی های جاوا اسکریپت',
+ 'setting-collapses-interface' => 'سقوط کل رابط کاربر HashOver',
+ 'setting-collapses-comments' => 'تعداد قابل تنظیم تعدیلات را از بین ببرید',
+ 'setting-collapse-limit' => 'تعداد نظرات به سقوط',
+ 'setting-reply-mode' => 'حالت نمایش از موضوعات نظر',
+ 'setting-stream-depth' => 'تعداد قطعه پاسخ قبل از جریان مسطح است',
+ 'setting-popularity-threshold' => 'تعداد خالی از نظرات مورد نظر باید محبوب باشد',
+ 'setting-popularity-limit' => 'تعداد نظرات محبوب برای نمایش',
+ 'setting-uses-markdown' => 'فعال کردن پشتیبانی از نشانه گذاری',
+ 'setting-server-timezone' => 'منطقه زمانی سرور',
+ 'setting-uses-user-timezone' => 'نمایش زمان / زمان در منطقه زمانی کاربر (حالت جاوا اسکریپت)',
+ 'setting-uses-short-dates' => 'تاریخ / زمان کوتاهتر را فعال کنید (مثلا "1 روز قبل")',
+ 'setting-time-format' => 'فرمت زمان، استفاده از "H:i" برای فرمت 24 ساعته',
+ 'setting-date-format' => 'فرمت تاریخ',
+ 'setting-displays-title' => 'نمایش صفحه عنوان را فعال کنید',
+ 'setting-form-position' => 'موقعیت برای فرم نظر اولیه',
+ 'setting-uses-auto-login' => 'به طور خودکار کاربران را در هنگام ارسال نظر وارد کنید',
+ 'setting-shows-reply-count' => 'نمایش پاسخ به طور جداگانه از کل تعداد',
+ 'setting-count-includes-deleted' => 'شامل نظرات حذف شده در تعداد نظرات',
+ 'setting-icon-mode' => 'نمایش حالت نماد آواتار',
+ 'setting-icon-size' => 'اندازه آیکون آواتار',
+ 'setting-image-format' => 'فرمت برای آیکون ها و تصاویر دیگر',
+ 'setting-uses-labels' => 'نمایش برچسب ها در بالای ورودی ها',
+ 'setting-uses-cancel-buttons' => 'آیا شکل دکمه ها را لغو می کند',
+ 'setting-appends-css' => 'به صورت خودکار HashOver CSS را به صفحه اضافه کنید',
+ 'setting-appends-rss' => 'افزودن پیوندهای HashOver RSS Feed به صفحه',
+ 'setting-login-method' => 'سیستم ورودی کاربر',
+ 'setting-sets-cookies' => 'فعال کردن کوکی ها',
+ 'setting-secure-cookies' => 'استفاده از کوکیهای امن HTTPS تنها',
+ 'setting-stores-ip-address' => 'ذخیره سازی آدرس های IP کاربر را فعال کنید',
+ 'setting-subscribes-user' => 'به طور پیشفرض کاربر را به ایمیل اعلان مشترک کنید',
+ 'setting-allows-user-replies' => 'ایمیل کاربر را به عنوان "پاسخ به" در پیام های پاسخ تنظیم کنید',
+ 'setting-noreply-email' => 'آدرس ایمیل مورد استفاده زمانی که هیچ ایمیل داده نمی شود',
+ 'setting-spam-batabase' => 'محل پایگاه داده اشخاص',
+ 'setting-spam-check-modes' => 'حالت برای انجام چک SPAM زیر',
+ 'setting-gravatar-force' => 'استفاده از تصاویر گراواتور با مضمون به جای آواتار ها',
+ 'setting-gravatar-default' => 'پیش فرض Gravatar تم برای استفاده',
+ 'setting-minifies-javascript' => 'فعال کردن جاوا اسکریپت',
+ 'setting-minify-level' => 'سطح معادن جاوا اسکریپت',
+ 'setting-allow-local-metadata' => 'اجازه دادن به متادیتای صفحه را از localhost به روز شود'
+);
diff --git a/bootstrap/comments/backend/locales/fr.php b/bootstrap/comments/backend/locales/fr.php
new file mode 100644
index 0000000..a25a8d5
--- /dev/null
+++ b/bootstrap/comments/backend/locales/fr.php
@@ -0,0 +1,213 @@
+<?php
+
+// Copyright (C) 2015-2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// I, Jacob Barkdull, hereby release this work into the public domain.
+// This applies worldwide. If this is not legally possible, I grant any
+// entity the right to use this work for any purpose, without any
+// conditions, unless such conditions are required by law.
+//
+// French fixed by Stéphane Mourey
+//
+// I, Stéphane Mourey, hereby release this work into the public domain.
+// This applies worldwide. If this is not legally possible, I grant any
+// entity the right to use this work for any purpose, without any
+// conditions, unless such conditions are required by law.
+
+
+// French text for forms, buttons, links, and tooltips
+$locale = array (
+ 'comment-form' => 'Écrivez ici votre commentaire...',
+ 'reply-form' => 'Écrivez ici votre réponse...',
+ 'comment-formatting' => 'Formats',
+ 'accepted-format' => '%s accepté',
+ 'accepted-html' => '&lt;b&gt;, &lt;strong&gt;, &lt;u&gt;, &lt;i&gt;, &lt;em&gt;, &lt;s&gt;, &lt;big&gt;, &lt;small&gt;, &lt;sup&gt;, &lt;sub&gt;, &lt;pre&gt;, &lt;ul&gt;, &lt;ol&gt;, &lt;li&gt;, &lt;blockquote&gt;, &lt;code&gt; échappe le HTML, les URLs sont transformées en liens, et [img]URL ici[/img] fait apparaître une image externe.',
+ 'accepted-markdown' => '**Gras**, _souligné_, *italique*, ~~barré~~, `souligné`, ```code``` échappe au format HTML. HTML et Markdown peuvent être utilisés ensemble dans votre commentaire.',
+ 'post-button' => 'Publier ce commentaire',
+ 'login' => 'Identifiant',
+ 'login-tip' => 'Identifiant (optionnel)',
+ 'logout' => 'Déconnexion',
+ 'be-first-name' => 'Aucun commentaire pour l\'instant.',
+ 'pending-name' => 'En attente...',
+ 'deleted-name' => 'Supprimé...',
+ 'error-name' => 'Erreur...',
+ 'be-first-note' => 'Soyez le premier à commenter !',
+ 'pending-note' => 'Ce commentaire est en attente d\'approbation.',
+ 'deleted-note' => 'Ce commentaire a été supprimé.',
+ 'error-note' => 'Quelque chose a mal tourné. Impossible de récupérer ce commentaire.',
+ 'options' => 'Options',
+ 'cancel' => 'Annuler',
+ 'reply-to-comment' => 'Répondre au commentaire',
+ 'edit-your-comment' => 'Éditer votre commentaire',
+ 'optional' => 'Optionnel',
+ 'required' => 'Obligatoire',
+ 'name' => 'Nom',
+ 'name-tip' => 'Nom (%s)',
+ 'password' => 'Mot de passe',
+ 'password-tip' => 'Mot de passe (%s, vous permettra d\'éditer ou de supprimer ce commentaire)',
+ 'confirm-password' => 'Confirmer le mot de passe',
+ 'email' => 'Adresse E-mail',
+ 'email-tip' => 'Adresse E-mail (%s, pour les notifications e-mail)',
+ 'website' => 'Site Internet',
+ 'website-tip' => 'Site Internet (%s)',
+ 'logged-in' => 'Vous vous êtes connecté avec succès !',
+ 'logged-out' => 'Vous vous êtes deconnecté avec succès !',
+ 'comment-needed' => 'Vous avez échoué à entrer un commentaire approprié. Veuillez réessayer.',
+ 'reply-needed' => 'Vous avez échoué à entrer une réponse appropriée. Veuillez réessayer.',
+ 'field-needed' => 'Le champ "%s" est obligatoire.',
+ 'post-fail' => 'Impossible de publier ce commentaire ! Vous n\'avez pas les permissions suffisantes.',
+ 'comment-deleted' => 'Commentaire supprimé !',
+ 'post-reply' => 'Publier cette réponse',
+ 'delete' => 'Supprimer',
+ 'permanently-delete' => 'Supprimer Définitivement',
+ 'subscribe' => 'Avertissez-moi des réponses',
+ 'subscribe-tip' => 'Souscrire aux notifications par e-mail',
+ 'edit-comment' => 'Éditer ce commentaire',
+ 'status' => 'Statut',
+ 'status-approved' => 'Approuvé',
+ 'status-pending' => 'En attente d\'approbation',
+ 'status-deleted' => 'Marqué supprimé',
+ 'save' => 'Enregistrer',
+ 'no-email-warning' => 'Vous ne recevrez pas de notifications en cas de réponse si vous ne fournissez pas d\'e-mail.',
+ 'invalid-email' => 'L\'adresse e-mail que vous avez entrée n\'est pas valide.',
+ 'delete-comment' => 'Confirmez-vous la suppression de ce commentaire ?',
+ 'post-comment-on' => array ('Poster un Commentaire', 'Poster un Commentaire sur "%s"'),
+ 'popular-comments' => array ('Commentaire le Plus Populaire', 'Commentaires les Plus Populaires'),
+ 'showing-comments' => array ('%d Commentaire Affiché', '%d Commentaires Affichés'),
+ 'count-link' => array ('%d Commentaire', '%d Commentaires'),
+ 'count-replies' => array ('%d compter réponse', '%d compter réponses'),
+ 'sort' => 'Trier',
+ 'sort-ascending' => 'Dans l\'ordre',
+ 'sort-descending' => 'Dans l\'ordre inverse',
+ 'sort-by-date' => 'La plus récente en premier',
+ 'sort-by-likes' => 'Par aiments',
+ 'sort-by-replies' => 'Par réponses',
+ 'sort-by-discussion' => 'Par discussion',
+ 'sort-by-popularity' => 'Par popularité',
+ 'sort-by-name' => 'Par commentateur',
+ 'sort-threads' => 'Fils',
+ 'thread' => 'En réponse à %s',
+ 'thread-tip' => 'Aller en haut de la discussion',
+ 'comments' => 'Commentaires',
+ 'replies' => 'Réponses',
+ 'edit' => 'Éditer',
+ 'reply' => 'Répondre',
+ 'like' => array ('J\'aime', 'Aiments'),
+ 'liked' => 'J\'aime',
+ 'unlike' => 'Défaire',
+ 'like-comment' => '\'J\'aime\' ce commentaire',
+ 'liked-comment' => 'Défaire \'J\'aime\'',
+ 'dislike' => array ('N\'aime pas', 'N\'aiment pas'),
+ 'disliked' => 'N\'aime pas',
+ 'dislike-comment' => 'Je n\'aime pas ce commentaire',
+ 'disliked-comment' => 'Vous n\'aimez pas ce commentaire',
+ 'commenter-tip' => 'Vous serez notifié par e-mail',
+ 'subscribed-tip' => 'sera notifié par e-mail',
+ 'unsubscribed-tip' => 'n\'a pas souscrit aux notifications',
+ 'show-other-comments' => array ('Afficher %d Autre Commentaire', 'Afficher %d Autres Commentaires'),
+ 'show-number-comments' => array ('Afficher %d Commentaire', 'Afficher %d Commentaires'),
+ 'date-time' => '%s \à %s',
+ 'date-years' => array ('Il y a %d an', 'Il y a %d ans'),
+ 'date-months' => array ('Il y a %d un mois', 'Il y a %d mois'),
+ 'date-days' => array ('Il y a %d jour', 'Il y a %d jours'),
+ 'date-today' => '%s aujourd\'hui',
+ 'date-day-names' => array ('Dimanche', 'Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi', 'Samedi'),
+ 'date-month-names' => array ('Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'),
+ 'untitled' => 'Sans titre',
+ 'external-image-tip' => 'Cliquez pour voir image externe',
+ 'loading' => 'Chargement...',
+ 'click-to-close' => 'Cliquez pour fermer',
+ 'hashover-comments' => 'HashOver Commentaires',
+ 'rss-feed' => 'Flux RSS',
+ 'source-code' => 'Source Code',
+
+ 'source-code-sub' => 'HashOver visionneuse de code source côté serveur',
+ 'type' => 'Type',
+ 'path' => 'Chemin',
+ 'view-as' => 'Voir comme',
+ 'text' => 'Texte',
+ 'download' => 'Télécharger',
+
+ 'documentation' => 'Documentation',
+ 'coming-soon' => 'Prochainement',
+ 'example' => 'Exemple',
+ 'back' => 'Retour',
+ 'value' => 'Valeur',
+
+ 'successful-save' => 'Sauvegardé avec succès!',
+ 'failed-to-save' => 'Impossible d\'enregistrer! Vérifiez les autorisations.',
+ 'permissions-info' => 'Donne "%s" les permissions 0755 et la propriété à l\'utilisateur "%s".',
+
+ 'admin' => 'Admin',
+ 'moderation' => 'Modération',
+ 'block-ip-addresses' => 'Bloquer les adresses IP',
+ 'filter-url-queries' => 'Filtrer les requêtes URL',
+ 'check-for-updates' => 'Vérifier les mises à jour',
+ 'settings' => 'Paramètres',
+
+ 'admin-required' => 'Vous devez être connecté en tant qu\'admin',
+
+ 'blocklist-title' => 'Liste d\'adresses IP bloquées',
+ 'blocklist-sub' => 'Bloquer les adresses IP spécifiques',
+ 'blocklist-ip-tip' => 'Adresse IP ou vide à supprimer',
+
+ 'url-queries-title' => 'Requêtes URL ignorées',
+ 'url-queries-sub' => 'Filtrer quelles requêtes d\'URL doivent être ignorées',
+ 'url-queries-name-tip' => 'Nom de la requête ou vide à supprimer',
+ 'url-queries-value-tip' => 'Valeur de requête ou vide pour toute valeur',
+
+ 'settings-sub' => 'Modifier les différents paramètres',
+ 'moderation-sub' => 'Publier, éditer, approuver et supprimer les commentaires',
+
+ 'setting-language' => 'Langue',
+ 'setting-theme' => 'Thème',
+ 'setting-uses-moderation' => 'Les commentaires postés par les utilisateurs normaux nécessitent une modération',
+ 'setting-pends-user-edits' => 'Les commentaires édités par des utilisateurs normaux nécessitent une modération supplémentaire',
+ 'setting-data-format' => 'Format des données de commentaire',
+ 'setting-default-name' => 'Nom du commentateur par défaut',
+ 'setting-allows-images' => 'Autoriser l\'affichage des images dans les commentaires',
+ 'setting-allows-login' => 'Autoriser les utilisateurs à se connecter',
+ 'setting-allows-likes' => 'Autoriser les utilisateurs à aimer les commentaires',
+ 'setting-allows-dislikes' => 'Autoriser les utilisateurs à ne pas aimer les commentaires',
+ 'setting-uses-ajax' => 'Activer les fonctionnalités JavaScript asynchrones',
+ 'setting-collapses-interface' => 'Réduire l\'intégralité de l\'interface utilisateur HashOver',
+ 'setting-collapses-comments' => 'Réduire un nombre configurable de commentaires',
+ 'setting-collapse-limit' => 'Nombre de commentaires à réduire',
+ 'setting-reply-mode' => 'Mode d\'affichage des fils de commentaires',
+ 'setting-stream-depth' => 'Nombre d\'indentions de réponse avant que le flux ne soit aplati',
+ 'setting-popularity-threshold' => 'Nombre net de likes un commentaire doit être populaire',
+ 'setting-popularity-limit' => 'Nombre de commentaires populaires à afficher',
+ 'setting-uses-markdown' => 'Activer le support de Markdown',
+ 'setting-server-timezone' => 'Fuseau horaire du serveur',
+ 'setting-uses-user-timezone' => 'Afficher les dates / heures dans le fuseau horaire de l\'utilisateur (mode JavaScript)',
+ 'setting-uses-short-dates' => 'Activer des dates / heures plus courtes (exemple "il y a 1 jour")',
+ 'setting-time-format' => 'Format de l\'heure, utilisez "H:i" pour le format 24 heures',
+ 'setting-date-format' => 'Format de date',
+ 'setting-displays-title' => 'Activer l\'affichage du titre de la page',
+ 'setting-form-position' => 'Position pour le formulaire de commentaire principal',
+ 'setting-uses-auto-login' => 'Consigner automatiquement les utilisateurs lorsqu\'ils publient des commentaires',
+ 'setting-shows-reply-count' => 'Afficher le nombre de réponses séparément du nombre total',
+ 'setting-count-includes-deleted' => 'Inclure les commentaires supprimés dans le compte des commentaires',
+ 'setting-icon-mode' => 'Mode d\'affichage de l\'avatar',
+ 'setting-icon-size' => 'Taille de l\'icône de l\'avatar',
+ 'setting-image-format' => 'Format pour les icônes et autres images',
+ 'setting-uses-labels' => 'Afficher les étiquettes au-dessus des entrées',
+ 'setting-uses-cancel-buttons' => 'Si les formulaires ont des boutons d\'annulation',
+ 'setting-appends-css' => 'Ajouter automatiquement CSS HashOver à la page',
+ 'setting-appends-rss' => 'Ajouter des liens HashOver RSS Feed à la page',
+ 'setting-login-method' => 'Système de connexion utilisateur',
+ 'setting-sets-cookies' => 'Activer les cookies',
+ 'setting-secure-cookies' => 'Utiliser des cookies HTTPS-only sécurisés',
+ 'setting-stores-ip-address' => 'Activer le stockage des adresses IP des utilisateurs',
+ 'setting-subscribes-user' => 'Abonnez l\'utilisateur aux notifications par courrier électronique par défaut',
+ 'setting-allows-user-replies' => 'Définir l\'adresse e-mail de l\'utilisateur comme "Répondre à" dans les notifications de réponse',
+ 'setting-noreply-email' => 'Adresse e-mail utilisée si aucun e-mail n\'est donné',
+ 'setting-spam-batabase' => 'Emplacement de la base de données SPAM',
+ 'setting-spam-check-modes' => 'Modes pour effectuer un test de SPAM sous',
+ 'setting-gravatar-force' => 'Utilise des images gravatariques à la place des avatars',
+ 'setting-gravatar-default' => 'Thème Gravatar par défaut à utiliser',
+ 'setting-minifies-javascript' => 'Activer la minification JavaScript',
+ 'setting-minify-level' => 'Niveau de minimisation JavaScript',
+ 'setting-allow-local-metadata' => 'Autoriser la mise à jour des métadonnées de page depuis localhost'
+);
diff --git a/bootstrap/comments/backend/locales/jp.php b/bootstrap/comments/backend/locales/jp.php
new file mode 100644
index 0000000..3686ddc
--- /dev/null
+++ b/bootstrap/comments/backend/locales/jp.php
@@ -0,0 +1,206 @@
+<?php
+
+// Copyright (C) 2015-2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// I, Jacob Barkdull, hereby release this work into the public domain.
+// This applies worldwide. If this is not legally possible, I grant any
+// entity the right to use this work for any purpose, without any
+// conditions, unless such conditions are required by law.
+
+
+// Japanese text for forms, buttons, links, and tooltips
+$locale = array (
+ 'comment-form' => 'ココにコメントを入力…',
+ 'reply-form' => 'ココに返信を入力…',
+ 'comment-formatting' => '書式設定',
+ 'accepted-format' => '%s容認',
+ 'accepted-html' => '&lt;b&gt;、&lt;strong&gt;、&lt;u&gt;、&lt;i&gt;、&lt;em&gt;、&lt;s&gt;、&lt;big&gt;、&lt;small&gt;、&lt;sup&gt;、&lt;sub&gt;、&lt;pre&gt;、&lt;ul&gt;、&lt;ol&gt;、&lt;li&gt;、&lt;blockquote&gt;、&lt;code&gt;はHTMLをエスケープ、URLは自動的にリンクになり、とここで[img]URLここ[/img]外部画像を表示します。',
+ 'accepted-markdown' => '**太字**、_下線_、*イタリック*、~~取り消し線~~、`ハイライト`、```コード``` HTMLをエスケープします。HTMLとMarkdownはあなたのコメントに一緒に使用することができます。',
+ 'post-button' => '送信する',
+ 'login' => 'ログイン',
+ 'login-tip' => 'ログイン (任意)',
+ 'logout' => 'ログアウト',
+ 'be-first-name' => 'まだコメントがありません。',
+ 'pending-name' => '保留中…',
+ 'deleted-name' => 'は削除…',
+ 'error-name' => 'エラー…',
+ 'be-first-note' => 'ぜひ最初のコメントを!',
+ 'pending-note' => 'このコメントは承認が保留されています。',
+ 'deleted-note' => 'このコメントは削除されました。',
+ 'error-note' => '何かが間違っていました。このコメントを取得できませんでした。',
+ 'options' => 'のオプション',
+ 'cancel' => 'キャンセル',
+ 'reply-to-comment' => 'コメントへ返信',
+ 'edit-your-comment' => 'あなたのコメントを編集',
+ 'optional' => '任意',
+ 'required' => '必須',
+ 'name' => 'お名前',
+ 'name-tip' => 'お名前 (%s)',
+ 'password' => 'パスワード',
+ 'password-tip' => 'パスワード (%s、編集またはこのコメントを削除することができます。)',
+ 'confirm-password' => '確認パスワード',
+ 'email' => 'メールアドレス',
+ 'email-tip' => 'メールアドレス (%s、返信をお知らせする為のメールアドレスです。)',
+ 'website' => 'WEBサイト',
+ 'website-tip' => 'WEBサイト・URL (%s)',
+ 'logged-in' => 'あなたは、正常にログインされています!',
+ 'logged-out' => '正常にログアウトされました!',
+ 'comment-needed' => '有効なコメントが入力ていません。もう一度お試しください。',
+ 'reply-needed' => '有効な返信入力していません。もう一度お試しください。',
+ 'field-needed' => '「%s」フィールドは必須です。',
+ 'post-fail' => '失敗しました!十分な権限がありません。',
+ 'comment-deleted' => 'コメントは削除!',
+ 'post-reply' => '返信する',
+ 'delete' => '削除する',
+ 'permanently-delete' => '完全に削除してください',
+ 'subscribe' => '返信をお知らせ',
+ 'subscribe-tip' => 'コメントへの返信をメールでお知らせ',
+ 'edit-comment' => 'コメントを編集する',
+ 'status' => 'ステータス',
+ 'status-approved' => '承認',
+ 'status-pending' => '承認待ち',
+ 'status-deleted' => 'マーク削除',
+ 'save' => '保存する',
+ 'no-email-warning' => 'メールアドレスを入力しないとコメントへの返信のお知らせを受け取ることができません。よろしいですか?',
+ 'invalid-email' => '入力したメールアドレスが無効です。',
+ 'delete-comment' => 'このコメントを削除してもよろしいですか?',
+ 'post-comment-on' => array ('コメントの投稿', 'コメントの投稿上「%s」'),
+ 'popular-comments' => array ('コメントほとんど人気です', '最も人気のコメント'),
+ 'showing-comments' => array ('%d件のコメント', '%d件のコメント'),
+ 'count-link' => array ('%dコメント', '%dのコメント'),
+ 'count-replies' => array ('返信含む%d件', '%d件の返信含む'),
+ 'sort' => 'ソート',
+ 'sort-ascending' => 'コメント順',
+ 'sort-descending' => '新しいもの順',
+ 'sort-by-date' => '最新のコメント',
+ 'sort-by-likes' => 'いいね',
+ 'sort-by-replies' => '回答によって',
+ 'sort-by-discussion' => '議論することによって',
+ 'sort-by-popularity' => '人気順',
+ 'sort-by-name' => '評価順',
+ 'sort-threads' => 'ツリー形式',
+ 'thread' => '%sへの返信',
+ 'thread-tip' => 'スレッドの先頭にジャンプ',
+ 'comments' => 'コメント',
+ 'replies' => '回答',
+ 'edit' => '編集',
+ 'reply' => '返信',
+ 'like' => array ('いいね', 'いいね'),
+ 'liked' => 'いいね',
+ 'unlike' => '取消す',
+ 'like-comment' => '「いいね」する',
+ 'liked-comment' => '「いいね」を取消す',
+ 'dislike' => array ('厭悪', '厭悪言いました'),
+ 'disliked' => '嫌わ',
+ 'dislike-comment' => '「厭悪」このコメントを',
+ 'disliked-comment' => 'あなたが「厭悪」このコメントを',
+ 'commenter-tip' => '電子メールを介して通知されません',
+ 'subscribed-tip' => '電子メールを介して通知され',
+ 'unsubscribed-tip' => '匿名の場合はメール通知されません',
+ 'show-other-comments' => array ('その他%d件のコメントを表示', '全%d件のコメントを表示'),
+ 'show-number-comments' => array ('%d件のコメントを表示', '他%dコメントを表示'),
+ 'date-time' => '%sで%s',
+ 'date-years' => array ('%d年前', '%d年前'),
+ 'date-months' => array ('%d月前', '%dヶ月前'),
+ 'date-days' => array ('%d日前', '%d日前'),
+ 'date-today' => '%s今日',
+ 'date-day-names' => array ('日曜日', '月曜日', '火曜日', '水曜日', '木曜日', '金曜日', '土曜日'),
+ 'date-month-names' => array ('1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'),
+ 'untitled' => 'タイトルなし',
+ 'external-image-tip' => '遠隔画像表示しにはクリック',
+ 'loading' => 'ローディング…',
+ 'click-to-close' => '閉じるにはクリック',
+ 'hashover-comments' => 'HashOverコメント',
+ 'rss-feed' => 'RSSフィード',
+ 'source-code' => 'ソースコード',
+
+ 'source-code-sub' => 'HashOverサーバーサイドのソースコードビューア',
+ 'type' => 'タイプ',
+ 'path' => 'パス',
+ 'view-as' => '別名で表示',
+ 'text' => 'テキスト',
+ 'download' => 'ダウンロード',
+
+ 'documentation' => 'ドキュメンテーション',
+ 'coming-soon' => 'すぐに来る',
+ 'example' => '例',
+ 'back' => '戻る',
+ 'value' => '値',
+
+ 'successful-save' => '成功しました!',
+ 'failed-to-save' => '保存に失敗しました!権限を確認してください。',
+ 'permissions-info' => '「%s」のパーミッション0755と所有権を「%s」ユーザに与えます。',
+
+ 'admin' => '管理者',
+ 'moderation' => '節度',
+ 'block-ip-addresses' => 'IPアドレスをブロックする',
+ 'filter-url-queries' => 'URLクエリをフィルタリング',
+ 'check-for-updates' => 'アップデートを確認',
+ 'settings' => '設定',
+
+ 'admin-required' => 'あなたは管理者としてログインする必要があります',
+
+ 'blocklist-title' => 'IPアドレスブロックリスト',
+ 'blocklist-sub' => '特定のIPアドレスをブロックする',
+ 'blocklist-ip-tip' => '削除するIPアドレスまたは空白',
+
+ 'url-queries-title' => '無視されたURLクエリ',
+ 'url-queries-sub' => 'どのURLクエリを無視すべきかをフィルタする',
+ 'url-queries-name-tip' => '削除するクエリ名または空白',
+ 'url-queries-value-tip' => '任意の値に対するクエリ値または空白',
+
+ 'settings-sub' => 'さまざまな設定を変更する',
+ 'moderation-sub' => 'コメントの投稿、編集、承認、削除',
+
+ 'setting-language' => '言語',
+ 'setting-theme' => 'テーマ',
+ 'setting-uses-moderation' => '普通のユーザーが投稿したコメントは管理が必要です',
+ 'setting-pends-user-edits' => '通常のユーザーが編集したコメントには追加の管理が必要です',
+ 'setting-data-format' => 'コメントデータフォーマット',
+ 'setting-default-name' => 'デフォルトのコメント作成者の名前',
+ 'setting-allows-images' => 'コメントの画像表示を許可する',
+ 'setting-allows-login' => 'ユーザーのログインを許可する',
+ 'setting-allows-likes' => 'ユーザーがコメントを気に入るようにする',
+ 'setting-allows-dislikes' => 'ユーザーがコメントを嫌うことを許可する',
+ 'setting-uses-ajax' => '非同期JavaScript機能を有効にする',
+ 'setting-collapses-interface' => 'HashOverのユーザーインターフェース全体を折りたたむ',
+ 'setting-collapses-comments' => '設定可能な数のコメントを折りたたむ',
+ 'setting-collapse-limit' => '崩壊するコメントの数',
+ 'setting-reply-mode' => 'コメントスレッドの表示モード',
+ 'setting-stream-depth' => 'ストリームがフラット化される前の応答インデントの数',
+ 'setting-popularity-threshold' => 'コメントが人気がある必要がある好きなネットの数',
+ 'setting-popularity-limit' => '表示する一般的なコメントの数',
+ 'setting-uses-markdown' => 'Markdownサポートを有効にする',
+ 'setting-server-timezone' => 'サーバーのタイムゾーン',
+ 'setting-uses-user-timezone' => 'ユーザーのタイムゾーンでの日付/時刻の表示(JavaScriptモード)',
+ 'setting-uses-short-dates' => '短い日付/時刻を有効にする(例:1日前に)',
+ 'setting-time-format' => '時刻形式、24時間形式の場合は「H:i」',
+ 'setting-date-format' => '日付形式',
+ 'setting-displays-title' => 'ページタイトルの表示を有効にする',
+ 'setting-form-position' => '一次コメントフォームの位置',
+ 'setting-uses-auto-login' => 'ユーザーがコメントを投稿すると自動的にログインする',
+ 'setting-shows-reply-count' => '総数とは別に回答数を表示する',
+ 'setting-count-includes-deleted' => 'コメント数に削除されたコメントを含める',
+ 'setting-icon-mode' => 'アバターアイコン表示モード',
+ 'setting-icon-size' => 'アバターアイコンサイズ',
+ 'setting-image-format' => 'アイコンやその他の画像のフォーマット',
+ 'setting-uses-labels' => 'ラベルを入力の上に表示する',
+ 'setting-uses-cancel-buttons' => 'フォームにキャンセルボタンがあるかどうか',
+ 'setting-appends-css' => 'ページにHashOver CSSを自動的に追加する',
+ 'setting-appends-rss' => 'ページへのHashOver RSSフィードリンクを追加',
+ 'setting-login-method' => 'ユーザーログインシステム',
+ 'setting-sets-cookies' => 'クッキーを有効にする',
+ 'setting-secure-cookies' => '安全なHTTPSのみのクッキーを使用する',
+ 'setting-stores-ip-address' => 'ユーザーIPアドレスの保存を有効にする',
+ 'setting-subscribes-user' => '既定でユーザーに電子メール通知を登録します',
+ 'setting-allows-user-replies' => 'ユーザーの電子メールを返信通知の「返信先」に設定し',
+ 'setting-noreply-email' => 'メールがないときに使用されるメールアドレス',
+ 'setting-spam-batabase' => 'SPAMデータベースの場所',
+ 'setting-spam-check-modes' => 'SPAMチェックを行うモード',
+ 'setting-gravatar-force' => 'アバターの代わりにテーマグラバター画像を使う',
+ 'setting-gravatar-default' => 'デフォルトのGravatarテーマを使用する',
+ 'setting-minifies-javascript' => 'JavaScriptの有効化を有効にする',
+ 'setting-minify-level' => 'JavaScriptの縮小レベル',
+ 'setting-allow-local-metadata' => 'ページ・メタデータをlocalhostから更新できるようにする'
+);
diff --git a/bootstrap/comments/backend/locales/ko.php b/bootstrap/comments/backend/locales/ko.php
new file mode 100644
index 0000000..cb87148
--- /dev/null
+++ b/bootstrap/comments/backend/locales/ko.php
@@ -0,0 +1,206 @@
+<?php
+
+// Copyright (C) 2015-2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// I, Jacob Barkdull, hereby release this work into the public domain.
+// This applies worldwide. If this is not legally possible, I grant any
+// entity the right to use this work for any purpose, without any
+// conditions, unless such conditions are required by law.
+
+
+// Korean text for forms, buttons, links, and tooltips
+$locale = array (
+ 'comment-form' => '댓글을 적으세요...',
+ 'reply-form' => '답글을 적으세요...',
+ 'comment-formatting' => '꾸미기',
+ 'accepted-format' => '사용가능한 %s 형식',
+ 'accepted-html' => '&lt;b&gt;, &lt;strong&gt;, &lt;u&gt;, &lt;i&gt;, &lt;em&gt;, &lt;s&gt;, &lt;big&gt;, &lt;small&gt;, &lt;sup&gt;, &lt;sub&gt;, &lt;pre&gt;, &lt;ul&gt;, &lt;ol&gt;, &lt;li&gt;, &lt;blockquote&gt;, &lt;code&gt; 코드삽입, URL은 자동으로 링크로 변환되고, [img]Image URL[/img] 외부 그림을 표시합니다.',
+ 'accepted-markdown' => '**굵게**, _밑줄_, *기울이기*, ~~취소선~~, `강조`, ```코드```. HTML 과 Markdown 을 동시에 사용할 수 있습니다.',
+ 'post-button' => '댓글 올리기',
+ 'login' => '로그인',
+ 'login-tip' => '로그인 (선택)',
+ 'logout' => '로그아웃',
+ 'be-first-name' => '댓글이 없습니다.',
+ 'pending-name' => '보류중...',
+ 'deleted-name' => '삭제됨...',
+ 'error-name' => '오류...',
+ 'be-first-note' => '첫번째 댓글을 남겨주세요!',
+ 'pending-note' => '이 댓글은 승인을 기다리고 있습니다.',
+ 'deleted-note' => '이 댓글은 삭제되었습니다.',
+ 'error-note' => '무엇인가 잘못되었습니다. 댓글을 표시할 수 없습니다.',
+ 'options' => '선택사항',
+ 'cancel' => '취소',
+ 'reply-to-comment' => '댓글에 답글 달기',
+ 'edit-your-comment' => '댓글 수정',
+ 'optional' => '선택사항',
+ 'required' => '필수',
+ 'name' => '이름',
+ 'name-tip' => '이름 (%s)',
+ 'password' => '암호',
+ 'password-tip' => '암호 (%s, 나중에 댓글을 수정하거나 삭제할 수 있습니다)',
+ 'confirm-password' => '암호 확인',
+ 'email' => '전자메일 주소',
+ 'email-tip' => '전자메일 주소 (%s, 전자메일 알림 용)',
+ 'website' => '웹사이트',
+ 'website-tip' => '웹사이트 (%s)',
+ 'logged-in' => '성공적으로 로그인 했습니다!',
+ 'logged-out' => '성공적으로 로그아웃 했습니다!',
+ 'comment-needed' => '적절한 댓글을 입력하지 못했습니다. 다시 시도해보세요.',
+ 'reply-needed' => '적절한 답글을 입력하지 못했습니다. 다시 시도해보세요.',
+ 'field-needed' => '"%s" 입력이 필요합니다.',
+ 'post-fail' => '실패! 적절한 권한이 없습니다.',
+ 'comment-deleted' => '댓글이 삭제되었습니다!',
+ 'post-reply' => '답글 달기',
+ 'delete' => '삭제',
+ 'permanently-delete' => '영구적으로 삭제',
+ 'subscribe' => '답글을 나에게 알려주세요',
+ 'subscribe-tip' => '전자메일 알림을 설정합니다.',
+ 'edit-comment' => '댓글 수정',
+ 'status' => '상태',
+ 'status-approved' => '승인됨',
+ 'status-pending' => '승인 대기 중',
+ 'status-deleted' => '관리자에 의해 삭제됨',
+ 'save' => '저장',
+ 'no-email-warning' => '전자메일 주소를 입력하지 않으면, 답글 알림을 받을 수 없습니다.',
+ 'invalid-email' => '입력한 전자메일 주소가 바르지 않습니다.',
+ 'delete-comment' => '이 댓글을 삭제하시겠습니까?',
+ 'post-comment-on' => array ('댓글 쓰기', '"%s"에 댓글 쓰기'),
+ 'popular-comments' => array ('가장 인기 있는 댓글', '가장 인기 있는 댓글들'),
+ 'showing-comments' => array ('댓글 %d 개 표시됨', '댓글 %d 개 표시됨'),
+ 'count-link' => array ('%d 댓글', '%d 댓글'),
+ 'count-replies' => array ('%d 답글', '%d 답글'),
+ 'sort' => '정렬',
+ 'sort-ascending' => '오름차순',
+ 'sort-descending' => '내림차순',
+ 'sort-by-date' => '최신부터',
+ 'sort-by-likes' => '\'좋아요\'순',
+ 'sort-by-replies' => '답글수 순',
+ 'sort-by-discussion' => '토의 순',
+ 'sort-by-popularity' => '인기도순',
+ 'sort-by-name' => '작성자순',
+ 'sort-threads' => '글타래',
+ 'thread' => '%s 에 대한 답글',
+ 'thread-tip' => '맨 처음으로',
+ 'comments' => '댓글',
+ 'replies' => '답글',
+ 'edit' => '수정',
+ 'reply' => '답글',
+ 'like' => array ('좋아요', '좋아요'),
+ 'liked' => '좋아합니다',
+ 'unlike' => '안좋아요',
+ 'like-comment' => '이 댓글을 \'좋아요\' 합니다.',
+ 'liked-comment' => '이 댓글을 싫어합니다.',
+ 'dislike' => array ('싫어요', '싫어요'),
+ 'disliked' => '싫어합니다',
+ 'dislike-comment' => '이 댓글을 \'좋아요\' 합니다.',
+ 'disliked-comment' => '이 댓글을 \'안좋아요\' 합니다.',
+ 'commenter-tip' => '전자메일로 변경사항을 알려드리지 않습니다.',
+ 'subscribed-tip' => '전자메일로 변경사항을 알려드립니다.',
+ 'unsubscribed-tip' => '전자메일로 변경사항을 알려드리지 않습니다.',
+ 'show-other-comments' => array ('다른 %d 개의 댓글을 봅니다.', '다른 %d 개의 댓글을 봅니다.'),
+ 'show-number-comments' => array ('%d 개의 댓글을 봅니다.', '%d 개의 댓글을 봅니다.'),
+ 'date-time' => '%s \a\t %s',
+ 'date-years' => array ('%d 년 전', '%d 년 전'),
+ 'date-months' => array ('%d 개월 전', '%d 개월 전 '),
+ 'date-days' => array ('%d 일 전', '%d 일 전'),
+ 'date-today' => '%s 오늘',
+ 'date-day-names' => array ('일요일', '월요일', '화요일', '수요일', '목요일', '금요일', '토요일'),
+ 'date-month-names' => array ('1월', '2월','3월', '4월','5월','6월','7월','8월','9월','10월','11월','12월'),
+ 'untitled' => '제목없슴',
+ 'external-image-tip' => '외부그림보기',
+ 'loading' => '불러오는 중...',
+ 'click-to-close' => '닫기',
+ 'hashover-comments' => '해쉬오버 댓글',
+ 'rss-feed' => 'RSS 글타래',
+ 'source-code' => '소스코드',
+
+ 'source-code-sub' => 'HashOver 서버 측 소스 코드 뷰어',
+ 'type' => '유형',
+ 'path' => '통로',
+ 'view-as' => '다른 이름으로보기',
+ 'text' => '본문',
+ 'download' => '다운로드',
+
+ 'documentation' => '문서',
+ 'coming-soon' => '곧 나옵니다.',
+ 'example' => '예',
+ 'back' => '뒤로',
+ 'value' => '값',
+
+ 'successful-save' => '성공적으로 저장되었습니다!',
+ 'failed-to-save' => '저장하지 못했습니다! 사용 권한을 확인하십시오.',
+ 'permissions-info' => '"% s"권한과 0755 권한을 "% s"사용자에게 부여하십시오.',
+
+ 'admin' => '관리자',
+ 'moderation' => '검토',
+ 'block-ip-addresses' => 'IP 주소 차단',
+ 'filter-url-queries' => 'URL 쿼리 필터링',
+ 'check-for-updates' => '업데이트 확인',
+ 'settings' => '설정',
+
+ 'admin-required' => '관리자로 로그인해야합니다',
+
+ 'blocklist-title' => 'IP 주소 차단 목록',
+ 'blocklist-sub' => '특정 IP 주소 차단',
+ 'blocklist-ip-tip' => '제거 할 IP 주소 또는 공백',
+
+ 'url-queries-title' => '무시 된 URL 검색어',
+ 'url-queries-sub' => '무시할 URL 검색어 필터링',
+ 'url-queries-name-tip' => '제거 할 쿼리 이름 또는 공백',
+ 'url-queries-value-tip' => '쿼리 값 또는 모든 값의 공백',
+
+ 'settings-sub' => '다양한 설정 변경',
+ 'moderation-sub' => '의견 게시, 수정, 승인 및 삭제',
+
+ 'setting-language' => '언어',
+ 'setting-theme' => '테마',
+ 'setting-uses-moderation' => '일반 사용자가 올린 의견은 검토가 필요합니다',
+ 'setting-pends-user-edits' => '일반 사용자가 편집 한 댓글에는 추가 검토가 필요합니다.',
+ 'setting-data-format' => '설명 데이터 형식',
+ 'setting-default-name' => '기본 댓글 작성자 이름',
+ 'setting-allows-images' => '댓글에 이미지 표시 허용',
+ 'setting-allows-login' => '사용자 로그인 허용',
+ 'setting-allows-likes' => '사용자가 댓글을 달 수 있도록 허용',
+ 'setting-allows-dislikes' => '사용자가 댓글을 싫어하도록 허용',
+ 'setting-uses-ajax' => '비동기 자바 스크립트 기능 사용',
+ 'setting-collapses-interface' => '전체 HashOver 사용자 인터페이스 축소',
+ 'setting-collapses-comments' => '구성 가능한 개수 축소',
+ 'setting-collapse-limit' => '접을 댓글 수',
+ 'setting-reply-mode' => '코멘트 스레드의 표시 모드',
+ 'setting-stream-depth' => '스트림이 병합되기 전의 응답 들여 쓰기 수',
+ 'setting-popularity-threshold' => '의견이 인기가 있어야하는 넷 좋아요 수',
+ 'setting-popularity-limit' => '표시 할 인기있는 댓글 수',
+ 'setting-uses-markdown' => '마크 다운 지원 사용',
+ 'setting-server-timezone' => '서버 시간대',
+ 'setting-uses-user-timezone' => '사용자의 시간대로 날짜 / 시간을 표시합니다 (JavaScript 모드)',
+ 'setting-uses-short-dates' => '더 짧은 날짜 / 시간 사용 (예 : "1 days ago")',
+ 'setting-time-format' => '시간 형식, 24시 형식의 경우 "H:i"사용',
+ 'setting-date-format' => '날짜 형식',
+ 'setting-displays-title' => '페이지 제목 표시 사용',
+ 'setting-form-position' => '기본 댓글 양식의 위치',
+ 'setting-uses-auto-login' => '사용자가 주석을 게시 할 때 자동 로그인',
+ 'setting-shows-reply-count' => '총 카운트와 별도로 회신 카운트 표시',
+ 'setting-count-includes-deleted' => '삭제 된 코멘트를 코멘트 카운트에 포함',
+ 'setting-icon-mode' => '아바타 아이콘 표시 모드',
+ 'setting-icon-size' => '아바타 아이콘 크기',
+ 'setting-image-format' => '아이콘 및 기타 이미지 포맷',
+ 'setting-uses-labels' => '입력 위에 레이블 표시',
+ 'setting-uses-cancel-buttons' => '폼에 취소 버튼이 있는지',
+ 'setting-appends-css' => '페이지에 HashOver CSS 자동 추가',
+ 'setting-appends-rss' => '페이지에 HashOver RSS 피드 링크 추가',
+ 'setting-login-method' => '사용자 로그인 시스템',
+ 'setting-sets-cookies' => '쿠키 사용',
+ 'setting-secure-cookies' => '보안 HTTPS 전용 쿠키 사용',
+ 'setting-stores-ip-address' => '사용자 IP 주소 저장 사용',
+ 'setting-subscribes-user' => '기본적으로 전자 메일 알림을 구독합니다',
+ 'setting-allows-user-replies' => '답장 알림에서 사용자 전자 메일을 "답장"으로 설정하십시오.',
+ 'setting-noreply-email' => '이메일이 없을 때 사용되는 이메일 주소',
+ 'setting-spam-batabase' => '스팸 데이터베이스 위치',
+ 'setting-spam-check-modes' => 'SPAM 검사를 수행 할 모드',
+ 'setting-gravatar-force' => '테마 그라바타 이미지 대신 아바타 사용',
+ 'setting-gravatar-default' => '기본 Gravatar 테마 사용',
+ 'setting-minifies-javascript' => '자바 스크립트 축소 사용',
+ 'setting-minify-level' => '자바 스크립트 축소 수준',
+ 'setting-allow-local-metadata' => '페이지 메타 데이터가 localhost에서 업데이트되도록 허용'
+);
diff --git a/bootstrap/comments/backend/locales/lt.php b/bootstrap/comments/backend/locales/lt.php
new file mode 100644
index 0000000..154f537
--- /dev/null
+++ b/bootstrap/comments/backend/locales/lt.php
@@ -0,0 +1,215 @@
+<?php
+
+// Copyright (C) 2015-2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// I, Jacob Barkdull, hereby release this work into the public domain.
+// This applies worldwide. If this is not legally possible, I grant any
+// entity the right to use this work for any purpose, without any
+// conditions, unless such conditions are required by law.
+
+// Lithuanian text for forms, buttons, links, and tooltips
+// Translated by vKaotik
+// Translated for HashOver Comment system.
+// 19-May-2017
+
+// Lietuviškas tekstas formoms, mygtukams, nuorodoms ir t.t
+// Išvertė vKaotik
+// Išversta sistemai HashOver.
+// 2017-05-19
+
+
+$locale = array (
+ 'comment-form' => 'Palikti atsiliepimą...',
+ 'reply-form' => 'Atsakyti į komentarą..',
+ 'comment-formatting' => 'Formatavimas',
+ 'accepted-format' => 'Priimtinas formatavimas: %s',
+ 'accepted-html' => '&lt;b&gt;, &lt;strong&gt;, &lt;u&gt;, &lt;i&gt;, &lt;em&gt;, &lt;s&gt;, &lt;big&gt;, &lt;small&gt;, &lt;sup&gt;, &lt;sub&gt;, &lt;pre&gt;, &lt;ul&gt;, &lt;ol&gt;, &lt;li&gt;, &lt;blockquote&gt;, &lt;code&gt; įterps kodą, URL magiškai taps nuorodomis, ir[img]Nuoroda[/img] įkels įšorinį paveikslėlį.',
+ 'accepted-markdown' => '**Paryškintas**, _apatinė linija_, *itališkas*, ~~perbrėžti~~, `paryškinti`, ```code``` įterpia kodą. HTML ir Markdown gali būti naudojami kartu jūsų komentare.',
+ 'post-button' => 'Palikti atsiliepimą',
+ 'login' => 'Prisijungti',
+ 'login-tip' => 'Prisijungti',
+ 'logout' => 'Atsijungti',
+ 'be-first-name' => 'Jokių komentarų apie šį žmogų nėra. Būk pirmas!',
+ 'pending-name' => 'Vykdoma...',
+ 'deleted-name' => 'Ištrintas...',
+ 'error-name' => 'Klaida...',
+ 'be-first-note' => 'Jokių komentarų apie šį žmogų nėra. Būk pirmas!',
+ 'pending-note' => 'Šis komentaras laukia patvirtinimo.',
+ 'deleted-note' => 'Šis komentaras buvo ištrintas.',
+ 'error-note' => 'Klaida parsiunčiant komentarus..',
+ 'options' => 'Nustatymai',
+ 'cancel' => 'Atšaukti',
+ 'reply-to-comment' => 'Atsakyti į komentarą',
+ 'edit-your-comment' => 'Redaguoti komentarą',
+ 'optional' => 'Pasirinktinai',
+ 'required' => 'Būtinas',
+ 'name' => 'Vardas',
+ 'name-tip' => 'Vardas (%s)',
+ 'password' => 'Slaptažodis',
+ 'password-tip' => 'Slaptažodis (%s, komentarų redagavimui.)',
+ 'confirm-password' => 'Patvirtinti slaptažodį',
+ 'email' => 'El.Pašto adresas',
+ 'email-tip' => 'El.Pašto adresas (%s, būtinas naujienom paštu.)',
+ 'website' => 'Puslapis',
+ 'website-tip' => 'Puslapis (%s)',
+ 'logged-in' => 'Sėkmingai prisijungėte!',
+ 'logged-out' => 'Sėkmingai atsijungėte!',
+ 'comment-needed' => 'Komentaras tuščias.',
+ 'reply-needed' => 'Komentaras tuščias.',
+ 'field-needed' => 'Šis "%s" laukelis yra būtinas.',
+ 'post-fail' => 'Klaida! Nepakanka privilegijų komentuoti..',
+ 'comment-deleted' => 'Komentaras ištrintas!',
+ 'post-reply' => 'Rašyti atsakymą',
+ 'delete' => 'Ištrinti',
+ 'permanently-delete' => 'Ištrinti visam laikui',
+ 'subscribe' => 'Siųsti naujienas paštu',
+ 'subscribe-tip' => 'Užsiprenumeruoti naujienas paštu',
+ 'edit-comment' => 'Redaguoti',
+ 'status' => 'Statusas',
+ 'status-approved' => 'Patvirtinta',
+ 'status-pending' => 'Laukia patvirtinimo',
+ 'status-deleted' => 'Nepatvirtintas',
+ 'save' => 'Išsaugoti',
+ 'no-email-warning' => 'Neįvesdami el.pašto, negausite naujienų iš šio puslapio.',
+ 'invalid-email' => 'El.Pašto adresas neteisingas.',
+ 'delete-comment' => 'Ar jūs tikrai norite ištrinti šį komentarą?',
+ 'post-comment-on' => array ('Rašyti atsiliepimą', 'Rašyti atsiliepimą apie "%s"'),
+ 'popular-comments' => array ('Populiariausias atsiliepimas', 'Populiariausi atsiliepimai'),
+ 'showing-comments' => array ('Rodomas %d komentaras', 'Rodomi %d komentarai'),
+ 'count-link' => array ('%d Komentaras', '%d Komentarai'),
+ 'count-replies' => array ('%d įskaitant atsakymą', '%d įskaitant atsakymus'),
+ 'sort' => 'Rūšiuoti',
+ 'sort-ascending' => 'Eilės tvarka',
+ 'sort-descending' => 'Atvirkštine tvarka',
+ 'sort-by-date' => 'Naujiausi viršuje',
+ 'sort-by-likes' => 'Pagal teigiamus',
+ 'sort-by-replies' => 'Pagal atsakymus',
+ 'sort-by-discussion' => 'Pagal diskusijas',
+ 'sort-by-popularity' => 'Pagal populiarumą',
+ 'sort-by-name' => 'Pagal komentuotoją',
+ 'sort-threads' => 'Temas',
+ 'thread' => 'Į viršų',
+ 'thread-tip' => 'Į viršų',
+ 'comments' => 'Komentarai',
+ 'replies' => 'Atsakymai',
+ 'edit' => 'Redaguoti',
+ 'reply' => 'Atsakyti',
+ 'like' => array ('Teigiamas', 'Teigiami'),
+ 'liked' => 'Teigiamas',
+ 'unlike' => 'Nuimti teigiamą įvertinimą',
+ 'like-comment' => 'Teigiamas',
+ 'liked-comment' => 'Nuimti vertinimą',
+ 'dislike' => array ('Neigiamas', 'Neigiami'),
+ 'disliked' => 'Neigiamas',
+ 'dislike-comment' => 'Neigiamas vertinimas',
+ 'disliked-comment' => 'Nuimti vertinimą',
+ 'commenter-tip' => 'Neįvedę el.pašto , negausite naujienų paštu.',
+ 'subscribed-tip' => 'gaus naujienas paštu',
+ 'unsubscribed-tip' => 'negauna naujienų paštu',
+ 'show-other-comments' => array ('Rodyti %d kitą komentarą', 'Rodyti %d kitus komentarus'),
+ 'show-number-comments' => array ('Rodyti %d komentarą', 'Rodyti %d komentarus'),
+ 'date-time' => '%s \a\t %s',
+ 'date-years' => array ('Prieš %d metus', 'Prieš %d metus'),
+ 'date-months' => array ('Prieš %d mėnesį', 'Prieš %d mėnesius'),
+ 'date-days' => array ('Prieš %d-ą dieną', 'Prieš %d dienas'),
+ 'date-today' => '%s šiandieną',
+ 'date-day-names' => array ('Sekmadienis', 'Pirmadienis', 'Antradienis', 'Trečiadienis', 'Ketvirtadienis', 'Penktadienis', 'Šeštadienis'),
+ 'date-month-names' => array ('Sausis', 'Vasaris', 'Kovas', 'Balandis', 'Gegužė', 'Biržėlis', 'Liepa', 'Rugpjūtis', 'Rugsėjis', 'Spalis', 'Lapkritis', 'Gruodis'),
+ 'untitled' => 'Be vardo',
+ 'external-image-tip' => 'Spausti kad peržiūrėti paveikslėlį',
+ 'loading' => 'Kraunama..',
+ 'click-to-close' => 'Uždaryti',
+ 'hashover-comments' => 'HashOver sistema',
+ 'rss-feed' => 'RSS',
+ 'source-code' => 'Atviras Kodas',
+
+ 'source-code-sub' => 'HashOver serverio šaltinio kodo žiūryklė',
+ 'type' => 'Tipas',
+ 'path' => 'Kelias',
+ 'view-as' => 'Žiūrėti kaip',
+ 'text' => 'Tekstas',
+ 'download' => 'Parsisiųsti',
+
+ 'documentation' => 'Dokumentacija',
+ 'coming-soon' => 'Netrukus',
+ 'example' => 'Pavyzdys',
+ 'back' => 'Atgal',
+ 'value' => 'Vertė',
+
+ 'successful-save' => 'Sėkmingai išsaugotas!',
+ 'failed-to-save' => 'Nepavyko išsaugoti! Patikrinkite teises.',
+ 'permissions-info' => 'Duok "%s" leidimus 0755 ir nuosavybės teisę į "%s" naudotoją.',
+
+ 'admin' => 'Administratorius',
+ 'moderation' => 'Moderacija',
+ 'block-ip-addresses' => 'Blokuoti IP adresus',
+ 'filter-url-queries' => 'Filtruoti URL užklausas',
+ 'check-for-updates' => 'Patikrinkite atnaujinimus',
+ 'settings' => 'Nustatymai',
+
+ 'admin-required' => 'Jūs turite būti prisijungęs kaip administratorius',
+
+ 'blocklist-title' => 'IP adresų blokinis sąrašas',
+ 'blocklist-sub' => 'Blokuoti konkrečius IP adresus',
+ 'blocklist-ip-tip' => 'IP adresas arba tuščias, jei norite pašalinti',
+
+ 'url-queries-title' => 'Nepaisyti URL užklausų',
+ 'url-queries-sub' => 'Filtruoti, kurie URL užklausos turėtų būti ignoruojami',
+ 'url-queries-name-tip' => 'Užklausos pavadinimas arba tuščias, norint pašalinti',
+ 'url-queries-value-tip' => 'Užklausos vertė arba bet kuri verte tuščia',
+
+ 'settings-sub' => 'Keisti įvairius nustatymus',
+ 'moderation-sub' => 'Skelbti, redaguoti, patvirtinti ir ištrinti komentarus',
+
+ 'setting-language' => 'Kalba',
+ 'setting-theme' => 'Tema',
+ 'setting-uses-moderation' => 'Įprastų vartotojų paskelbtiems komentarams reikia moderacijos',
+ 'setting-pends-user-edits' => 'Norint įprastus naudotojus redaguoti komentarai reikalauja papildomo moderavimo',
+ 'setting-data-format' => 'Komentarų duomenų formatas',
+ 'setting-default-name' => 'Numatytojo komentatoriaus vardas',
+ 'setting-allows-images' => 'Leisti rodyti komentarus',
+ 'setting-allows-login' => 'Leisti vartotojams prisijungti',
+ 'setting-allows-likes' => 'Leisti vartotojams patinka komentuoti',
+ 'setting-allows-dislikes' => 'Leisti vartotojams nepatinka komentarų',
+ 'setting-uses-ajax' => 'Įgalinti asinchronines JavaScript funkcijas',
+ 'setting-collapses-interface' => 'Sutraukti visą HashOver vartotojo sąsają',
+ 'setting-collapses-comments' => 'Sutraukti konfigūruojamą komentarų skaičių',
+ 'setting-collapse-limit' => 'Komentarų skaičius sutraukti',
+ 'setting-reply-mode' => 'Rodyti temų komentavimo būdas',
+ 'setting-stream-depth' => 'Atsakymų ištraukų skaičius prieš srautą suplaktas',
+ 'setting-popularity-threshold' => 'Grynasis komentarų skaičiaus skaičius turi būti populiarus',
+ 'setting-popularity-limit' => 'Populiarių komentarų skaičius rodymui',
+ 'setting-uses-markdown' => 'Įgalinti žymėjimo palaikymą',
+ 'setting-server-timezone' => 'Serverio laiko juosta',
+ 'setting-uses-user-timezone' => 'Rodyti datą / laiką naudotojo laiko juostoje (JavaScript režime)',
+ 'setting-uses-short-dates' => 'Įgalinti trumpesnes datas / kartus (pvz., Prieš 1 dieną)',
+ 'setting-time-format' => 'Laiko formatas, naudokite "H:i" 24 valandų formatu',
+ 'setting-date-format' => 'Datos formatas',
+ 'setting-displays-title' => 'Įgalinti puslapio pavadinimą',
+ 'setting-form-position' => 'Pozicija pirminės komentarų formoje',
+ 'setting-uses-auto-login' => 'Automatiškai registruoti vartotojus, kai jie komentuoja komentarus',
+ 'setting-shows-reply-count' => 'Rodyti atsakymų skaičių atskirai nuo bendro skaičiaus',
+ 'setting-count-includes-deleted' => 'Įtraukti ištrintus komentarus į komentarus',
+ 'setting-icon-mode' => 'Avatarų piktogramų rodymo režimas',
+ 'setting-icon-size' => 'Avataro piktogramos dydis',
+ 'setting-image-format' => 'Formatas piktogramoms ir kitiems vaizdams',
+ 'setting-uses-labels' => 'Rodyti etiketes virš įvesties',
+ 'setting-uses-cancel-buttons' => 'Ar formos turi atšaukti mygtukus',
+ 'setting-appends-css' => 'Automatiškai pridėti HashOver CSS prie puslapio',
+ 'setting-appends-rss' => 'Pridėti HashOver RSS kanalo nuorodas į puslapį',
+ 'setting-login-method' => 'Vartotojo prisijungimo sistema',
+ 'setting-sets-cookies' => 'Įgalinti slapukus',
+ 'setting-secure-cookies' => 'Naudoti saugius HTTPS slapukus',
+ 'setting-stores-ip-address' => 'Įgalinti naudotojo IP adresų saugojimą',
+ 'setting-subscribes-user' => 'Pagal numatytuosius nustatymus prenumeruojamas naudotojas el. Paštu',
+ 'setting-allows-user-replies' => 'Nustatykite naudotojo el. Laišką kaip "Atsakymą į" atsakymų pranešimuose',
+ 'setting-noreply-email' => 'El. Pašto adresas naudojamas, kai nėra el. Pašto',
+ 'setting-spam-batabase' => 'SPAM duomenų bazės vieta',
+ 'setting-spam-check-modes' => 'Veiksmai, skirti atlikti SPAM patikrinimą',
+ 'setting-gravatar-force' => 'Naudokite teminius "Gravatar" atvaizdus, ​​o ne "avatarus".',
+ 'setting-gravatar-default' => 'Numatytoji Gravatar tema, kurią norite naudoti',
+ 'setting-minifies-javascript' => 'Įgalinti JavaScript žymėjimą',
+ 'setting-minify-level' => 'JavaScript minimizavimo lygis',
+ 'setting-allow-local-metadata' => 'Leisti puslapio metaduomenis atnaujinti iš "localhost'
+);
diff --git a/bootstrap/comments/backend/locales/nl.php b/bootstrap/comments/backend/locales/nl.php
new file mode 100644
index 0000000..7f9bfea
--- /dev/null
+++ b/bootstrap/comments/backend/locales/nl.php
@@ -0,0 +1,206 @@
+<?php
+
+// Copyright (C) 2016-2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// I, Jacob Barkdull, hereby release this work into the public domain.
+// This applies worldwide. If this is not legally possible, I grant any
+// entity the right to use this work for any purpose, without any
+// conditions, unless such conditions are required by law.
+
+
+// Dutch text for forms, buttons, links, and tooltips
+$locale = array (
+ 'comment-form' => 'Schrijf uw reactie hier ...',
+ 'reply-form' => 'Schrijf uw antwoord hier ...',
+ 'comment-formatting' => 'Opmaak',
+ 'accepted-format' => 'Toegelaten %s',
+ 'accepted-html' => '&lt;b&gt;, &lt;strong&gt;, &lt;u&gt;, &lt;i&gt;, &lt;em&gt;, &lt;s&gt;, &lt;big&gt;, &lt;small&gt;, &lt;sup&gt;, &lt;sub&gt;, &lt;pre&gt;, &lt;ul&gt;, &lt;ol&gt;, &lt;li&gt;, &lt;blockquote&gt;, &lt;code&gt; ontsnapt HTML, URLs automatisch worden links, en [img]URL hier[/img] zal tonen een extern beeld.',
+ 'accepted-markdown' => '**Vet**, _onderstrepen_, *cursief*, ~~doorhalen~~, `hoogtepunt`, ```code``` ontsnapt HTML. HTML en Markdown kan samen worden gebruikt in je reactie.',
+ 'post-button' => 'Verstuur Reactie',
+ 'login' => 'Inloggen',
+ 'login-tip' => 'Inloggen (Optioneel)',
+ 'logout' => 'Uitloggen',
+ 'be-first-name' => 'Nog geen reacties.',
+ 'pending-name' => 'Onderweg...',
+ 'deleted-name' => 'Verwijderd...',
+ 'error-name' => 'Fout...',
+ 'be-first-note' => 'Wees de eerste om te reactie!',
+ 'pending-note' => 'Dit reactie wacht op goedkeuring.',
+ 'deleted-note' => 'Dit reactie is verwijderd.',
+ 'error-note' => 'Er ging iets mis. Kon niet halen deze reactie.',
+ 'options' => 'Opties',
+ 'cancel' => 'Annuleer',
+ 'reply-to-comment' => 'Reageer op reactie',
+ 'edit-your-comment' => 'Bewerk uw reactie',
+ 'optional' => 'Optioneel',
+ 'required' => 'Verplicht',
+ 'name' => 'Naam',
+ 'name-tip' => 'Naam (%s)',
+ 'password' => 'Wachtwoord',
+ 'password-tip' => 'Wachtwoord (%s, maakt het mogelijk om uw reactie te bewerken)',
+ 'confirm-password' => 'Bevestig wachtwoord',
+ 'email' => 'E-mail adres',
+ 'email-tip' => 'E-mail adres (%s, voor e-mail notificaties)',
+ 'website' => 'Website',
+ 'website-tip' => 'Website (%s)',
+ 'logged-in' => 'Inloggen gelukt!',
+ 'logged-out' => 'Uitloggen gelukt!',
+ 'comment-needed' => 'U heeft geen reactie ingevuld. Probeer het opnieuw.',
+ 'reply-needed' => 'U heeft geen antwoord ingevuld. Probeer het opnieuw.',
+ 'field-needed' => 'Het veld "%s" is verplicht.',
+ 'post-fail' => 'Mislukking! U gebrek voldoende toestemming.',
+ 'comment-deleted' => 'Reactie Verwijderd!',
+ 'post-reply' => 'Stuur antwoord',
+ 'delete' => 'Verwijder',
+ 'permanently-delete' => 'Verwijder Permanent',
+ 'subscribe' => 'Herinner mij wanneer een antwoorden wordt geplaatst',
+ 'subscribe-tip' => 'Meld aan voor herinneringen',
+ 'edit-comment' => 'Bewerk reactie',
+ 'status' => 'Status',
+ 'status-approved' => 'Goedgekeurd',
+ 'status-pending' => 'In afwachting van goedkeuring',
+ 'status-deleted' => 'Gemarkeerd verwijderd',
+ 'save' => 'Bewaar',
+ 'no-email-warning' => 'U zult geen reactie krijgen wanneer een reactie wordt geplaatst als u geen e-mail adres invult (maar uw reactie wordt gewoon geplaatst).',
+ 'invalid-email' => 'Het opgegeven email adres is niet geldig.',
+ 'delete-comment' => 'Weet u zeker dat u het reactie wilt verwijderen?',
+ 'post-comment-on' => array ('Plaats een reactie', 'Plaats een reactie op "%s"'),
+ 'popular-comments' => array ('Meest populaire reactie', 'Meest populaire reacties'),
+ 'showing-comments' => array ('%d reactie geplaatst', '%d reacties geplaatst'),
+ 'count-link' => array ('%d reactie', '%d reacties'),
+ 'count-replies' => array ('%d tellen antwoord', '%d tellen antwoorden'),
+ 'sort' => 'Sorteer',
+ 'sort-ascending' => 'In volgorde',
+ 'sort-descending' => 'In omgekeerde volgorde',
+ 'sort-by-date' => 'Nieuwste eerst',
+ 'sort-by-likes' => 'Door likes',
+ 'sort-by-replies' => 'Door antwoorden',
+ 'sort-by-discussion' => 'Door discussie',
+ 'sort-by-popularity' => 'Door populariteit',
+ 'sort-by-name' => 'Door commenter',
+ 'sort-threads' => 'Threads',
+ 'thread' => 'In antwoord op %s',
+ 'thread-tip' => 'Spring naar top van thread',
+ 'comments' => 'Reacties',
+ 'replies' => 'Antwoorden',
+ 'edit' => 'Bewerk',
+ 'reply' => 'Antwoord',
+ 'like' => array ('Like', 'Likes'),
+ 'liked' => 'Liked',
+ 'unlike' => 'Unlike',
+ 'like-comment' => '\'Like\' dit reactie',
+ 'liked-comment' => 'Unlike dit reactie',
+ 'dislike' => array ('Dislike', 'Dislikes'),
+ 'disliked' => 'Disliked',
+ 'dislike-comment' => '\'Dislike\' dit reactie',
+ 'disliked-comment' => 'You \'Disliked\' dit reactie',
+ 'commenter-tip' => 'U zult geen notificatie krijgen via e-mail',
+ 'subscribed-tip' => 'zal per email op de hoogte worden gebracht',
+ 'unsubscribed-tip' => 'is niet geabonneerd op notificaties',
+ 'show-other-comments' => array ('Toon %d andere reactie', 'Toon %d andere reacties'),
+ 'show-number-comments' => array ('Toon %d reactie', 'Toon %d reacties'),
+ 'date-time' => '%s \o\m %s',
+ 'date-years' => array ('%d jaar geleden', '%d jaar geleden'),
+ 'date-months' => array ('%d maand geleden', '%d maanden geleden'),
+ 'date-days' => array ('%d dag geleden', '%d dagen geleden'),
+ 'date-today' => '%s vandaag',
+ 'date-day-names' => array ('Zondag', 'Maandag', 'Dinsdag', 'Woensdag', 'Donderdag', 'Vrijdag', 'Zaterdag'),
+ 'date-month-names' => array ('Januari', 'Februari', 'Maart', 'April', 'Mei', 'Juni', 'Juli', 'Augustus', 'September', 'Oktober', 'November', 'December'),
+ 'untitled' => 'Untitled',
+ 'external-image-tip' => 'Klik om te bekijken extern beeld',
+ 'loading' => 'Loading ...',
+ 'click-to-close' => 'Klik om te sluiten',
+ 'hashover-comments' => 'HashOver Reacties',
+ 'rss-feed' => 'RSS Feed',
+ 'source-code' => 'Broncode',
+
+ 'source-code-sub' => 'HashOver server-side broncode-viewer',
+ 'type' => 'Type',
+ 'path' => 'Pad',
+ 'view-as' => 'Zien als',
+ 'text' => 'Tekst',
+ 'download' => 'Downloaden',
+
+ 'documentation' => 'Documentatie',
+ 'coming-soon' => 'Binnenkort beschikbaar',
+ 'example' => 'Voorbeeld',
+ 'back' => 'Terug',
+ 'value' => 'Waarde',
+
+ 'successful-save' => 'Succesvol opgeslagen!',
+ 'failed-to-save' => 'Opslaan mislukt! Controleer de rechten.',
+ 'permissions-info' => 'Geef "%s" toestemmingen 0755 en eigendom aan de gebruiker "%s".',
+
+ 'admin' => 'Beheerder',
+ 'moderation' => 'Moderatie',
+ 'block-ip-addresses' => 'Blokkeer IP-adressen',
+ 'filter-url-queries' => 'Filter URL-zoekopdrachten',
+ 'check-for-updates' => 'Controleren op updates',
+ 'settings' => 'Instellingen',
+
+ 'admin-required' => 'Je moet ingelogd zijn als admin',
+
+ 'blocklist-title' => 'IP Address Blocklist',
+ 'blocklist-sub' => 'Blokkeer specifieke IP-adressen',
+ 'blocklist-ip-tip' => 'IP-adres of leeg om te verwijderen',
+
+ 'url-queries-title' => 'Genegeerde URL-zoekopdrachten',
+ 'url-queries-sub' => 'Filter welke urlqueries moeten worden genegeerd',
+ 'url-queries-name-tip' => 'Vraagnaam of leeg om te verwijderen',
+ 'url-queries-value-tip' => 'Querywaarde of leeg voor elke waarde',
+
+ 'settings-sub' => 'Verschillende instellingen wijzigen',
+ 'moderation-sub' => 'Reacties plaatsen, bewerken, goedkeuren en verwijderen',
+
+ 'setting-language' => 'Taal',
+ 'setting-theme' => 'Thema',
+ 'setting-uses-moderation' => 'Opmerkingen geplaatst door normale gebruikers vereisen moderatie',
+ 'setting-pends-user-edits' => 'Opmerkingen bewerkt door normale gebruikers vereisen extra moderatie',
+ 'setting-data-format' => 'Gegevensindeling van de gegevens',
+ 'setting-default-name' => 'Default commenter name',
+ 'setting-allows-images' => 'Sta weergave van afbeeldingen toe in reacties',
+ 'setting-allows-login' => 'Gebruikers toestaan ​​in te loggen',
+ 'setting-allows-likes' => 'Sta toe dat gebruikers reacties leuk vinden',
+ 'setting-allows-dislikes' => 'Sta toe dat gebruikers reacties niet leuk vinden',
+ 'setting-uses-ajax' => 'Asynchrone JavaScript-functies inschakelen',
+ 'setting-collapses-interface' => 'Collapse hele HashOver gebruikersinterface',
+ 'setting-collapses-comments' => 'Een instelbaar aantal reacties samenvoegen',
+ 'setting-collapse-limit' => 'Aantal in te klappen opmerkingen',
+ 'setting-reply-mode' => 'Weergavemodus van reactiethreads',
+ 'setting-stream-depth' => 'Aantal antwoordinsprekingen voordat de stream is afgevlakt',
+ 'setting-popularity-threshold' => 'Netto aantal vind-ik-leuks dat een reactie populair moet zijn',
+ 'setting-popularity-limit' => 'Aantal populaire reacties om weer te geven',
+ 'setting-uses-markdown' => 'Markdown-ondersteuning inschakelen',
+ 'setting-server-timezone' => 'Servertijdzone',
+ 'setting-uses-user-timezone' => 'Toon datums / tijden in de tijdzone van de gebruiker (JavaScript-modus)',
+ 'setting-uses-short-dates' => 'Kortere datums / tijden inschakelen (voorbeeld "1 dag geleden")',
+ 'setting-time-format' => 'Tijdnotatie, gebruik "H:i" voor 24-uursnotatie',
+ 'setting-date-format' => 'Datumnotatie',
+ 'setting-displays-title' => 'Weergave van paginatitel inschakelen',
+ 'setting-form-position' => 'Positie voor primaire opmerking',
+ 'setting-uses-auto-login' => 'Gebruikers automatisch aanmelden wanneer ze reacties plaatsen',
+ 'setting-shows-reply-count' => 'Toon het aantal antwoorden afzonderlijk van de totale telling',
+ 'setting-count-includes-deleted' => 'Inclusief verwijderde opmerkingen in aantal reacties',
+ 'setting-icon-mode' => 'Avatar pictogram weergavemodus',
+ 'setting-icon-size' => 'Avatar pictogramgrootte',
+ 'setting-image-format' => 'Formaat voor pictogrammen en andere afbeeldingen',
+ 'setting-uses-labels' => 'Toon labels boven ingangen',
+ 'setting-uses-cancel-buttons' => 'Of formulieren annulatieknoppen hebben',
+ 'setting-appends-css' => 'Voeg automatisch HashOver CSS toe aan pagina',
+ 'setting-appends-rss' => 'HashOver RSS Feed-links toevoegen aan pagina',
+ 'setting-login-method' => 'Gebruikersaanmeldingssysteem',
+ 'setting-sets-cookies' => 'Cookies inschakelen',
+ 'setting-secure-cookies' => 'Gebruik veilige cookies met alleen HTTPS',
+ 'setting-stores-ip-address' => 'Opslag van gebruikers-IP-adressen inschakelen',
+ 'setting-subscribes-user' => 'Abonneer de gebruiker standaard op berichten per e-mail',
+ 'setting-allows-user-replies' => 'Stel gebruikers-e-mail in als antwoord in antwoordmeldingen',
+ 'setting-noreply-email' => 'E-mailadres gebruikt wanneer geen e-mail wordt gegeven',
+ 'setting-spam-batabase' => 'SPAM-database locatie',
+ 'setting-spam-check-modes' => 'Modi om SPAM-controle uit te voeren onder',
+ 'setting-gravatar-force' => 'Gebruik als thema Gravatar-afbeeldingen in plaats van avatars',
+ 'setting-gravatar-default' => 'Standaard Gravatar-thema om te gebruiken',
+ 'setting-minifies-javascript' => 'JavaScript-verkleining inschakelen',
+ 'setting-minify-level' => 'JavaScript-minificatieniveau',
+ 'setting-allow-local-metadata' => 'Sta toe dat paginametadata worden bijgewerkt van localhost'
+);
diff --git a/bootstrap/comments/backend/locales/pl.php b/bootstrap/comments/backend/locales/pl.php
new file mode 100644
index 0000000..0d68de4
--- /dev/null
+++ b/bootstrap/comments/backend/locales/pl.php
@@ -0,0 +1,206 @@
+<?php
+
+// Copyright (C) 2015-2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// I, Jacob Barkdull, hereby release this work into the public domain.
+// This applies worldwide. If this is not legally possible, I grant any
+// entity the right to use this work for any purpose, without any
+// conditions, unless such conditions are required by law.
+
+
+// Polish text for forms, buttons, links, and tooltips
+$locale = array (
+ 'comment-form' => 'Napisz komentarz...',
+ 'reply-form' => 'Napisz odpowiedź...',
+ 'comment-formatting' => 'Formatowanie',
+ 'accepted-format' => 'Akceptowany %s',
+ 'accepted-html' => '&lt;b&gt;, &lt;strong&gt;, &lt;u&gt;, &lt;i&gt;, &lt;em&gt;, &lt;s&gt;, &lt;big&gt;, &lt;small&gt;, &lt;sup&gt;, &lt;sub&gt;, &lt;pre&gt;, &lt;ul&gt;, &lt;ol&gt;, &lt;li&gt;, &lt;blockquote&gt;, &lt;code&gt; escapes HTML, URLe automatycznie zamieniają się w linki, a [img]URL tutaj[/img] wyświetli zewnętrzny obrazek.',
+ 'accepted-markdown' => '**Pogrubienie**, _podkreślenie_, *kursywa*, ~~przekreślenie~~, `zaznacz`, ```kod``` ucieka HTML. HTML i Markdown mogą być stosowane razem w swoim komentarzu.',
+ 'post-button' => 'Wyślij komentarz',
+ 'login' => 'Zaloguj Się',
+ 'login-tip' => 'Zaloguj Się (opcjonalne)',
+ 'logout' => 'Wyloguj Się',
+ 'be-first-name' => 'Brak komentarzy Jeszcze.',
+ 'pending-name' => 'Oczekujące...',
+ 'deleted-name' => 'Usunięty...',
+ 'error-name' => 'Błąd...',
+ 'be-first-note' => 'Napisz pierwszy komentarz!',
+ 'pending-note' => 'Ten komentarz jest oczekujące na zatwierdzenie.',
+ 'deleted-note' => 'Komentarz został usunięty.',
+ 'error-note' => 'Coś poszło nie tak. Nie można pobrać ten komentarz.',
+ 'options' => 'Opcje',
+ 'cancel' => 'Anuluj',
+ 'reply-to-comment' => 'Odpowiedz na komentarz',
+ 'edit-your-comment' => 'Edytuj komentarz',
+ 'optional' => 'Opcjonalne',
+ 'required' => 'Wymagane',
+ 'name' => 'Imię',
+ 'name-tip' => 'Imię (%s)',
+ 'password' => 'Hasło',
+ 'password-tip' => 'Hasło (%s, pozwala edytować lub usunąć to komentarz)',
+ 'confirm-password' => 'Potwierdź hasło',
+ 'email' => 'Adres E-mail',
+ 'email-tip' => 'Adres E-mail (%s, dla powiadomień e-mail)',
+ 'website' => 'Strona www',
+ 'website-tip' => 'Strona www (%s)',
+ 'logged-in' => 'Zostałeś pomyślnie zalogowany!',
+ 'logged-out' => 'Zostałeś pomyślnie wylogowany!',
+ 'comment-needed' => 'Wpisz komentarz do właściwego pola. Proszę spróbuj ponownie.',
+ 'reply-needed' => 'Wpisz odpowiedź do właściwego pola. Proszę spróbuj ponownie.',
+ 'field-needed' => '"%s" pola jest wymagane.',
+ 'post-fail' => 'Komentarz nie wysłany! Nie masz wystarczających uprawnień.',
+ 'comment-deleted' => 'Komentarz usunięty!',
+ 'post-reply' => 'Wyślij odpowiedź',
+ 'delete' => 'Usunąć',
+ 'permanently-delete' => 'Trwale Usunąć',
+ 'subscribe' => 'Subskrybuj',
+ 'subscribe-tip' => 'Subskrybuj aby otrzymywać powiadomienia E-mailem',
+ 'edit-comment' => 'Edytuj komentarz',
+ 'status' => 'Status',
+ 'status-approved' => 'Zatwierdzony',
+ 'status-pending' => 'W oczekiwaniu na zatwierdzenie',
+ 'status-deleted' => 'Oznaczono usunięte',
+ 'save' => 'Zapisz',
+ 'no-email-warning' => 'Nie będziesz otrzymywał powiadomień o odpowiedziach na Twój komentarz bez podania e-maila.',
+ 'invalid-email' => 'Adres e-mail jest niewłaściwy.',
+ 'delete-comment' => 'Czy na pewno chcesz usunąć komentarz?',
+ 'post-comment-on' => array ('Wyślij komentarz', 'Wyślij komentarz na "%s"'),
+ 'popular-comments' => array ('Najbardziej Popularny Komentarz', 'Najbardziej Popularne Komentarze'),
+ 'showing-comments' => array ('Wyświetlanie %d Komentarza', 'Wyświetlanie %d Komentarzy'),
+ 'count-link' => array ('%d Komentarz', '%d Komentarze'),
+ 'count-replies' => array ('%d liczenie odpowiedzi', '%d liczenie odpowiedzi'),
+ 'sort' => 'Wyświetl wg',
+ 'sort-ascending' => 'Kolejności',
+ 'sort-descending' => 'Odwrotnej Kolejności',
+ 'sort-by-date' => 'Najnowsze pierwsze',
+ 'sort-by-likes' => 'Wg likes',
+ 'sort-by-replies' => 'Wg odpowiedzi',
+ 'sort-by-discussion' => 'Wg dyskusji',
+ 'sort-by-popularity' => 'Wg popularności',
+ 'sort-by-name' => 'Wg autora',
+ 'sort-threads' => 'Nici',
+ 'thread' => 'W odpowiedzi na %s',
+ 'thread-tip' => 'Przejdź do początku',
+ 'comments' => 'Komentarze',
+ 'replies' => 'Odpowiedzi',
+ 'edit' => 'Edytuj',
+ 'reply' => 'Odpowiedz',
+ 'like' => array ('Like', 'Likes'),
+ 'liked' => 'Liked',
+ 'unlike' => 'Unlike',
+ 'like-comment' => '\'Like\' ten komentarz',
+ 'liked-comment' => 'Polubiłeś \'Liked\' ten komentarz',
+ 'dislike' => array ('Dislike', 'Dislikes'),
+ 'disliked' => 'Disliked',
+ 'dislike-comment' => '\'Dislike\' Ten Komentarz',
+ 'disliked-comment' => 'Polubiłeś \'Disliked\' ten komentarz',
+ 'commenter-tip' => 'Nie będziesz otrzymywać powiadomień e-mailem',
+ 'subscribed-tip' => 'będzie powiadomiony e-mailem',
+ 'unsubscribed-tip' => 'nie będzie powiadomiony e-mailem',
+ 'show-other-comments' => array ('Wyświetlacz %d Inny Komentarz', 'Wyświetlacz %d Inne Komentarze'),
+ 'show-number-comments' => array ('Wyświetlacz %d Komentarz', 'Wyświetlacz %d Komentarze'),
+ 'date-time' => '%s \n\a %s',
+ 'date-years' => array ('%d rok temu', '%d lat temu'),
+ 'date-months' => array ('%d miesiąc temu', '%d miesięcy temu'),
+ 'date-days' => array ('%d dzień temu', '%d dni temu'),
+ 'date-today' => '%s dzisiaj',
+ 'date-day-names' => array ('Niedziela', 'Poniedziałek', 'Wtorek', 'Środa', 'Czwartek', 'Piątek', 'Sobota'),
+ 'date-month-names' => array ('Styczeń', 'Luty', 'Marsz', 'Kwiecień', 'Może', 'Czerwiec', 'Lipiec', 'Sierpień', 'Wrzesień', 'Październik', 'Listopad', 'Grudzień'),
+ 'untitled' => 'Bez tytułu',
+ 'external-image-tip' => 'Kliknij aby zobaczyć obraz zewnętrzny',
+ 'loading' => 'Załadunek...',
+ 'click-to-close' => 'Kliknij aby zamknąć',
+ 'hashover-comments' => 'HashOver Komentarze',
+ 'rss-feed' => 'Kanału RSS',
+ 'source-code' => 'Kod Źródłowy',
+
+ 'source-code-sub' => 'Przeglądarka kodu źródłowego HashOver po stronie serwera',
+ 'type' => 'Typ',
+ 'path' => 'Ścieżka',
+ 'view-as' => 'Wyświetl jako',
+ 'text' => 'Tekst',
+ 'download' => 'Pobieranie',
+
+ 'documentation' => 'Dokumentacja',
+ 'coming-soon' => 'Wkrótce',
+ 'example' => 'Przykład',
+ 'back' => 'Powrót',
+ 'value' => 'Wartość',
+
+ 'successful-save' => 'Pomyślnie zapisany!',
+ 'failed-to-save' => 'Nie udało się zapisać! Sprawdź uprawnienia.',
+ 'permissions-info' => '"Daj "%s" uprawnienia 0755 i prawa własności do "%s" użytkownika."',
+
+ 'admin' => 'Administrator',
+ 'moderation' => 'Moderacja',
+ 'block-ip-addresses' => 'Blokuj adresy IP',
+ 'filter-url-queries' => 'Filtruj zapytania URL',
+ 'check-for-updates' => 'Sprawdź aktualizacje',
+ 'settings' => 'Ustawienia',
+
+ 'admin-required' => 'Musisz być zalogowany jako admin',
+
+ 'blocklist-title' => 'Lista zablokowanych adresów IP',
+ 'blocklist-sub' => 'Blokuj określone adresy IP',
+ 'blocklist-ip-tip' => 'Adres IP lub puste, aby usunąć',
+
+ 'url-queries-title' => 'Zignorowane zapytania URL',
+ 'url-queries-sub' => 'Filtruj, które zapytania URL powinny być ignorowane',
+ 'url-queries-name-tip' => 'Nazwa zapytania lub puste miejsce do usunięcia',
+ 'url-queries-value-tip' => 'Wartość zapytania lub puste dla dowolnej wartości',
+
+ 'settings-sub' => 'Zmień różne ustawienia',
+ 'moderation-sub' => 'Publikuj, edytuj, zatwierdzaj i usuwaj komentarze',
+
+ 'setting-language' => 'Język',
+ 'setting-theme' => 'Temat',
+ 'setting-uses-moderation' => 'Komentarze publikowane przez zwykłych użytkowników wymagają moderacji',
+ 'setting-pends-user-edits' => 'Komentarze edytowane przez zwykłych użytkowników wymagają dodatkowego moderowania',
+ 'setting-data-format' => 'Format danych komentarzy',
+ 'setting-default-name' => 'Domyślna nazwa komentera',
+ 'setting-allows-images' => 'Pozwól wyświetlać obrazy w komentarzach',
+ 'setting-allows-login' => 'Zezwalaj użytkownikom na logowanie',
+ 'setting-allows-likes' => 'Pozwól użytkownikom lubić komentarze',
+ 'setting-allows-dislikes' => 'Pozwól użytkownikom nie lubić komentarzy',
+ 'setting-uses-ajax' => 'Włącz asynchroniczne funkcje JavaScript',
+ 'setting-collapses-interface' => 'Zwiń cały interfejs użytkownika HashOver',
+ 'setting-collapses-comments' => 'Zwiń konfigurowalną liczbę komentarzy',
+ 'setting-collapse-limit' => 'Liczba komentarzy do zwinięcia',
+ 'setting-reply-mode' => 'Tryb wyświetlania wątków komentarzy',
+ 'setting-stream-depth' => 'Liczba wcinków odpowiedzi przed spłaszczeniem strumienia',
+ 'setting-popularity-threshold' => 'Liczba netto podobnych komentarzy musi być popularna',
+ 'setting-popularity-limit' => 'Liczba popularnych komentarzy do wyświetlenia',
+ 'setting-uses-markdown' => 'Włącz obsługę znaczników',
+ 'setting-server-timezone' => 'Strefa czasowa serwera',
+ 'setting-uses-user-timezone' => 'Wyświetlaj daty / godziny w strefie czasowej użytkownika (tryb-JavaScript)',
+ 'setting-uses-short-dates' => 'Włącz krótsze daty / godziny (przykład" 1 dzień temu ")',
+ 'setting-time-format' => 'Format czasu, użyj "H:i" dla formatu 24-godzinnego',
+ 'setting-date-format' => 'Format daty',
+ 'setting-displays-title' => 'Włącz wyświetlanie tytułu strony',
+ 'setting-form-position' => 'Pozycja dla pierwotnego formularza komentarza',
+ 'setting-uses-auto-login' => 'Automatycznie loguj użytkowników, kiedy publikują komentarze',
+ 'setting-shows-reply-count' => 'Wyświetl liczenie odpowiedzi niezależnie od całkowitej liczby',
+ 'setting-count-includes-deleted' => 'Uwzględnij usunięte komentarze w komentarzach',
+ 'setting-icon-mode' => 'Tryb wyświetlania ikony awataru',
+ 'setting-icon-size' => 'Rozmiar ikony awatarów',
+ 'setting-image-format' => 'Format ikon i innych obrazów',
+ 'setting-uses-labels' => 'Wyświetl etykiety powyżej danych wejściowych',
+ 'setting-uses-cancel-buttons' => 'Czy formularze mają przyciski anulowania',
+ 'setting-appends-css' => 'Automatycznie dodaj HashOver CSS do strony',
+ 'setting-appends-rss' => 'Dodaj kanały RSS kanału HashOver do strony',
+ 'setting-login-method' => 'System logowania użytkownika',
+ 'setting-sets-cookies' => 'Włącz ciasteczka',
+ 'setting-secure-cookies' => 'Użyj bezpiecznych plików cookie HTTPS',
+ 'setting-stores-ip-address' => 'Włącz przechowywanie adresów IP użytkowników',
+ 'setting-subscribes-user' => 'Domyślnie zasubskrybuj użytkownika, aby otrzymywać powiadomienia e-mail',
+ 'setting-allows-user-replies' => 'Ustaw adres e-mail użytkownika jako "Odpowiedz do" w powiadomieniach o odpowiedzi',
+ 'setting-noreply-email' => 'Adres e-mail użyty, gdy nie podano adresu e-mail',
+ 'setting-spam-batabase' => 'Lokalizacja bazy danych SPAM',
+ 'setting-spam-check-modes' => 'Tryby do wykonania sprawdzania spamu w ramach',
+ 'setting-gravatar-force' => 'Użyj tematycznych obrazów Gravatar zamiast awatarów',
+ 'setting-gravatar-default' => 'Domyślny motyw Gravatta do użycia',
+ 'setting-minifies-javascript' => 'Włącz minimalizację JavaScript',
+ 'setting-minify-level' => 'Poziom minimalizacji JavaScript',
+ 'setting-allow-local-metadata' => 'Pozwól na aktualizowanie metadanych strony z localhosta'
+);
diff --git a/bootstrap/comments/backend/locales/pt-br.php b/bootstrap/comments/backend/locales/pt-br.php
new file mode 100644
index 0000000..a5facfa
--- /dev/null
+++ b/bootstrap/comments/backend/locales/pt-br.php
@@ -0,0 +1,206 @@
+<?php
+
+// Copyright (C) 2015-2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// I, Jacob Barkdull, hereby release this work into the public domain.
+// This applies worldwide. If this is not legally possible, I grant any
+// entity the right to use this work for any purpose, without any
+// conditions, unless such conditions are required by law.
+
+
+// Brazilian Portuguese text for forms, buttons, links, and tooltips
+$locale = array (
+ 'comment-form' => 'Digite aqui seu comentário...',
+ 'reply-form' => 'Digite a resposta aqui...',
+ 'comment-formatting' => 'Formatação',
+ 'accepted-format' => '%s permitido',
+ 'accepted-html' => '&lt;b&gt;, &lt;strong&gt;, &lt;u&gt;, &lt;i&gt;, &lt;em&gt;, &lt;s&gt;, &lt;big&gt;, &lt;small&gt;, &lt;sup&gt;, &lt;sub&gt;, &lt;pre&gt;, &lt;ul&gt;, &lt;ol&gt;, &lt;li&gt;, &lt;blockquote&gt;, &lt;code&gt; evite HTML, URLs automaticamente se tornam links, e [img]URL aqui[/img] irão mostrar uma imagem externa.',
+ 'accepted-markdown' => '**Negrito**, _sublinhado_, *itálico*, ~~tachado~~, `destaque`, ```código``` escapes HTML. HTML e Markdown podem ser usados em conjunto no seu comentário.',
+ 'post-button' => 'Enviar comentário.',
+ 'login' => 'Fazer login',
+ 'login-tip' => 'Fazer login (opcionais)',
+ 'logout' => 'Sair',
+ 'be-first-name' => 'Ainda não há comentários.',
+ 'pending-name' => 'Pendente...',
+ 'deleted-name' => 'Suprimido...',
+ 'error-name' => 'Erro...',
+ 'be-first-note' => 'Seja o primeiro a comentar!',
+ 'pending-note' => 'Esse comentário está pendente de aprovação.',
+ 'deleted-note' => 'Este comentário foi deletado.',
+ 'error-note' => 'Algo deu errado. Não foi possível recuperar este comentário.',
+ 'options' => 'Opções',
+ 'cancel' => 'Cancelar',
+ 'reply-to-comment' => 'Responder ao comentário',
+ 'edit-your-comment' => 'Editar seu comentário',
+ 'optional' => 'Opcionais',
+ 'required' => 'Obrigatório',
+ 'name' => 'Nome',
+ 'name-tip' => 'Nome (%s)',
+ 'password' => 'Senha',
+ 'password-tip' => 'Senha (%s, permite que você edite ou inclua este comentário)',
+ 'confirm-password' => 'Confirme a Senha',
+ 'email' => 'E-mail',
+ 'email-tip' => 'E-mail Address (%s, para as notificações de e-mail)',
+ 'website' => 'Website',
+ 'website-tip' => 'Website (%s)',
+ 'logged-in' => 'Você logou com sucesso!',
+ 'logged-out' => 'Você tem saiu com sucesso!',
+ 'comment-needed' => 'Você não enviou um comentário de maneira correta. Por favor, tente novamente.',
+ 'reply-needed' => 'Você não respondeu ao comentário de maneira correta. Por favor, tente novamente.',
+ 'field-needed' => 'O campo "%s" é obrigatório.',
+ 'post-fail' => 'Falha! Você não possui permissões suficientes.',
+ 'comment-deleted' => 'Comentário suprimido!',
+ 'post-reply' => 'Enviar resposta',
+ 'delete' => 'Apagar',
+ 'permanently-delete' => 'Apagar Permanentemente',
+ 'subscribe' => 'Notifique-me sobre respostas',
+ 'subscribe-tip' => 'Assine para receber notícias por email.',
+ 'edit-comment' => 'Editar comentários',
+ 'status' => 'Status',
+ 'status-approved' => 'Aprovado',
+ 'status-pending' => 'Pendente de aprovação',
+ 'status-deleted' => 'Marcado suprimido',
+ 'save' => 'Salvar',
+ 'no-email-warning' => 'Você não receberá notificações ou respostas do seu comentário sem fornecer um endereço de email.',
+ 'invalid-email' => 'O endereço de e-mail digitado é inválido.',
+ 'delete-comment' => 'Tem certeza que deseja apagar este comentário?',
+ 'post-comment-on' => array ('Enviar um comentário', 'Enviar um comentário na "%s"'),
+ 'popular-comments' => array ('Mais Populares Comentário', 'Mais Populares Comentários'),
+ 'showing-comments' => array ('Mostrando %d Comentário', 'Mostrando %d Comentários'),
+ 'count-link' => array ('%d Comentário', '%d Comentários'),
+ 'count-replies' => array ('%d contando resposta', '%d contando respostas'),
+ 'sort' => 'Ordenar',
+ 'sort-ascending' => 'Em ordem',
+ 'sort-descending' => 'Em ordem onversa',
+ 'sort-by-date' => 'Recentes primeiro',
+ 'sort-by-likes' => 'Por likes',
+ 'sort-by-replies' => 'Por respostas',
+ 'sort-by-discussion' => 'Por discussão',
+ 'sort-by-popularity' => 'Por popularidade',
+ 'sort-by-name' => 'Por comentarista',
+ 'sort-threads' => 'Threads',
+ 'thread' => 'Em resposta a %s',
+ 'thread-tip' => 'Ir para o início da conversa',
+ 'comments' => 'Comentários',
+ 'replies' => 'Respostas',
+ 'edit' => 'Editar',
+ 'reply' => 'Resposta',
+ 'like' => array ('Gostei', 'Likes'),
+ 'liked' => 'Gostou',
+ 'unlike' => 'Desgostei',
+ 'like-comment' => '\'Gostei\' deste comentário',
+ 'liked-comment' => 'Você \'Gostou\' Deste comentário',
+ 'dislike' => array ('Desgostei', 'Dislikes'),
+ 'disliked' => 'Desgostou',
+ 'dislike-comment' => '\'Desgostei\' deste comentário',
+ 'disliked-comment' => 'Você \'Desgostou\' deste comentário',
+ 'commenter-tip' => 'Você não será avisado por email.',
+ 'subscribed-tip' => 'será avisado por email.',
+ 'unsubscribed-tip' => 'não assinou para receber notificações por email',
+ 'show-other-comments' => array ('Mostrar %d outro Comentário', 'Mostrar %d outro Comentários'),
+ 'show-number-comments' => array ('Mostrar %d Comentário', 'Mostrar %d Comentários'),
+ 'date-time' => '%s \à\s %s',
+ 'date-years' => array ('%d ano atrás', '%d anos atrás'),
+ 'date-months' => array ('%d mes atrás', '%d meses atrás'),
+ 'date-days' => array ('%d dia atrás', '%d dias atrás'),
+ 'date-today' => '%s hoje',
+ 'date-day-names' => array ('Domingo', 'Segunda-feira', 'Terça', 'Quarta-feira', 'Quinta-feira', 'Sexta-feira', 'Sábado'),
+ 'date-month-names' => array ('Janeiro', 'Fevereiro', 'Marcha', 'Abril', 'Pode', 'Junho', 'Julho', 'Agosto', 'Setembro', 'Outubro', 'Novembro', 'Dezembro'),
+ 'untitled' => 'Sem Título',
+ 'external-image-tip' => 'Clique para ver imagem externa',
+ 'loading' => 'Carregando...',
+ 'click-to-close' => 'Clique para fechar',
+ 'hashover-comments' => 'HashOver Comentários',
+ 'rss-feed' => 'RSS Feed',
+ 'source-code' => 'Código Fonte',
+
+ 'source-code-sub' => 'Visualizador de código fonte do lado do servidor HashOver',
+ 'type' => 'Tipo',
+ 'path' => 'Caminho',
+ 'view-as' => 'Vista como',
+ 'text' => 'Texto',
+ 'download' => 'Download',
+
+ 'documentation' => 'Documentação',
+ 'coming-soon' => 'Próxima',
+ 'example' => 'Exemplo',
+ 'back' => 'Voltar',
+ 'value' => 'Valor',
+
+ 'successful-save' => 'Gravado com sucesso!',
+ 'failed-to-save' => 'Falha ao salvar! Verifique as permissões.',
+ 'permissions-info' => 'Dê "%s" permissões 0755 e propriedade para o usuário "%s".',
+
+ 'admin' => 'Administrador',
+ 'moderation' => 'Moderação',
+ 'block-ip-addresses' => 'Bloquear Endereços IP',
+ 'filter-url-queries' => 'Filtrar consultas de URL',
+ 'check-for-updates' => 'Verificar atualizações',
+ 'settings' => 'Configurações',
+
+ 'admin-required' => 'Você deve estar logado como administrador',
+
+ 'blocklist-title' => 'Lista de Bloqueio de Endereço IP',
+ 'blocklist-sub' => 'Bloquear endereços IP específicos',
+ 'blocklist-ip-tip' => 'Endereço IP ou em branco para remover',
+
+ 'url-queries-title' => 'Consultas de URL ignoradas',
+ 'url-queries-sub' => 'Filtre quais consultas de URL devem ser ignoradas',
+ 'url-queries-name-tip' => 'Nome da consulta ou em branco para remover',
+ 'url-queries-value-tip' => 'Valor da consulta ou em branco para qualquer valor',
+
+ 'settings-sub' => 'Alterar várias configurações',
+ 'moderation-sub' => 'Publicar, editar, aprovar e excluir comentários',
+
+ 'setting-language' => 'Língua',
+ 'setting-theme' => 'Tema',
+ 'setting-uses-moderation' => 'Os comentários postados pelos usuários normais requerem moderação',
+ 'setting-pends-user-edits' => 'Os comentários editados por usuários normais requerem moderação adicional',
+ 'setting-data-format' => 'Formato de dados de comentários',
+ 'setting-default-name' => 'Nome do comentador padrão',
+ 'setting-allows-images' => 'Permitir exibição de imagens nos comentários',
+ 'setting-allows-login' => 'Permitir que os usuários façam login',
+ 'setting-allows-likes' => 'Permitir que os usuários gostem de comentários',
+ 'setting-allows-dislikes' => 'Permitir que os usuários não gostem de comentários',
+ 'setting-uses-ajax' => 'Habilitar recursos de JavaScript assíncronos',
+ 'setting-collapses-interface' => 'Reduzir toda a interface do usuário do HashOver',
+ 'setting-collapses-comments' => 'Recolher um número configurável de comentários',
+ 'setting-collapse-limit' => 'Número de comentários para colapsar',
+ 'setting-reply-mode' => 'Modo de exibição de tópicos de comentários',
+ 'setting-stream-depth' => 'Número de indenções de resposta antes do fluxo é achatado',
+ 'setting-popularity-threshold' => 'Número líquido de gostos um comentário precisa ser popular',
+ 'setting-popularity-limit' => 'Número de comentários populares a serem exibidos',
+ 'setting-uses-markdown' => 'Ative o suporte do Markdown',
+ 'setting-server-timezone' => 'Fuso horário do servidor',
+ 'setting-uses-user-timezone' => 'Exibir datas / horas no fuso horário do usuário (modo JavaScript)',
+ 'setting-uses-short-dates' => 'Habilitar datas / vezes mais curtas (exemplo "1 dia atrás")',
+ 'setting-time-format' => 'Formato de hora, use "H:i" para o formato de 24 horas',
+ 'setting-date-format' => 'Formato da data',
+ 'setting-displays-title' => 'Ativar exibição do título da página',
+ 'setting-form-position' => 'Posição para formulário de comentário primário',
+ 'setting-uses-auto-login' => 'Regista automaticamente os usuários quando eles postam comentários',
+ 'setting-shows-reply-count' => 'Exibir contagem de respostas separadamente da contagem total',
+ 'setting-count-includes-deleted' => 'Incluir comentários excluídos nas contagens de comentários',
+ 'setting-icon-mode' => 'Modo de exibição do ícone Avatar',
+ 'setting-icon-size' => 'Tamanho do ícone do Avatar',
+ 'setting-image-format' => 'Formato para ícones e outras imagens',
+ 'setting-uses-labels' => 'Exibir etiquetas acima das entradas',
+ 'setting-uses-cancel-buttons' => 'Se os formulários têm botões de cancelamento',
+ 'setting-appends-css' => 'Adiciona automaticamente HashOver CSS à página',
+ 'setting-appends-rss' => 'Adicionar HashOver RSS Feed links para a página',
+ 'setting-login-method' => 'Sistema de login do usuário',
+ 'setting-sets-cookies' => 'Habilitar cookies',
+ 'setting-secure-cookies' => 'Usar cookies seguros do HTTPS',
+ 'setting-stores-ip-address' => 'Habilitar o armazenamento de endereços IP do usuário',
+ 'setting-subscribes-user' => 'Assine o usuário para notificações por e-mail por padrão',
+ 'setting-allows-user-replies' => 'Configure o e-mail do usuário como "Responder-Para" nas notificações de resposta',
+ 'setting-noreply-email' => 'Endereço de e-mail usado quando nenhum e-mail é fornecido',
+ 'setting-spam-batabase' => 'Localização do banco de dados SPAM',
+ 'setting-spam-check-modes' => 'Modos para executar a verificação SPAM sob',
+ 'setting-gravatar-force' => 'Use imagens gravatar temáticas em vez de avatares',
+ 'setting-gravatar-default' => 'Tema gravatar padrão para usar',
+ 'setting-minifies-javascript' => 'Ativar minificação de JavaScript',
+ 'setting-minify-level' => 'Nível de minificação de JavaScript',
+ 'setting-allow-local-metadata' => 'Permitir que os metadados da página sejam atualizados do localhost'
+);
diff --git a/bootstrap/comments/backend/locales/ro.php b/bootstrap/comments/backend/locales/ro.php
new file mode 100644
index 0000000..9be48be
--- /dev/null
+++ b/bootstrap/comments/backend/locales/ro.php
@@ -0,0 +1,206 @@
+<?php
+
+// Copyright (C) 2015-2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// I, Jacob Barkdull, hereby release this work into the public domain.
+// This applies worldwide. If this is not legally possible, I grant any
+// entity the right to use this work for any purpose, without any
+// conditions, unless such conditions are required by law.
+
+
+// Romanian text for forms, buttons, links, and tooltips
+$locale = array (
+ 'comment-form' => 'Scrie comentariu aici...',
+ 'reply-form' => 'Scrie reply aici...',
+ 'comment-formatting' => 'Formatarea',
+ 'accepted-format' => 'Caractere %s acceptate',
+ 'accepted-html' => '&lt;b&gt;, &lt;strong&gt;, &lt;u&gt;, &lt;i&gt;, &lt;em&gt;, &lt;s&gt;, &lt;big&gt;, &lt;small&gt;, &lt;sup&gt;, &lt;sub&gt;, &lt;pre&gt;, &lt;ul&gt;, &lt;ol&gt;, &lt;li&gt;, &lt;blockquote&gt;, &lt;code&gt; escapes HTML, URL automat devin link si [img]URL[/img] se deschid in alt tab.',
+ 'accepted-markdown' => '**Bold**, _subliniat_, *cursiv*, ~~barate~~, `evidențiați`, ```cod``` scapă HTML. HTML și Markdown pot fi utilizate împreună în comentariu.',
+ 'post-button' => 'Posteaza Comentariu',
+ 'login' => 'Conectează-te',
+ 'login-tip' => 'Conectează-te (optionale)',
+ 'logout' => 'Deconectează-te',
+ 'be-first-name' => 'Niciun comentariu pana acum.',
+ 'pending-name' => 'În așteptarea...',
+ 'deleted-name' => 'Sters...',
+ 'error-name' => 'Eroare...',
+ 'be-first-note' => 'Fii primul care comenteaza!',
+ 'pending-note' => 'Acest comentariu este în curs de aprobare.',
+ 'deleted-note' => 'Acest comentariu a fost șters.',
+ 'error-note' => 'Ceva n-a mers bine. Nu a putut prelua acest comentariu.',
+ 'options' => 'Optiuni',
+ 'cancel' => 'Renunta',
+ 'reply-to-comment' => 'Reply la comentariu',
+ 'edit-your-comment' => 'Editare comentariu',
+ 'optional' => 'Optionale',
+ 'required' => 'Obligatoriu',
+ 'name' => 'Nume',
+ 'name-tip' => 'Nume (%s)',
+ 'password' => 'Parola',
+ 'password-tip' => 'Parola (%s, permite să editați sau să ștergeți acest comentariu)',
+ 'confirm-password' => 'Confirmă Parola',
+ 'email' => 'Adresa E-mail',
+ 'email-tip' => 'Adresa E-mail (%s, pentru notificări prin e-mail)',
+ 'website' => 'Website',
+ 'website-tip' => 'Website (%s)',
+ 'logged-in' => 'Conectare reusita!',
+ 'logged-out' => 'Conectare eșec!',
+ 'comment-needed' => 'Tu nu a reușit să introduceți un comentariu adecvat. Vă rugăm să încercați din nou.',
+ 'reply-needed' => 'Tu nu a reușit să introduceți un reply adecvat. Vă rugăm să încercați din nou.',
+ 'field-needed' => '"%s" Câmpul de Este obligatoriu.',
+ 'post-fail' => 'Eșec! Tu lipse permisiunea suficientă.',
+ 'comment-deleted' => 'Comentariu sters!',
+ 'post-reply' => 'Adauga Reply',
+ 'delete' => 'Sterge',
+ 'permanently-delete' => 'Permiteți ștergerea definitivă',
+ 'subscribe' => 'Notifica-ma de raspunsuri',
+ 'subscribe-tip' => 'Subscribe la notificari pe mail',
+ 'edit-comment' => 'Editare comentariu',
+ 'status' => 'Stare',
+ 'status-approved' => 'Aprobat',
+ 'status-pending' => 'În așteptarea aprobării',
+ 'status-deleted' => 'Marcate șters',
+ 'save' => 'Salveaza',
+ 'no-email-warning' => 'Fara adresa de e-mail, NU vei primi notificari cand cineva raspunde la comentariul tau!',
+ 'invalid-email' => 'Cele adresa de e-mail pe care ați introdus nu este valid.',
+ 'delete-comment' => 'Sigur doresti stergerea comentariului?',
+ 'post-comment-on' => array ('Adauga comentariu', 'Adauga comentariu la "%s"'),
+ 'popular-comments' => array ('Cele mai populare Comentariu', 'Cele mai populare Comentarii'),
+ 'showing-comments' => array ('Arata %d Comentariu', 'Arata %d Comentarii'),
+ 'count-link' => array ('%d Comentariu', '%d Comentarii'),
+ 'count-replies' => array ('%d numărare răspuns', '%d numărare răspunsuri'),
+ 'sort' => 'Sortare',
+ 'sort-ascending' => 'Ascendent',
+ 'sort-descending' => 'Descendent',
+ 'sort-by-date' => 'Cele mai noi',
+ 'sort-by-likes' => 'Dupa Like-uri',
+ 'sort-by-replies' => 'Dupa răspunsuri',
+ 'sort-by-discussion' => 'Dupa discuții',
+ 'sort-by-popularity' => 'Dupa popularitate',
+ 'sort-by-name' => 'Dupa user',
+ 'sort-threads' => 'Fire',
+ 'thread' => 'Ca răspuns la %s',
+ 'thread-tip' => 'Top inceput comentariu',
+ 'comments' => 'Comentarii',
+ 'replies' => 'răspunsuri',
+ 'edit' => 'Editare',
+ 'reply' => 'Reply',
+ 'like' => array ('Like', 'Like-uri'),
+ 'liked' => 'Liked',
+ 'unlike' => 'Unlike',
+ 'like-comment' => '\'Like\' acest comentariu',
+ 'liked-comment' => 'Tu \'Liked\' acest comentariu',
+ 'dislike' => array ('Dislike', 'Dislike-uri'),
+ 'disliked' => 'Disliked',
+ 'dislike-comment' => '\'Dislike\' acest comentariu',
+ 'disliked-comment' => 'Tu \'Disliked\' acest comentariu',
+ 'commenter-tip' => 'Tu nu va fi notificat prin e-mail',
+ 'subscribed-tip' => 'va fi notificat prin e-mail',
+ 'unsubscribed-tip' => 'nu este abonat la notificări prin e-mail',
+ 'show-other-comments' => array ('Arata %d Alte Comentariu', 'Arata %d Alte Comentarii'),
+ 'show-number-comments' => array ('Arata %d Comentariu', 'Arata %d Comentarii'),
+ 'date-time' => '%s \l\a %s',
+ 'date-years' => array ('%d an in urma', '%d ani in urma'),
+ 'date-months' => array ('%d lună în urmă', '%d luni în urmă'),
+ 'date-days' => array ('%d zi în urmă', '%d zile în urmă'),
+ 'date-today' => '%s astăzi',
+ 'date-day-names' => array ('Duminică', 'Luni', 'Marţi', 'Miercuri', 'Joi', 'Vineri', 'Sâmbătă'),
+ 'date-month-names' => array ('Ianuarie', 'Februarie', 'Martie', 'Aprilie', 'Mai', 'Iunie', 'Iulie', 'August', 'Septembrie', 'Octombrie', 'Noiembrie', 'Decembrie'),
+ 'untitled' => 'Fără Titlu',
+ 'external-image-tip' => 'Click pentru a vizualiza imaginea externă',
+ 'loading' => 'Se incarca...',
+ 'click-to-close' => 'Click pentru a închide',
+ 'hashover-comments' => 'HashOver Comentarii',
+ 'rss-feed' => 'RSS Feed',
+ 'source-code' => 'Cod Sursa',
+
+ 'source-code-sub' => 'Vizualizatorul codului sursă HashOver de pe server',
+ 'type' => 'Tip',
+ 'path' => 'Cale',
+ 'view-as' => 'Vizualizare ca',
+ 'text' => 'Text',
+ 'download' => 'Descărcare',
+
+ 'documentation' => 'Documentație',
+ 'coming-soon' => 'Vino în curând',
+ 'example' => 'Exemplu',
+ 'back' => 'Înapoi',
+ 'value' => 'Valoare',
+
+ 'successful-save' => 'Salvat cu succes!',
+ 'failed-to-save' => 'Sa salvat! Verificați permisiunile.',
+ 'permissions-info' => 'Acordați permisiunile "%s" 0755 și proprietatea pentru utilizatorul "%s".',
+
+ 'admin' => 'Admin',
+ 'moderation' => 'Moderare',
+ 'block-ip-addresses' => 'Blocați adresele IP',
+ 'filter-url-queries' => 'Filtre interogări URL',
+ 'check-for-updates' => 'Verificați pentru actualizări',
+ 'settings' => 'Setări',
+
+ 'admin-required' => 'Trebuie să fii logat ca administrator',
+
+ 'blocklist-title' => 'Listă de blocare a adresei IP',
+ 'blocklist-sub' => 'Blocați adresele IP specifice',
+ 'blocklist-ip-tip' => 'Adresă IP sau gol pentru a fi eliminat',
+
+ 'url-queries-title' => 'Interogări de adrese URL ignorate',
+ 'url-queries-sub' => 'Filtrați interogările adreselor URL care ar trebui ignorate',
+ 'url-queries-name-tip' => 'Nume de interogare sau gol pentru a fi eliminat',
+ 'url-queries-value-tip' => 'Valoare interogare sau gol pentru orice valoare',
+
+ 'settings-sub' => 'Schimbarea diferitelor setări',
+ 'moderation-sub' => 'Postați, editați, aprobați și ștergeți comentariile',
+
+ 'setting-language' => 'Limba',
+ 'setting-theme' => 'Tema',
+ 'setting-uses-moderation' => 'Comentarii postate de utilizatori normali necesită moderare',
+ 'setting-pends-user-edits' => 'Comentariile editate de utilizatorii normali necesită moderare suplimentară',
+ 'setting-data-format' => 'Format de date pentru comentarii',
+ 'setting-default-name' => 'Numele implicit de comentator',
+ 'setting-allows-images' => 'Permite afișarea imaginilor în comentarii',
+ 'setting-allows-login' => 'Permite utilizatorilor să se autentifice',
+ 'setting-allows-likes' => 'Permite utilizatorilor să placă comentariile',
+ 'setting-allows-dislikes' => 'Permite utilizatorilor să nu-i placă comentariile',
+ 'setting-uses-ajax' => 'Activarea caracteristicilor JavaScript asincrone',
+ 'setting-collapses-interface' => 'Restrângeți întreaga interfață utilizator HashOver',
+ 'setting-collapses-comments' => 'Reduceți numărul de comentarii configurabile',
+ 'setting-collapse-limit' => 'Număr de comentarii pentru restrângere',
+ 'setting-reply-mode' => 'Modul de afișare a firelor de comentarii',
+ 'setting-stream-depth' => 'Numărul indiciilor de răspuns înainte ca fluxul să fie aplatizat',
+ 'setting-popularity-threshold' => 'Numărul net de favoriri a unui comentariu trebuie să fie popular',
+ 'setting-popularity-limit' => 'Numărul de comentarii populare de afișat',
+ 'setting-uses-markdown' => 'Activați suportul Markdown',
+ 'setting-server-timezone' => 'Fusul orar al serverului',
+ 'setting-uses-user-timezone' => 'Afișează datele / orele în fusul orar al utilizatorului (modul JavaScript)',
+ 'setting-uses-short-dates' => 'Activați date / ore mai scurte (exemplu "acum 1 zi")',
+ 'setting-time-format' => 'Format de timp, utilizați "H:i" pentru formatul de 24 de ore',
+ 'setting-date-format' => 'Formatul datei',
+ 'setting-displays-title' => 'Activați afișarea titlului paginii',
+ 'setting-form-position' => 'Poziția pentru formularul de comentariu primar',
+ 'setting-uses-auto-login' => 'Inregistreaza automat utilizatorii atunci cand posteaza comentarii',
+ 'setting-shows-reply-count' => 'Afișați răspunsul afișat separat din numărul total',
+ 'setting-count-includes-deleted' => 'Include comentariile șterse în conturile de comentarii',
+ 'setting-icon-mode' => 'Modul de afișare a imaginilor în Avatar',
+ 'setting-icon-size' => 'Dimensiunea pictogramei Avatar',
+ 'setting-image-format' => 'Format pentru pictograme și alte imagini',
+ 'setting-uses-labels' => 'Afișați etichete deasupra intrărilor',
+ 'setting-uses-cancel-buttons' => 'Dacă formularele au butoane de anulare',
+ 'setting-appends-css' => 'Adăugați automat HashOver CSS în pagină',
+ 'setting-appends-rss' => 'Adăugați link-uri HashOver RSS feeds to page',
+ 'setting-login-method' => 'Sistem de conectare utilizator',
+ 'setting-sets-cookies' => 'Activați cookie-urile',
+ 'setting-secure-cookies' => 'Utilizați cookie-urile securizate HTTPS',
+ 'setting-stores-ip-address' => 'Activați stocarea adreselor IP utilizator',
+ 'setting-subscribes-user' => 'Abonează-l pe utilizator să trimită notificări prin e-mail în mod implicit',
+ 'setting-allows-user-replies' => 'Setați e-mail-ul utilizator ca "Răspundeți-vă" în notificările de răspuns',
+ 'setting-noreply-email' => 'Adresa de e-mail utilizată atunci când nu este dat un e-mail',
+ 'setting-spam-batabase' => 'Locația bazei de date SPAM',
+ 'setting-spam-check-modes' => 'Moduri pentru a efectua verificarea SPAM sub',
+ 'setting-gravatar-force' => 'Folosiți imagini Gravatar tematice în loc de avataruri',
+ 'setting-gravatar-default' => 'Tema Gravatar implicită pentru utilizare',
+ 'setting-minifies-javascript' => 'Activați minificarea JavaScript',
+ 'setting-minify-level' => 'Nivel de minime JavaScript',
+ 'setting-allow-local-metadata' => 'Permiteți actualizarea metadatelor de pagină de la localhost'
+);
diff --git a/bootstrap/comments/backend/locales/tr.php b/bootstrap/comments/backend/locales/tr.php
new file mode 100644
index 0000000..49e48bf
--- /dev/null
+++ b/bootstrap/comments/backend/locales/tr.php
@@ -0,0 +1,206 @@
+<?php
+
+// Copyright (C) 2015-2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// I, Jacob Barkdull, hereby release this work into the public domain.
+// This applies worldwide. If this is not legally possible, I grant any
+// entity the right to use this work for any purpose, without any
+// conditions, unless such conditions are required by law.
+
+
+// Turkish text for forms, buttons, links, and tooltips
+$locale = array (
+ 'comment-form' => 'Yorumunuzu buraya yazın...',
+ 'reply-form' => 'Cevabınızı buraya yazın...',
+ 'comment-formatting' => 'Biçimlendirme',
+ 'accepted-format' => 'Kabul edilen %s',
+ 'accepted-html' => '&lt;b&gt;, &lt;strong&gt;, &lt;u&gt;, &lt;i&gt;, &lt;em&gt;, &lt;s&gt;, &lt;big&gt;, &lt;small&gt;, &lt;sup&gt;, &lt;sub&gt;, &lt;pre&gt;, &lt;ul&gt;, &lt;ol&gt;, &lt;li&gt;, &lt;blockquote&gt;, &lt;code&gt; HTML\'den kaçar, URL\'ler otomatik olarak bağlantı olur ve [img]URL burada[/img] harici bir görüntü gösterecektir.',
+ 'accepted-markdown' => '**Koyu**, _altı çizili_, *italik*, ~~üstü çizili~~, `vurgulayın`, ```kod``` HTML\'den çıkar. HTML ve Markdown, yorumunuzda birlikte kullanılabilir.',
+ 'post-button' => 'Gönder',
+ 'login' => 'Giriş Yap',
+ 'login-tip' => 'Giriş Yap (opsiyonel)',
+ 'logout' => 'Çıkış Yap',
+ 'be-first-name' => 'Henüz yorumu yok.',
+ 'pending-name' => 'Bekleyen...',
+ 'deleted-name' => 'Silindi...',
+ 'error-name' => 'Hata...',
+ 'be-first-note' => 'İlk yorumu siz yapın!',
+ 'pending-note' => 'Bu yorum onayı bekliyor.',
+ 'deleted-note' => 'Bu yorum silindi.',
+ 'error-note' => 'Bir şeyler yanlış gitti. Bu yorumu alınamadı.',
+ 'options' => 'Ayarlar',
+ 'cancel' => 'İptal',
+ 'reply-to-comment' => 'Yorumu cevapla',
+ 'edit-your-comment' => 'Yorumunuzu değiştirin',
+ 'optional' => 'Opsiyonel',
+ 'required' => 'Gerekiyor',
+ 'name' => 'İsim',
+ 'name-tip' => 'İsim (%s)',
+ 'password' => 'Şifre',
+ 'password-tip' => 'Şifre (%s, Düzenlemek veya bu yorumunu silmek için izin verir)',
+ 'confirm-password' => 'Şifreyi Onayla',
+ 'email' => 'E-posta adresi',
+ 'email-tip' => 'E-posta adresi (%s, e-posta ile uyarılar için)',
+ 'website' => 'Website',
+ 'website-tip' => 'Website (%s)',
+ 'logged-in' => 'Giriş yaptınız!',
+ 'logged-out' => 'Çıkış yaptınız!',
+ 'comment-needed' => 'Düzgün bir yorum olmadı bu. Lütfen tekrar deneyin.',
+ 'reply-needed' => 'Düzgün bir cevap olmadı bu. Lütfen tekrar deneyin.',
+ 'field-needed' => '"%s" alanı gerekiyor.',
+ 'post-fail' => 'Gönderirken hatalar oluştu. Yeterli izinlere sahip değilsiniz.',
+ 'comment-deleted' => 'Yorum silindi!',
+ 'post-reply' => 'Cevapla',
+ 'delete' => 'Sil',
+ 'permanently-delete' => 'Kalıcı olarak sil',
+ 'subscribe' => 'Cevaplardan beni haberdar edin',
+ 'subscribe-tip' => 'E-posta uyarılarına kayıt ol',
+ 'edit-comment' => 'Yorumu değiştir',
+ 'status' => 'Durum',
+ 'status-approved' => 'Onaylı',
+ 'status-pending' => 'Onay bekleyen',
+ 'status-deleted' => 'İşaretli silindi',
+ 'save' => 'Kaydet',
+ 'no-email-warning' => 'E-posta girişi yapmanız yorumunuza gelen cevaplara uyarı alamayacaksınız.',
+ 'invalid-email' => 'Girdiğiniz e-posta adresi geçerli değil.',
+ 'delete-comment' => 'Bu yorumu silmek istediğinizden emin misiniz',
+ 'post-comment-on' => array ('Bir yorum yap', 'Bir yorum yap on "%s"'),
+ 'popular-comments' => array ('En popüler yorum', 'En popüler yorumlar'),
+ 'showing-comments' => array ('%d tane yorum gösteriliyor', '%d yorum gösteriliyor'),
+ 'count-link' => array ('%d yorum', '%d yorum'),
+ 'count-replies' => array ('%d cevap', '%d cevap'),
+ 'sort' => 'Sırala',
+ 'sort-ascending' => 'Sırayla',
+ 'sort-descending' => 'Ters',
+ 'sort-by-date' => 'Yeni olan önce',
+ 'sort-by-likes' => 'Beğenme göre',
+ 'sort-by-replies' => 'Cevapların göre',
+ 'sort-by-discussion' => 'Tartışma göre',
+ 'sort-by-popularity' => 'Popülariteye göre',
+ 'sort-by-name' => 'Yorum yapana göre',
+ 'sort-threads' => 'Ipler',
+ 'thread' => '%s\'a cevaben',
+ 'thread-tip' => 'Konunun başına dön',
+ 'comments' => 'Yorum',
+ 'replies' => 'Cevap',
+ 'edit' => 'Düzenle',
+ 'reply' => 'Cevap',
+ 'like' => array ('Beğenme', 'Beğenme'),
+ 'liked' => 'Beğendi',
+ 'unlike' => 'Unlike',
+ 'like-comment' => 'Bu yorumu \'beğen\'',
+ 'liked-comment' => 'Bu yorumu \'beğendiniz\'',
+ 'dislike' => array ('Hoşlanmayan', 'Hoşlanmayan'),
+ 'disliked' => 'Beğenmedi',
+ 'dislike-comment' => 'Bu yorumu \'beğenme\' ',
+ 'disliked-comment' => 'Bu yorumu \'beğenmediniz\'',
+ 'commenter-tip' => 'E-posta ile bilgilendirilmeyeceksiniz',
+ 'subscribed-tip' => 'e-posta ile bilgilendirilecek.',
+ 'unsubscribed-tip' => 'e-posta ile bilgilendirmeye kayıt olmamış',
+ 'show-other-comments' => array ('Diğer %d yorumu göster', 'Diğer %d yorumu göster'),
+ 'show-number-comments' => array ('Diğer %d yorumu göster', 'Diğer %d yorumu göster'),
+ 'date-time' => '%s \s\a\a\t %s',
+ 'date-years' => array ('%d yıl önce', '%d yıl önce'),
+ 'date-months' => array ('%d ay önce', '%d ay önce'),
+ 'date-days' => array ('%d gün önce', '%d gün önce'),
+ 'date-today' => '%s bugün',
+ 'date-day-names' => array ('Pazar', 'Pazartesi', 'Salı', 'Çarşamba', 'Perşembe', 'Cuma', 'Cumartesi'),
+ 'date-month-names' => array ('Ocak ayı', 'Şubat ayı', 'Mart', 'Nisan', 'Mayıs ayı', 'Haziran', 'Temmuz', 'Ağustos', 'Eylül', 'Ekim', 'Kasım', 'Aralık'),
+ 'untitled' => 'Başlıksız',
+ 'external-image-tip' => 'Harici resmi görüntülemek için tıklayın',
+ 'loading' => 'Yükleniyor...',
+ 'click-to-close' => 'Kapatmak için tıklayın',
+ 'hashover-comments' => 'HashOver Yorum',
+ 'rss-feed' => 'RSS Kaynağı',
+ 'source-code' => 'Kaynak Kodu',
+
+ 'source-code-sub' => 'HashOver sunucu tarafı kaynak kodu görüntüleyici',
+ 'type' => 'Tipi',
+ 'path' => 'Yolu',
+ 'view-as' => 'Olarak görüntüle',
+ 'text' => 'Metin',
+ 'download' => 'İndir',
+
+ 'documentation' => 'Belgeler',
+ 'coming-soon' => 'Yakında',
+ 'example' => 'Örnek',
+ 'back' => 'Geri',
+ 'value' => 'Değer',
+
+ 'successful-save' => 'Başarıyla kaydedildi!',
+ 'failed-to-save' => 'Kaydedilemedi! İzinleri kontrol et.',
+ 'permissions-info' => 'Ver "%s" izinleri 0755 ve "%s" kullanıcısına sahiplik.',
+
+ 'admin' => 'Yönetici',
+ 'moderation' => 'Moderasyon',
+ 'block-ip-addresses' => 'IP Adreslerini Engelle',
+ 'filter-url-queries' => 'URL Sorgularını Filtrele',
+ 'check-for-updates' => 'Güncellemeleri Kontrol Et',
+ 'settings' => 'Ayarlar',
+
+ 'admin-required' => 'Yönetici olarak giriş yapmalısınız',
+
+ 'blocklist-title' => 'IP Adresi Engelleme Listesi',
+ 'blocklist-sub' => 'Belirli IP adreslerini engelle',
+ 'blocklist-ip-tip' => 'Kaldırılacak IP Adresi veya boş',
+
+ 'url-queries-title' => 'Yok Sayılan URL Sorguları',
+ 'url-queries-sub' => 'Hangi URL sorgularının dikkate alınmaması gerektiğini filtreleyin',
+ 'url-queries-name-tip' => 'Sorgu adı veya kaldırmak için boş',
+ 'url-queries-value-tip' => 'Herhangi bir değer için sorgu değeri veya boş',
+
+ 'settings-sub' => 'Değişik ayarları değiştir',
+ 'moderation-sub' => 'Yorumları yayınla, düzenle, onayla ve sil',
+
+ 'setting-language' => 'Dil',
+ 'setting-theme' => 'Tema',
+ 'setting-uses-moderation' => 'Normal kullanıcılar tarafından gönderilen yorumlar ılımlılık gerektirir',
+ 'setting-pends-user-edits' => 'Normal kullanıcılar tarafından düzenlenen yorumlar ek denetleme gerektirir',
+ 'setting-data-format' => 'Yorum veri formatı',
+ 'setting-default-name' => 'Varsayılan yorum yazan adı',
+ 'setting-allows-images' => 'Görüntülerde görüntülerin görüntülenmesine izin ver',
+ 'setting-allows-login' => 'Kullanıcıların giriş yapmasına izin ver',
+ 'setting-allows-likes' => 'Kullanıcıların yorumları beğenmesine izin ver',
+ 'setting-allows-dislikes' => 'Kullanıcıların yorumları beğenmemesine izin ver',
+ 'setting-uses-ajax' => 'Eşzamansız JavaScript özelliklerini etkinleştir',
+ 'setting-collapses-interface' => 'HashOver kullanıcı arayüzünün tamamını daralt',
+ 'setting-collapses-comments' => 'Yapılandırılabilir bir yorum sayısını daralt',
+ 'setting-collapse-limit' => 'Çökecek yorum sayısı',
+ 'setting-reply-mode' => 'Yorum dizilerinin görüntüleme modu',
+ 'setting-stream-depth' => 'Akış düzleştirilmeden önce yanıt girintileri sayısı',
+ 'setting-popularity-threshold' => 'Yorumların popüler sayısının popüler olması gerekiyor',
+ 'setting-popularity-limit' => 'Gösterilecek popüler yorumların sayısı',
+ 'setting-uses-markdown' => 'Markdown desteğini etkinleştir',
+ 'setting-server-timezone' => 'Sunucu saat dilimi',
+ 'setting-uses-user-timezone' => 'Kullanıcıların saat dilimlerinde (JavaScript modu) tarihleri ​​/ saatleri göster',
+ 'setting-uses-short-dates' => 'Daha kısa tarihleri ​​/ saatleri etkinleştir (örnek "1 gün önce")',
+ 'setting-time-format' => 'Saat formatı, 24 saat formatı için "H:i" yi kullanın.',
+ 'setting-date-format' => 'Tarih formatı',
+ 'setting-displays-title' => 'Sayfa başlığının görüntülenmesini etkinleştir',
+ 'setting-form-position' => 'Birincil yorum formu için konum',
+ 'setting-uses-auto-login' => 'Kullanıcıları yorum gönderdiklerinde otomatik olarak günlüğe kaydet',
+ 'setting-shows-reply-count' => 'Yanıt sayısını toplam sayımdan ayrı göster',
+ 'setting-count-includes-deleted' => 'Yorum sayımlarında silinen yorumları dahil et',
+ 'setting-icon-mode' => 'Avatar simgesi görüntüleme modu',
+ 'setting-icon-size' => 'Avatar simgesi boyutu',
+ 'setting-image-format' => 'Simgeler ve diğer resimler için format',
+ 'setting-uses-labels' => 'Girişlerin üzerindeki etiketler göster',
+ 'setting-uses-cancel-buttons' => 'Formların iptal butonları olup olmadığı',
+ 'setting-appends-css' => 'HashOver CSS\'yi otomatik olarak sayfaya ekle',
+ 'setting-appends-rss' => 'HashOver RSS Feed linklerini sayfaya ekleyin',
+ 'setting-login-method' => 'Kullanıcı giriş sistemi',
+ 'setting-sets-cookies' => 'Çerezleri etkinleştir',
+ 'setting-secure-cookies' => 'Güvenli HTTPS sadece çerezleri kullan',
+ 'setting-stores-ip-address' => 'Kullanıcı IP adreslerinin saklanmasını etkinleştir',
+ 'setting-subscribes-user' => 'Varsayılan olarak bildirimleri e-postaya göndermek için kullanıcıya abone olun',
+ 'setting-allows-user-replies' => 'Kullanıcı e-postasını cevap bildirimlerinde "Cevapla" olarak ayarla',
+ 'setting-noreply-email' => 'E-posta verilmediğinde kullanılan e-posta adresi',
+ 'setting-spam-batabase' => 'SPAM veritabanı konumu',
+ 'setting-spam-check-modes' => 'SPAM denetimi gerçekleştirecek modlar',
+ 'setting-gravatar-force' => 'Avatar yerine temalı Gravatar resimleri kullan',
+ 'setting-gravatar-default' => 'Kullanılacak varsayılan Gravatar teması',
+ 'setting-minifies-javascript' => 'JavaScript küçültmeyi etkinleştir',
+ 'setting-minify-level' => 'JavaScript minification level',
+ 'setting-allow-local-metadata' => 'Sayfa meta verilerinin localhost\'tan güncellenmesine izin ver'
+);
diff --git a/bootstrap/comments/backend/locales/zh-cn.php b/bootstrap/comments/backend/locales/zh-cn.php
new file mode 100644
index 0000000..81826f7
--- /dev/null
+++ b/bootstrap/comments/backend/locales/zh-cn.php
@@ -0,0 +1,206 @@
+<?php
+
+// Copyright (C) 2015-2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// I, Jacob Barkdull, hereby release this work into the public domain.
+// This applies worldwide. If this is not legally possible, I grant any
+// entity the right to use this work for any purpose, without any
+// conditions, unless such conditions are required by law.
+
+
+// Simplified Chinese text for forms, buttons, links, and tooltips
+$locale = array (
+ 'comment-form' => '在这里写点什么吧…',
+ 'reply-form' => '请在这里输入你要回复的内容…',
+ 'comment-formatting' => '格式化',
+ 'accepted-format' => '接受%s',
+ 'accepted-html' => '&lt;b&gt;,&lt;u&gt;,&lt;i&gt;,&lt;s&gt;,&lt;big&gt;,&lt;em&gt;,&lt;small&gt;,&lt;strong&gt;,&lt;sub&gt;,&lt;sup&gt;,&lt;pre&gt;,&lt;ul&gt;,&lt;ol&gt;,&lt;li&gt;,&lt;blockquote&gt;,&lt;code&gt;转义HTML,网址自动成为链接,[img]URL在这里[/img] 将显示外部图像。',
+ 'accepted-markdown' => '**粗体**,_强调_,*斜体*,~~删除线~~,`突出`,```code```转义HTML。 HTML和Markdown可以在您的评论中一起使用。',
+ 'post-button' => '发表评论',
+ 'login' => '登陆',
+ 'login-tip' => '登陆 (可选)',
+ 'logout' => '退出',
+ 'be-first-name' => '沙发空缺中,还不快抢?',
+ 'pending-name' => '请稍后…',
+ 'deleted-name' => '已删除…',
+ 'error-name' => '出错了…',
+ 'be-first-note' => '写下第一个评论!',
+ 'pending-note' => '此评论正在等待批准。',
+ 'deleted-note' => '此评论已经被删除了。',
+ 'error-note' => '出了些问题。无法查看此评论。',
+ 'options' => '选项',
+ 'cancel' => '取消',
+ 'reply-to-comment' => '回复评论',
+ 'edit-your-comment' => '编辑你的评论',
+ 'optional' => '可选',
+ 'required' => '必填',
+ 'name' => '昵称',
+ 'name-tip' => '昵称 (%s)',
+ 'password' => '密码',
+ 'password-tip' => '密码 (%s,允许您编辑或删除此评论)',
+ 'confirm-password' => '确认密码',
+ 'email' => '邮箱',
+ 'email-tip' => '邮箱 (%s,用于接收通知邮件)',
+ 'website' => '网址',
+ 'website-tip' => '网址 (%s)',
+ 'logged-in' => '您已成功登录!',
+ 'logged-out' => '您已成功退出!',
+ 'comment-needed' => '您未能输入正确的评论。使用下面的表格。',
+ 'reply-needed' => '您未能输入正确的回复。使用下面的表格。',
+ 'field-needed' => '「%s」是必须要填写的。',
+ 'post-fail' => 'Oops!你没有足够的权限。',
+ 'comment-deleted' => '评论已删除!',
+ 'post-reply' => '发表回复',
+ 'delete' => '删除',
+ 'permanently-delete' => '永久删除',
+ 'subscribe' => '有回复通知我',
+ 'subscribe-tip' => '订阅电子邮件通知',
+ 'edit-comment' => '编辑评论',
+ 'status' => '状态',
+ 'status-approved' => '批准',
+ 'status-pending' => '等待审核',
+ 'status-deleted' => '标记为已删除',
+ 'save' => '保存',
+ 'no-email-warning' => '如果不填写邮箱,你将不会收到对你评论的回复通知。',
+ 'invalid-email' => '输入的邮箱是无效的。',
+ 'delete-comment' => '你确定要删除此评论吗?',
+ 'post-comment-on' => array ('发表评论', '在「%s」发表评论'),
+ 'popular-comments' => array ('热门评论', '热门评论'),
+ 'showing-comments' => array ('目前有%d条评论', '目前有%d条评论'),
+ 'count-link' => array ('%d条评论', '%d条评论'),
+ 'count-replies' => array ('%d条回复', '%d条回复'),
+ 'sort' => '类别',
+ 'sort-ascending' => '默认',
+ 'sort-descending' => '以相反的顺序',
+ 'sort-by-date' => '最新发表优先',
+ 'sort-by-likes' => '点赞最多优先',
+ 'sort-by-replies' => '回复最多优先',
+ 'sort-by-discussion' => '讨论模式',
+ 'sort-by-popularity' => '人气模式',
+ 'sort-by-name' => '评论者模式',
+ 'sort-threads' => '更多模式',
+ 'thread' => '回复%s',
+ 'thread-tip' => '返回顶部',
+ 'comments' => '条评论',
+ 'replies' => '条回复',
+ 'edit' => '编辑',
+ 'reply' => '回复',
+ 'like' => array ('个赞', '个赞'),
+ 'liked' => '已点赞',
+ 'unlike' => '不赞了',
+ 'like-comment' => '「点赞」此评论',
+ 'liked-comment' => '取消此评论的赞',
+ 'dislike' => array ('不喜欢', '不喜欢'),
+ 'disliked' => '不喜欢',
+ 'dislike-comment' => '「不喜欢」这条评论',
+ 'disliked-comment' => '你已经「不喜欢」这条评论了',
+ 'commenter-tip' => '您的邮箱将不会收到通知',
+ 'subscribed-tip' => '将通过邮箱通知',
+ 'unsubscribed-tip' => '未订阅邮箱通知',
+ 'show-other-comments' => array ('显示其它%d条评论', '显示其它%d条评论'),
+ 'show-number-comments' => array ('已有%d条评论', '已有%d条评论'),
+ 'date-time' => '%s在%s',
+ 'date-years' => array ('%d年以前', '%d年以前'),
+ 'date-months' => array ('%d月以前', '%d月以前'),
+ 'date-days' => array ('%d天前', '%d天前'),
+ 'date-today' => '今天%s',
+ 'date-day-names' => array ('星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'),
+ 'date-month-names' => array ('1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'),
+ 'untitled' => '无标题',
+ 'external-image-tip' => '点击查看外部图像',
+ 'loading' => '加载中…',
+ 'click-to-close' => '点击关闭',
+ 'hashover-comments' => 'HashOver评论',
+ 'rss-feed' => 'RSS订阅',
+ 'source-code' => '源代码',
+
+ 'source-code-sub' => 'HashOver服務器端源代碼查看器',
+ 'type' => '类型',
+ 'path' => '路径',
+ 'view-as' => '查看为',
+ 'text' => '文本',
+ 'download' => '下载',
+
+ 'documentation' => '文档',
+ 'coming-soon' => '即将推出',
+ 'example' => '示例',
+ 'back' => '返回',
+ 'value' => '价值',
+
+ 'successful-save' => '成功保存!',
+ 'failed-to-save' => '无法保存!检查权限。',
+ 'permissions-info' => '给/“%s”权限0755和“%s”用户的所有权。',
+
+ 'admin' => '管理员',
+ 'moderation' => '审核',
+ 'block-ip-addresses' => '阻止IP地址',
+ 'filter-url-queries' => '过滤网址查询',
+ 'check-for-updates' => '检查更新',
+ 'settings' => '设置',
+
+ 'admin-required' => '您必须以管理员身份登录',
+
+ 'blocklist-title' => 'IP地址阻止列表',
+ 'blocklist-sub' => '阻止特定的IP地址',
+ 'blocklist-ip-tip' => 'IP地址或空白删除',
+
+ 'url-queries-title' => '忽略的网址查询',
+ 'url-queries-sub' => '过滤哪些URL查询应该被忽略',
+ 'url-queries-name-tip' => '查询名称或空白删除',
+ 'url-queries-value-tip' => '查询值或空白的任何值',
+
+ 'settings-sub' => '更改各种设置',
+ 'moderation-sub' => '发布,编辑,批准和删除评论',
+
+ 'setting-language' => '语言',
+ 'setting-theme' => '主题',
+ 'setting-uses-moderation' => '普通用户发布的评论需要审核',
+ 'setting-pends-user-edits' => '普通用户编辑的评论需要额外的审核',
+ 'setting-data-format' => '评论数据格式',
+ 'setting-default-name' => '默认评论者名称',
+ 'setting-allows-images' => '允许在评论中显示图片',
+ 'setting-allows-login' => '允许用户登录',
+ 'setting-allows-likes' => '允许用户评论',
+ 'setting-allows-dislikes' => '允许用户不喜欢评论',
+ 'setting-uses-ajax' => '启用异步JavaScript功能',
+ 'setting-collapses-interface' => '折叠整个HashOver用户界面',
+ 'setting-collapses-comments' => '折叠可配置的评论数量',
+ 'setting-collapse-limit' => '要折叠的评论数量',
+ 'setting-reply-mode' => '评论帖子的显示模式',
+ 'setting-stream-depth' => '数据流在数据流平坦化之前',
+ 'setting-popularity-threshold' => '评论需要流行的喜欢的网络数量',
+ 'setting-popularity-limit' => '显示的热门评论数量',
+ 'setting-uses-markdown' => '启用Markdown支持',
+ 'setting-server-timezone' => '服务器时区',
+ 'setting-uses-user-timezone' => '在用户的时区(JavaScript模式)中显示日期/时间',
+ 'setting-uses-short-dates' => '启用较短的日期/时间(例如“1天前”',
+ 'setting-time-format' => '时间格式,使用“H:i”24小时格式“',
+ 'setting-date-format' => '日期格式',
+ 'setting-displays-title' => '启用页面标题的显示',
+ 'setting-form-position' => '主评论表单的位置',
+ 'setting-uses-auto-login' => '当他们发表评论时自动登录用户',
+ 'setting-shows-reply-count' => '显示回复次数与总次数分开',
+ 'setting-count-includes-deleted' => '在评论计数中包含已删除的评论',
+ 'setting-icon-mode' => '阿凡达图标显示模式',
+ 'setting-icon-size' => '阿凡达图标大小',
+ 'setting-image-format' => '图标和其他图像的格式',
+ 'setting-uses-labels' => '在输入上显示标签',
+ 'setting-uses-cancel-buttons' => '表单是否有取消按钮',
+ 'setting-appends-css' => '自动添加HashOver CSS到页面',
+ 'setting-appends-rss' => '将HashOver RSS Feed链接添加到页面',
+ 'setting-login-method' => '用户登录系统',
+ 'setting-sets-cookies' => '启用Cookie',
+ 'setting-secure-cookies' => '使用安全的HTTPS专用Cookie',
+ 'setting-stores-ip-address' => '启用存储用户IP地址',
+ 'setting-subscribes-user' => '订阅用户默认通过电子邮件通知',
+ 'setting-allows-user-replies' => '将用户电子邮件设置为“答复通知”中的“回复”',
+ 'setting-noreply-email' => '未给出电子邮件时使用的电子邮件地址',
+ 'setting-spam-batabase' => 'SPAM数据库位置',
+ 'setting-spam-check-modes' => '在下执行垃圾邮件检查的模式',
+ 'setting-gravatar-force' => '使用主题Gravatar图像而不是化身',
+ 'setting-gravatar-default' => '使用默认的Gravatar主题',
+ 'setting-minifies-javascript' => '启用JavaScript缩小',
+ 'setting-minify-level' => 'JavaScript缩小级别',
+ 'setting-allow-local-metadata' => '允许页面元数据从本地主机更新'
+);
diff --git a/bootstrap/comments/backend/nocache-headers.php b/bootstrap/comments/backend/nocache-headers.php
new file mode 100644
index 0000000..b0475dc
--- /dev/null
+++ b/bootstrap/comments/backend/nocache-headers.php
@@ -0,0 +1,25 @@
+<?php namespace HashOver;
+
+// Copyright (C) 2017-2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// HashOver is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// HashOver is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with HashOver. If not, see <http://www.gnu.org/licenses/>.
+
+
+// Disable browser cache
+header ('Expires: Wed, 08 May 1991 12:00:00 GMT');
+header ('Last-Modified: ' . gmdate ('D, d M Y H:i:s') . ' GMT');
+header ('Cache-Control: no-store, no-cache, must-revalidate');
+header ('Cache-Control: post-check=0, pre-check=0', false);
+header ('Pragma: no-cache');
diff --git a/bootstrap/comments/backend/php-setup.php b/bootstrap/comments/backend/php-setup.php
new file mode 100644
index 0000000..7f2aa5b
--- /dev/null
+++ b/bootstrap/comments/backend/php-setup.php
@@ -0,0 +1,33 @@
+<?php namespace HashOver;
+
+// Copyright (C) 2017-2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// HashOver is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// HashOver is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with HashOver. If not, see <http://www.gnu.org/licenses/>.
+
+
+// Do some standard HashOver setup work
+require ('standard-setup.php');
+
+// Autoload class files
+spl_autoload_register (function ($uri) {
+ $uri = str_replace ('\\', '/', strtolower ($uri));
+ $class_name = basename ($uri);
+
+ // Display error if class file could not be included
+ if (!@include ('classes/' . $class_name . '.php')) {
+ echo '"' . $class_name . '.php" file could not be included!';
+ exit;
+ }
+});
diff --git a/bootstrap/comments/backend/source-viewer.html b/bootstrap/comments/backend/source-viewer.html
new file mode 100644
index 0000000..b6573c3
--- /dev/null
+++ b/bootstrap/comments/backend/source-viewer.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+
+<html lang="en" dir="ltr">
+ <head>
+ <title>HashOver - {hashover:title}</title>
+
+ <link type="text/css" href="../themes/default/general.css" rel="stylesheet">
+ <link type="text/css" href="../themes/default/special.css" rel="stylesheet">
+
+ <meta http-equiv="Content-type" content="text/html; charset=utf-8">
+ <meta http-equiv="Content-Language" content="EN">
+
+ <link type="image/x-icon" href="../images/favicon.png" rel="shortcut icon">
+ <link type="image/x-icon" href="../images/favicon.png" rel="icon">
+ <link href="https://www.gnu.org/licenses/agpl" rel="copyright">
+ </head>
+
+ <body>
+ <h1 class="underlined">
+ {hashover:title}
+ </h1>
+
+ <p class="muted-text">{hashover:sub-title}</p>
+
+ {hashover:files}
+ </body>
+</html>
diff --git a/bootstrap/comments/backend/source-viewer.php b/bootstrap/comments/backend/source-viewer.php
new file mode 100644
index 0000000..26bcc6e
--- /dev/null
+++ b/bootstrap/comments/backend/source-viewer.php
@@ -0,0 +1,121 @@
+<?php namespace HashOver;
+
+// Copyright (C) 2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// HashOver is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// HashOver is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with HashOver. If not, see <http://www.gnu.org/licenses/>.
+
+
+// Do some standard HashOver setup work
+require ('php-setup.php');
+
+try {
+ // Instantiate SourceCode class
+ $source_code = new SourceCode ();
+
+ // Check if a file is requested
+ if (isset ($_GET['file'])) {
+ // Get return type
+ $type = !empty ($_GET['type']) ? $_GET['type'] : 'text';
+
+ // Display source code
+ $source_code->display ($_GET['file'], $type);
+ } else {
+ // Instantiate HashOver class
+ $hashover = new \HashOver ();
+ $hashover->initiate ();
+ $hashover->finalize ();
+
+ // Create table for source code files
+ $table = new HTMLTag ('table', array (
+ 'id' => 'threads',
+ 'class' => 'striped-rows-even column-borders',
+ 'cellspacing' => '0',
+ 'cellpadding' => '4'
+ ));
+
+ // Append column headers row
+ $table->appendChild (new HTMLTag ('tr', array (
+ 'children' => array (
+ new HTMLTag ('td', new HTMLTag ('b', array (
+ 'innerHTML' => $hashover->locale->text['type']
+ ), false), false),
+
+ new HTMLTag ('td', new HTMLTag ('b', array (
+ 'innerHTML' => $hashover->locale->text['name']
+ ), false), false),
+
+ new HTMLTag ('td', new HTMLTag ('b', array (
+ 'innerHTML' => $hashover->locale->text['path']
+ ), false), false),
+
+ new HTMLTag ('td', new HTMLTag ('b', array (
+ 'innerHTML' => $hashover->locale->text['view-as']
+ ), false), false)
+ )
+ )));
+
+ // Run through HashOver files array
+ foreach ($source_code->files as $file) {
+ $path = $hashover->setup->getHttpPath ($file['path']);
+ $name = !empty ($file['name']) ? $file['name'] : basename ($path);
+ $href = '?file=' . $file['path'];
+
+ // Create row and columns
+ $tr = new HTMLTag ('tr', array (
+ 'children' => array (
+ new HTMLTag ('td', $file['type'], false),
+ new HTMLTag ('td', $name, false),
+ new HTMLTag ('td', $path, false)
+ )
+ ));
+
+ // Append view formats column
+ $tr->appendChild (new HTMLTag ('td', array (
+ 'class' => 'margin-right-children',
+
+ 'children' => array (
+ new HTMLTag ('a', array (
+ 'href' => $href . '&amp;type=text',
+ 'innerHTML' => $hashover->locale->text['text']
+ ), false),
+
+ new HTMLTag ('a', array (
+ 'href' => $href . '&amp;type=html',
+ 'innerHTML' => 'HTML'
+ ), false),
+
+ new HTMLTag ('a', array (
+ 'href' => $href . '&amp;type=download',
+ 'innerHTML' => $hashover->locale->text['download']
+ ), false)
+ )
+ )));
+
+ // Append row to table
+ $table->appendChild ($tr);
+ }
+
+ // Load and parse HTML template
+ echo $hashover->templater->parseTemplate ('source-viewer.html', array (
+ 'title' => $hashover->locale->text['source-code'],
+ 'sub-title' => $hashover->locale->text['source-code-sub'],
+ 'files' => $table->asHTML ("\t\t")
+ ));
+ }
+} catch (\Exception $error) {
+ $misc = new Misc ('php');
+ $message = $error->getMessage ();
+ $misc->displayError ($message);
+}
diff --git a/bootstrap/comments/backend/standard-setup.php b/bootstrap/comments/backend/standard-setup.php
new file mode 100644
index 0000000..0f95a5e
--- /dev/null
+++ b/bootstrap/comments/backend/standard-setup.php
@@ -0,0 +1,25 @@
+<?php namespace HashOver;
+
+// Copyright (C) 2017-2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// HashOver is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// HashOver is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with HashOver. If not, see <http://www.gnu.org/licenses/>.
+
+
+// Use UTF-8 character set
+ini_set ('default_charset', 'UTF-8');
+
+// Enable display of PHP errors
+ini_set ('display_errors', true);
+error_reporting (E_ALL);
diff --git a/bootstrap/comments/comments.php b/bootstrap/comments/comments.php
new file mode 100644
index 0000000..d030ac8
--- /dev/null
+++ b/bootstrap/comments/comments.php
@@ -0,0 +1,265 @@
+<?php namespace HashOver;
+
+// Copyright (C) 2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// HashOver is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// HashOver is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with HashOver. If not, see <http://www.gnu.org/licenses/>.
+
+
+// Setup HashOver for JavaScript
+require ('backend/javascript-setup.php');
+
+try {
+ // Instantiate general setup class
+ $setup = new Setup (array (
+ 'mode' => 'javascript',
+ 'context' => 'normal'
+ ));
+
+ // Instantiate HashOver statistics class
+ $statistics = new Statistics ('javascript');
+
+ // Start execution timer
+ $statistics->executionStart ();
+
+ // Instantiate JavaScript build class
+ $javascript = new JavaScriptBuild ('frontend');
+
+ // Register initial constructor
+ $javascript->registerFile ('constructor.js');
+
+ // Register HashOver script tag getter method
+ $javascript->registerFile ('script.js');
+
+ // Register backend path setter
+ $javascript->registerFile ('backendpath.js');
+
+ // Register page URL getter method
+ $javascript->registerFile ('geturl.js');
+
+ // Register page title getter method
+ $javascript->registerFile ('gettitle.js');
+
+ // Register real constructor method
+ $javascript->registerFile ('instantiator.js', array (
+ 'dependencies' => array (
+ 'getbackendqueries.js'
+ )
+ ));
+
+ // Register HashOver ready state detection method
+ $javascript->registerFile ('onready.js');
+
+ // Register element creation methods
+ $javascript->registerFile ('elements.js');
+
+ // Register main HashOver element getter method
+ $javascript->registerFile ('getmainelement.js');
+
+ // Register error message handler method
+ $javascript->registerFile ('displayerror.js');
+
+ // Register AJAX-related methods
+ $javascript->registerFile ('ajax.js');
+
+ // Register pre-compiled regular expressions
+ $javascript->registerFile ('regex.js');
+
+ // Register end-of-line trimmer method
+ $javascript->registerFile ('eoltrim.js');
+
+ // Register parent permalink getter method
+ $javascript->registerFile ('permalinks.js');
+
+ // Register markdown methods
+ $javascript->registerFile ('markdown.js', array (
+ 'include' => $setup->usesMarkdown
+ ));
+
+ // Register date/time methods
+ $javascript->registerFile ('datetime.js', array (
+ 'include' => $setup->usesUserTimezone
+ ));
+
+ // Register search and replace methods
+ $javascript->registerFile ('strings.js');
+
+ // Register optional method handler method
+ $javascript->registerFile ('optionalmethod.js');
+
+ // Register comment parsing methods
+ $javascript->registerFile ('comments.js');
+
+ // Register embedded image method
+ $javascript->registerFile ('embedimage.js', array (
+ 'include' => $setup->allowsImages,
+
+ 'dependencies' => array (
+ 'openembeddedimage.js'
+ )
+ ));
+
+ // Register Like/Dislike methods
+ $javascript->registerFile ('addratings.js', array (
+ 'include' => ($setup->allowsLikes or $setup->allowsDislikes)
+ ));
+
+ // Register cancel button toggler method
+ $javascript->registerFile ('cancelswitcher.js');
+
+ // Register miscellaneous form event handler methods
+ $javascript->registerFile ('formevents.js');
+
+ // Register classList polyfill methods
+ $javascript->registerFile ('classes.js');
+
+ // Register message element methods
+ $javascript->registerFile ('messages.js');
+
+ // Register email validator method
+ $javascript->registerFile ('emailvalidator.js');
+
+ // Register email validator event handler method
+ $javascript->registerFile ('validateemail.js');
+
+ // Register comment validator method
+ $javascript->registerFile ('commentvalidator.js');
+
+ // Register comment validator event handler method
+ $javascript->registerFile ('validatecomment.js');
+
+ // Register AJAX comment post request method
+ $javascript->registerFile ('postrequest.js', array (
+ 'include' => $setup->usesAjax
+ ));
+
+ // Register comment post method
+ $javascript->registerFile ('postcomment.js');
+
+ // Register control event handler attacher method
+ $javascript->registerFile ('addcontrols.js');
+
+ // Register AJAX post comment event handler method
+ $javascript->registerFile ('ajaxpost.js', array (
+ 'include' => $setup->usesAjax,
+
+ 'dependencies' => array (
+ 'addcomments.js',
+ 'htmltonodelist.js',
+ 'incrementcounts.js'
+ )
+ ));
+
+ // Register AJAX edit comment event handler method
+ $javascript->registerFile ('ajaxedit.js', array (
+ 'include' => $setup->usesAjax,
+
+ 'dependencies' => array (
+ 'htmltonodelist.js'
+ )
+ ));
+
+ // Register formatting message onclick event handler method
+ $javascript->registerFile ('formattingonclick.js');
+
+ // Register reply form adder method
+ $javascript->registerFile ('replytocomment.js');
+
+ // Register edit form adder method
+ $javascript->registerFile ('editcomment.js');
+
+ // Register append comments method
+ $javascript->registerFile ('appendcomments.js', array (
+ 'include' => $setup->collapsesComments and $setup->usesAjax
+ ));
+
+ // Register show more comments method
+ $javascript->registerFile ('showmorecomments.js', array (
+ 'include' => $setup->collapsesComments,
+
+ 'dependencies' => array (
+ 'hidemorelink.js'
+ )
+ ));
+
+ // Register like/dislike comment method
+ $javascript->registerFile ('likecomment.js', array (
+ 'include' => ($setup->allowsLikes or $setup->allowsDislikes),
+
+ 'dependencies' => array (
+ 'mouseoverchanger.js'
+ )
+ ));
+
+ // Register clone object method
+ $javascript->registerFile ('cloneobject.js');
+
+ // Register get all comments method
+ $javascript->registerFile ('getallcomments.js');
+
+ // Register parse all comments method
+ $javascript->registerFile ('parseall.js');
+
+ // Register sort comments method
+ $javascript->registerFile ('sortcomments.js');
+
+ // Register theme stylesheet appender method
+ $javascript->registerFile ('appendcss.js', array (
+ 'include' => $setup->appendsCss
+ ));
+
+ // Register comments RSS feed appender method
+ $javascript->registerFile ('appendrss.js', array (
+ 'include' => ($setup->appendsRss and $setup->apiStatus ('rss') !== 'disabled')
+ ));
+
+ // Register uncollapse interface method
+ $javascript->registerFile ('uncollapseinterfacelink.js', array (
+ 'include' => $setup->collapsesInterface,
+
+ 'dependencies' => array (
+ 'uncollapseinterface.js'
+ )
+ ));
+
+ // Register uncollapse comments method
+ $javascript->registerFile ('uncollapsecommentslink.js', array (
+ 'include' => $setup->collapsesComments
+ ));
+
+ // Register initialization method
+ $javascript->registerFile ('init.js');
+
+ // Register automatic instantiation code
+ $javascript->registerFile ('instantiate.js', array (
+ 'include' => !isset ($_GET['nodefault'])
+ ));
+
+ // JavaScript build process output
+ $output = $javascript->build (
+ $setup->minifiesJavascript,
+ $setup->minifyLevel
+ );
+
+ // Display JavaScript build process output
+ echo $output, PHP_EOL;
+
+ // Display statistics
+ echo $statistics->executionEnd ();
+
+} catch (\Exception $error) {
+ $misc = new Misc ('javascript');
+ $message = $error->getMessage ();
+ $misc->displayError ($message);
+}
diff --git a/bootstrap/comments/frontend/addcomments.js b/bootstrap/comments/frontend/addcomments.js
new file mode 100644
index 0000000..292d41f
--- /dev/null
+++ b/bootstrap/comments/frontend/addcomments.js
@@ -0,0 +1,43 @@
+// For adding new comments to comments array (addcomments.js)
+HashOver.prototype.addComments = function (comment, isReply, index)
+{
+ isReply = isReply || false;
+ index = index || null;
+
+ // Check that comment is not a reply
+ if (isReply !== true) {
+ // If so, add to primary comments
+ if (index !== null) {
+ this.instance.comments.primary.splice (index, 0, comment);
+ return;
+ }
+
+ this.instance.comments.primary.push (comment);
+ return;
+ }
+
+ // If not, fetch parent comment
+ var parentPermalink = this.permalinks.getParent (comment.permalink);
+ var parent = this.permalinks.getComment (parentPermalink, this.instance.comments.primary);
+
+ // Check if the parent comment exists
+ if (parent !== null) {
+ // If so, check if comment has replies
+ if (parent.replies !== undefined) {
+ // If so, add comment to reply array
+ if (index !== null) {
+ parent.replies.splice (index, 0, comment);
+ return;
+ }
+
+ parent.replies.push (comment);
+ return;
+ }
+
+ // If not, create reply array
+ parent.replies = [ comment ];
+ }
+
+ // Otherwise, add to primary comments
+ this.instance.comments.primary.push (comment);
+};
diff --git a/bootstrap/comments/frontend/addcontrols.js b/bootstrap/comments/frontend/addcontrols.js
new file mode 100644
index 0000000..ba55b7e
--- /dev/null
+++ b/bootstrap/comments/frontend/addcontrols.js
@@ -0,0 +1,113 @@
+// Add various events to various elements in each comment (addcontrols.js)
+HashOverConstructor.prototype.addControls = function (json, popular)
+{
+ // Reference to this object
+ var hashover = this;
+
+ function stepIntoReplies ()
+ {
+ if (json.replies !== undefined) {
+ for (var reply = 0, total = json.replies.length; reply < total; reply++) {
+ hashover.addControls (json.replies[reply]);
+ }
+ }
+ }
+
+ if (json.notice !== undefined) {
+ stepIntoReplies ();
+ return false;
+ }
+
+ // Get permalink from JSON object
+ var permalink = json.permalink;
+
+ // Set onclick functions for external images
+ if (this.setup['allows-images'] !== false) {
+ // Get embedded image elements
+ var embeddedImgs = document.getElementsByClassName ('hashover-embedded-image');
+
+ for (var i = 0, il = embeddedImgs.length; i < il; i++) {
+ embeddedImgs[i].onclick = function ()
+ {
+ hashover.openEmbeddedImage (this);
+ };
+ }
+ }
+
+ // Check if collapsed comments are enabled
+ if (this.setup['collapses-comments'] !== false) {
+ // Get thread link of comment
+ this.elements.exists ('thread-link-' + permalink, function (threadLink) {
+ // Add onClick event to thread hyperlink
+ threadLink.onclick = function ()
+ {
+ hashover.showMoreComments (threadLink, function () {
+ var parentThread = permalink.replace (hashover.regex.thread, '$1');
+ var scrollToElement = hashover.elements.get (parentThread, true);
+
+ // Scroll to the comment
+ scrollToElement.scrollIntoView ({ behavior: 'smooth' });
+ });
+
+ return false;
+ };
+ });
+ }
+
+ // Get reply link of comment
+ this.elements.exists ('reply-link-' + permalink, function (replyLink) {
+ // Add onClick event to "Reply" hyperlink
+ replyLink.onclick = function ()
+ {
+ hashover.replyToComment (permalink);
+ return false;
+ };
+ });
+
+ // Check if the comment is editable for the user
+ this.elements.exists ('edit-link-' + permalink, function (editLink) {
+ // If so, add onClick event to "Edit" hyperlinks
+ editLink.onclick = function ()
+ {
+ hashover.editComment (json);
+ return false;
+ };
+ });
+
+ // Check if the comment doesn't belong to the logged in user
+ if (json['user-owned'] === undefined) {
+ // If so, check if likes are enabled
+ if (this.setup['allows-likes'] !== false) {
+ // If so, check if the like link exists
+ this.elements.exists ('like-' + permalink, function (likeLink) {
+ // Add onClick event to "Like" hyperlinks
+ likeLink.onclick = function ()
+ {
+ hashover.likeComment ('like', permalink);
+ return false;
+ };
+
+ // And add "Unlike" mouseover event to liked comments
+ if (hashover.classes.contains (likeLink, 'hashover-liked') === true) {
+ hashover.mouseOverChanger (likeLink, 'unlike', 'liked');
+ }
+ });
+ }
+
+ // Check if dislikes are enabled
+ if (this.setup['allows-dislikes'] !== false) {
+ // If so, check if the dislike link exists
+ this.elements.exists ('dislike-' + permalink, function (dislikeLink) {
+ // Add onClick event to "Dislike" hyperlinks
+ dislikeLink.onclick = function ()
+ {
+ hashover.likeComment ('dislike', permalink);
+ return false;
+ };
+ });
+ }
+ }
+
+ // Recursively execute this function on replies
+ stepIntoReplies ();
+};
diff --git a/bootstrap/comments/frontend/addratings.js b/bootstrap/comments/frontend/addratings.js
new file mode 100644
index 0000000..c69bd0a
--- /dev/null
+++ b/bootstrap/comments/frontend/addratings.js
@@ -0,0 +1,54 @@
+// Add Like/Dislike link and count to template (addratings.js)
+HashOver.prototype.comments.addRatings = function (comment, template, action, commentKey)
+{
+ // Reference to the parent object
+ var hashover = this.parent;
+
+ // The opposite action
+ var opposite = (action === 'like') ? 'dislike' : 'like';
+
+ // Check if the comment doesn't belong to the logged in user
+ if (comment['user-owned'] === undefined) {
+ // Check whether this comment was liked/disliked by the visitor
+ if (comment[action + 'd'] !== undefined) {
+ // If so, setup indicators that comment was liked/disliked
+ var className = 'hashover-' + action + 'd';
+ var title = hashover.locale[action + 'd-comment'];
+ var text = hashover.locale[action + 'd'];
+ } else {
+ // If not, setup indicators that comment can be liked/disliked
+ var className = 'hashover-' + action;
+ var title = hashover.locale[action + '-comment'];
+ var text = hashover.locale[action][0];
+ }
+
+ // Append class to indicate dislikes are enabled
+ if (hashover.setup['allows-' + opposite + 's'] === true) {
+ className += ' hashover-' + opposite + 's-enabled';
+ }
+
+ // Add like/dislike link to HTML template
+ template[action + '-link'] = hashover.strings.parseTemplate (hashover.ui[action + '-link'], {
+ permalink: commentKey,
+ class: className,
+ title: title,
+ text: text
+ });
+ }
+
+ // Check if the comment has been likes/dislikes
+ if (comment[action + 's'] !== undefined) {
+ // Add likes/dislikes to HTML template
+ template[action + 's'] = comment[action + 's'];
+
+ // Get "X Like/Dislike(s)" locale
+ var plural = (comment[action + 's'] === 1 ? 0 : 1);
+ var count = comment[action + 's'] + ' ' + hashover.locale[action][plural];
+ }
+
+ // Add like count to HTML template
+ template[action + '-count'] = hashover.strings.parseTemplate (hashover.ui[action + '-count'], {
+ permalink: commentKey,
+ text: count || ''
+ });
+};
diff --git a/bootstrap/comments/frontend/ajax.js b/bootstrap/comments/frontend/ajax.js
new file mode 100644
index 0000000..fdbdd06
--- /dev/null
+++ b/bootstrap/comments/frontend/ajax.js
@@ -0,0 +1,71 @@
+// Array of JSONP callbacks, starting with default error handler (ajax.js)
+HashOverConstructor.jsonp = [
+ function (json) { alert (json.message); }
+];
+
+// Send HTTP requests using either XMLHttpRequest or JSONP (ajax.js)
+HashOverConstructor.prototype.ajax = function (method, path, data, callback, async)
+{
+ // Check if the browser supports location origin
+ if (window.location.origin) {
+ // If so, use it as-is
+ var origin = window.location.origin;
+ } else {
+ // If not, construct origin manually
+ var protocol = window.location.protocol;
+ var hostname = window.location.hostname;
+ var port = window.location.port;
+
+ // Final origin
+ var origin = protocol + '//' + hostname + (port ? ':' + port : '');
+ }
+
+ // Create origin regular expression
+ var originRegex = new RegExp ('^' + origin + '/', 'i');
+
+ // Check if script is being remotely accessed
+ if (originRegex.test (this.constructor.script.src) === false) {
+ // If so, get constructor name
+ var source = this.constructor.toString ();
+ var constructor = source.match (/function (\w+)/)[1];
+
+ // Push callback into JSONP array
+ this.constructor.jsonp.push (callback);
+
+ // Add JSONP callback index and constructor to request data
+ data.push ('jsonp=' + (this.constructor.jsonp.length - 1));
+ data.push ('jsonp_object=' + constructor || 'HashOver');
+
+ // Create request script
+ var request = this.elements.create ('script', {
+ src: path + '?' + data.join ('&'),
+ async: async
+ });
+
+ // And append request script to page
+ document.body.appendChild (request);
+ } else {
+ // If not, create AJAX request
+ var request = new XMLHttpRequest ();
+
+ // Set callback as ready state change handler
+ request.onreadystatechange = function ()
+ {
+ // Do nothing if request wasn't successful in a meaningful way
+ if (this.readyState !== 4 || this.status !== 200) {
+ return;
+ }
+
+ // Parse response as JSON
+ var json = JSON.parse (this.responseText);
+
+ // Execute callback
+ callback.apply (this, [ json ]);
+ };
+
+ // And send request
+ request.open (method, path, async);
+ request.setRequestHeader ('Content-type', 'application/x-www-form-urlencoded');
+ request.send (data.join ('&'));
+ }
+};
diff --git a/bootstrap/comments/frontend/ajaxedit.js b/bootstrap/comments/frontend/ajaxedit.js
new file mode 100644
index 0000000..29c5656
--- /dev/null
+++ b/bootstrap/comments/frontend/ajaxedit.js
@@ -0,0 +1,29 @@
+// For editing comments (ajaxedit.js)
+HashOver.prototype.AJAXEdit = function (json, permalink, destination, isReply)
+{
+ // Get old comment element nodes
+ var comment = this.elements.get (permalink, true);
+ var oldNodes = comment.childNodes;
+ var oldComment = this.permalinks.getComment (permalink, this.instance.comments.primary);
+
+ // Get new comment element nodes
+ var newNodes = this.HTMLToNodeList (this.comments.parse (json.comment));
+ newNodes = newNodes[0].childNodes;
+
+ // Replace old comment with edited comment
+ for (var i = 0, il = newNodes.length; i < il; i++) {
+ if (typeof (oldNodes[i]) === 'object'
+ && typeof (newNodes[i]) === 'object')
+ {
+ comment.replaceChild (newNodes[i], oldNodes[i]);
+ }
+ }
+
+ // Add controls back to the comment
+ this.addControls (json.comment);
+
+ // Update old in array comment with edited comment
+ for (var attribute in json.comment) {
+ oldComment[attribute] = json.comment[attribute];
+ }
+};
diff --git a/bootstrap/comments/frontend/ajaxpost.js b/bootstrap/comments/frontend/ajaxpost.js
new file mode 100644
index 0000000..04ebeed
--- /dev/null
+++ b/bootstrap/comments/frontend/ajaxpost.js
@@ -0,0 +1,29 @@
+// For posting comments (ajaxpost.js)
+HashOver.prototype.AJAXPost = function (json, permalink, destination, isReply)
+{
+ // If there aren't any comments, replace first comment message
+ if (this.instance['total-count'] === 0) {
+ this.instance.comments.primary[0] = json.comment;
+ destination.innerHTML = this.comments.parse (json.comment);
+ } else {
+ // Add comment to comments array
+ this.addComments (json.comment, isReply);
+
+ // Create div element for comment
+ var commentNode = this.HTMLToNodeList (this.comments.parse (json.comment));
+
+ // Append comment to parent element
+ if (this.setup['stream-mode'] === true && permalink.split('r').length > this.setup['stream-depth']) {
+ destination.parentNode.insertBefore (commentNode[0], destination.nextSibling);
+ } else {
+ destination.appendChild (commentNode[0]);
+ }
+ }
+
+ // Add controls to the new comment
+ this.addControls (json.comment);
+
+ // Update comment count
+ this.elements.get ('count').textContent = json.count;
+ this.incrementCounts (isReply);
+};
diff --git a/bootstrap/comments/frontend/appendcomments.js b/bootstrap/comments/frontend/appendcomments.js
new file mode 100644
index 0000000..4bb54b0
--- /dev/null
+++ b/bootstrap/comments/frontend/appendcomments.js
@@ -0,0 +1,50 @@
+// For appending new comments to the thread on page (appendcomments.js)
+HashOver.prototype.appendComments = function (comments)
+{
+ // Run through each comment
+ for (var i = 0, il = comments.length; i < il; i++) {
+ // Skip existing comments
+ if (this.permalinks.getComment (comments[i].permalink, this.instance.comments.primary) !== null) {
+ // Check comment's replies
+ if (comments[i].replies !== undefined) {
+ this.appendComments (comments[i].replies);
+ }
+
+ continue;
+ }
+
+ // Check if comment is a reply
+ var isReply = (comments[i].permalink.indexOf ('r') > -1);
+
+ // Add comment to comments array
+ this.addComments (comments[i], isReply, i);
+
+ // Check that comment is not a reply
+ if (isReply !== true) {
+ // If so, append to primary comments
+ var element = this.instance['more-section'];
+ } else {
+ // If not, append to its parent's element
+ var parent = this.permalinks.getParent (comments[i].permalink, true);
+ var element = this.elements.get (parent, true) || this.instance['more-section'];
+ }
+
+ // Parse comment
+ var html = this.comments.parse (comments[i], null, true);
+
+ // Check if we can insert HTML adjacently
+ if ('insertAdjacentHTML' in element) {
+ // If so, just do so
+ element.insertAdjacentHTML ('beforeend', html);
+ } else {
+ // If not, convert HTML to NodeList
+ var comment = this.HTMLToNodeList (html);
+
+ // And append the first node
+ element.appendChild (comment[0]);
+ }
+
+ // Add controls to the comment
+ this.addControls (comments[i]);
+ }
+};
diff --git a/bootstrap/comments/frontend/appendcss.js b/bootstrap/comments/frontend/appendcss.js
new file mode 100644
index 0000000..9c16e43
--- /dev/null
+++ b/bootstrap/comments/frontend/appendcss.js
@@ -0,0 +1,66 @@
+// Appends HashOver theme CSS to page head (appendcss.js)
+HashOverConstructor.prototype.appendCSS = function (id)
+{
+ id = id || 'hashover';
+
+ // Get the page head
+ var head = document.head || document.getElementsByTagName ('head')[0];
+
+ // Get head link tags
+ var links = head.getElementsByTagName ('link');
+
+ // Theme CSS regular expression
+ var themeRegex = new RegExp (this.setup['theme-css']);
+
+ // Get the main HashOver element
+ var mainElement = this.getMainElement (id);
+
+ // Do nothing if the theme StyleSheet is already in the <head>
+ for (var i = 0, il = links.length; i < il; i++) {
+ if (themeRegex.test (links[i].href) === true) {
+ // Hide HashOver if the theme isn't loaded
+ if (links[i].loaded === false) {
+ mainElement.style.display = 'none';
+ }
+
+ // And do nothing else
+ return;
+ }
+ }
+
+ // Otherwise, create <link> element for theme StyleSheet
+ var css = this.elements.create ('link', {
+ rel: 'stylesheet',
+ href: this.setup['theme-css'],
+ type: 'text/css',
+ loaded: false
+ });
+
+ // Check if the browser supports CSS load events
+ if (css.onload !== undefined) {
+ // CSS load and error event handler
+ var onLoadError = function ()
+ {
+ // Get all HashOver class elements
+ var hashovers = document.getElementsByClassName ('hashover');
+
+ // Show all HashOver class elements
+ for (var i = 0, il = hashovers.length; i < il; i++) {
+ hashovers[i].style.display = '';
+ }
+
+ // Set CSS as loaded
+ css.loaded = true;
+ };
+
+ // Hide HashOver
+ mainElement.style.display = 'none';
+
+ // And and CSS load and error event listeners
+ css.addEventListener ('load', onLoadError, false);
+ css.addEventListener ('error', onLoadError, false);
+ }
+
+ // Append theme StyleSheet <link> element to page <head>
+ head.appendChild (css);
+};
diff --git a/bootstrap/comments/frontend/appendrss.js b/bootstrap/comments/frontend/appendrss.js
new file mode 100644
index 0000000..2913e94
--- /dev/null
+++ b/bootstrap/comments/frontend/appendrss.js
@@ -0,0 +1,17 @@
+// Appends HashOver comments RSS feed to page head (appendrss.js)
+HashOver.prototype.appendRSS = function ()
+{
+ // Get the page head
+ var head = document.head || document.getElementsByTagName ('head')[0];
+
+ // Create link element for comment RSS feed
+ var rss = this.elements.create ('link', {
+ rel: 'alternate',
+ href: this.setup['http-root'] + '/api/rss.php?url=' + encodeURIComponent (this.instance['page-url']),
+ type: 'application/rss+xml',
+ title: 'Comments'
+ });
+
+ // Append comment RSS feed link element to page head
+ head.appendChild (rss);
+};
diff --git a/bootstrap/comments/frontend/backendpath.js b/bootstrap/comments/frontend/backendpath.js
new file mode 100644
index 0000000..0845116
--- /dev/null
+++ b/bootstrap/comments/frontend/backendpath.js
@@ -0,0 +1,10 @@
+// Backend path (backendpath.js)
+HashOverConstructor.backendPath = (function () {
+ // Get the HashOver script source URL
+ var scriptSrc = HashOverConstructor.script.getAttribute ('src');
+
+ // Parse and set HashOver path
+ var root = scriptSrc.replace (/\/[^\/]*\/?$/, '');
+
+ return root + '/backend';
+}) ();
diff --git a/bootstrap/comments/frontend/cancelswitcher.js b/bootstrap/comments/frontend/cancelswitcher.js
new file mode 100644
index 0000000..84f4e02
--- /dev/null
+++ b/bootstrap/comments/frontend/cancelswitcher.js
@@ -0,0 +1,40 @@
+// Changes a given hyperlink into a "Cancel" hyperlink (cancelswitcher.js)
+HashOver.prototype.cancelSwitcher = function (form, link, wrapper, permalink)
+{
+ // Initial state properties of hyperlink
+ var reset = {
+ textContent: link.textContent,
+ title: link.title,
+ onclick: link.onclick
+ };
+
+ function linkOnClick ()
+ {
+ // Remove fields from form wrapper
+ wrapper.textContent = '';
+
+ // Reset button
+ link.textContent = reset.textContent;
+ link.title = reset.title;
+ link.onclick = reset.onclick;
+
+ return false;
+ }
+
+ // Change hyperlink to "Cancel" hyperlink
+ link.textContent = this.locale['cancel'];
+ link.title = this.locale['cancel'];
+
+ // This resets the "Cancel" hyperlink to initial state onClick
+ link.onclick = linkOnClick;
+
+ // Check if cancel buttons are enabled
+ if (this.setup['uses-cancel-buttons'] !== false) {
+ // If so, get "Cancel" button
+ var cancelButtonId = form + '-cancel-' + permalink;
+ var cancelButton = this.elements.get (cancelButtonId, true);
+
+ // Attach event listeners to "Cancel" button
+ cancelButton.onclick = linkOnClick;
+ }
+};
diff --git a/bootstrap/comments/frontend/classes.js b/bootstrap/comments/frontend/classes.js
new file mode 100644
index 0000000..831c924
--- /dev/null
+++ b/bootstrap/comments/frontend/classes.js
@@ -0,0 +1,59 @@
+// Collection of element class related functions (classes.js)
+HashOverConstructor.prototype.classes = new (function () {
+ // Check whether browser has classList support
+ if (document.documentElement.classList) {
+ // If so, wrap relevant functions
+ // classList.contains () method
+ this.contains = function (element, className)
+ {
+ return element.classList.contains (className);
+ };
+
+ // classList.add () method
+ this.add = function (element, className)
+ {
+ element.classList.add (className);
+ };
+
+ // classList.remove () method
+ this.remove = function (element, className)
+ {
+ element.classList.remove (className);
+ };
+ } else {
+ // If not, define fallback functions
+ // classList.contains () method
+ this.contains = function (element, className)
+ {
+ if (!element || !element.className) {
+ return false;
+ }
+
+ var regex = new RegExp ('(^|\\s)' + className + '(\\s|$)');
+ return regex.test (element.className);
+ };
+
+ // classList.add () method
+ this.add = function (element, className)
+ {
+ if (!element) {
+ return false;
+ }
+
+ if (!this.contains (element, className)) {
+ element.className += (element.className ? ' ' : '') + className;
+ }
+ };
+
+ // classList.remove () method
+ this.remove = function (element, className)
+ {
+ if (!element || !element.className) {
+ return false;
+ }
+
+ var regex = new RegExp ('(^|\\s)' + className + '(\\s|$)', 'g');
+ element.className = element.className.replace (regex, '$2');
+ };
+ }
+}) ();
diff --git a/bootstrap/comments/frontend/cloneobject.js b/bootstrap/comments/frontend/cloneobject.js
new file mode 100644
index 0000000..9b53d44
--- /dev/null
+++ b/bootstrap/comments/frontend/cloneobject.js
@@ -0,0 +1,5 @@
+// Returns a clone of an object (cloneobject.js)
+HashOver.prototype.cloneObject = function (object)
+{
+ return JSON.parse (JSON.stringify (object));
+};
diff --git a/bootstrap/comments/frontend/comments.js b/bootstrap/comments/frontend/comments.js
new file mode 100644
index 0000000..d8bace6
--- /dev/null
+++ b/bootstrap/comments/frontend/comments.js
@@ -0,0 +1,400 @@
+// Collection of comment parsing functions (comments.js)
+HashOverConstructor.prototype.comments = {
+ collapsedCount: 0,
+ codeOpenRegex: /<code>/i,
+ codeTagRegex: /(<code>)([\s\S]*?)(<\/code>)/ig,
+ preOpenRegex: /<pre>/i,
+ preTagRegex: /(<pre>)([\s\S]*?)(<\/pre>)/ig,
+ lineRegex: /(?:\r\n|\r|\n)/g,
+ codeTagMarkerRegex: /CODE_TAG\[([0-9]+)\]/g,
+ preTagMarkerRegex: /PRE_TAG\[([0-9]+)\]/g,
+
+ // Tags that will have their innerHTML trimmed
+ trimTagRegexes: {
+ blockquote: {
+ test: /<blockquote>/,
+ replace: /(<blockquote>)([\s\S]*?)(<\/blockquote>)/ig
+ },
+
+ ul: {
+ test: /<ul>/,
+ replace: /(<ul>)([\s\S]*?)(<\/ul>)/ig
+ },
+
+ ol: {
+ test: /<ol>/,
+ replace: /(<ol>)([\s\S]*?)(<\/ol>)/ig
+ }
+ },
+
+ // Add comment content to HTML template
+ parse: function (comment, parent, collapse, sort, method, popular)
+ {
+ parent = parent || null;
+ collapse = collapse || false;
+ sort = sort || false;
+ method = method || 'ascending';
+ popular = popular || false;
+
+ // Reference to the parent object
+ var hashover = this.parent;
+
+ var commentKey = comment.permalink;
+ var permalink = 'hashover-' + commentKey;
+ var nameClass = 'hashover-name-plain';
+ var template = { permalink: commentKey };
+ var isReply = (parent !== null);
+ var commentDate = comment.date;
+ var codeTagCount = 0;
+ var codeTags = [];
+ var preTagCount = 0;
+ var preTags = [];
+ var classes = '';
+ var replies = '';
+
+ // Text for avatar image alt attribute
+ var permatext = commentKey.slice (1);
+ permatext = permatext.split ('r');
+ permatext = permatext.pop ();
+
+ // Trims whitespace from an HTML tag's inner HTML
+ function tagTrimmer (fullTag, openTag, innerHTML, closeTag)
+ {
+ return openTag + hashover.EOLTrim (innerHTML) + closeTag;
+ }
+
+ // Get parent comment via permalink
+ if (isReply === false && commentKey.indexOf ('r') > -1) {
+ // Get the parent comment permalink
+ var parentPermalink = hashover.permalinks.getParent (commentKey);
+
+ // Get the parent comment by its permalink
+ parent = hashover.permalinks.getComment (parentPermalink, hashover.instance.comments.primary);
+ isReply = (parent !== null);
+ }
+
+ // Check if this comment is a popular comment
+ if (popular === true) {
+ // Remove "-pop" from text for avatar
+ permatext = permatext.replace ('-pop', '');
+ } else {
+ // Check if comment is a reply
+ if (isReply === true) {
+ // Check that comments are being sorted
+ if (!sort || method === 'ascending') {
+ // Append class to indicate comment is a reply
+ classes += ' hashover-reply';
+ // Append class to indicate odd or even reply for CSS styling
+ if ((commentKey.split("r").length & 1) == 0) {
+ classes += ' odd';
+ } else {
+ classes += ' even';
+ }
+ }
+ }
+
+ // Check if comments are being collapsed
+ if (hashover.setup['collapses-comments'] !== false) {
+ // If so, append class to indicate collapsed comment
+ if (hashover.instance['total-count'] > 0) {
+ if (collapse === true && this.collapsedCount >= hashover.setup['collapse-limit']) {
+ classes += ' hashover-hidden';
+ } else {
+ this.collapsedCount++;
+ }
+ }
+ }
+ }
+
+ // Add avatar image to template
+ template.avatar = hashover.strings.parseTemplate (hashover.ui['user-avatar'], {
+ src: comment.avatar,
+ href: permalink,
+ text: permatext
+ });
+
+ if (comment.notice === undefined) {
+ var name = comment.name || hashover.setup['default-name'];
+ var website = comment.website;
+ var isTwitter = false;
+
+ // Check if user's name is a Twitter handle
+ if (name.charAt (0) === '@') {
+ name = name.slice (1);
+ nameClass = 'hashover-name-twitter';
+ isTwitter = true;
+ var nameLength = name.length;
+
+ // Check if Twitter handle is valid length
+ if (nameLength > 1 && nameLength <= 30) {
+ // Set website to Twitter profile if a specific website wasn't given
+ if (website === undefined) {
+ website = 'http://twitter.com/' + name;
+ }
+ }
+ }
+
+ // Check whether user gave a website
+ if (website !== undefined) {
+ if (isTwitter === false) {
+ nameClass = 'hashover-name-website';
+ }
+
+ // If so, display name as a hyperlink
+ var nameElement = hashover.strings.parseTemplate (hashover.ui['name-link'], {
+ href: website,
+ permalink: commentKey,
+ name: name
+ });
+ } else {
+ // If not, display name as plain text
+ var nameElement = hashover.strings.parseTemplate (hashover.ui['name-span'], {
+ permalink: commentKey,
+ name: name
+ });
+ }
+
+ // Construct thread link
+ if ((comment.url && comment.title) !== undefined) {
+ template['thread-link'] = hashover.strings.parseTemplate (hashover.ui['thread-link'], {
+ url: comment.url,
+ title: comment.title
+ });
+ }
+
+ // Construct parent thread hyperlink
+ if (isReply === true) {
+ var parentThread = 'hashover-' + parent.permalink;
+ var parentName = parent.name || hashover.setup['default-name'];
+
+ // Add thread parent hyperlink to template
+ template['parent-link'] = hashover.strings.parseTemplate (hashover.ui['parent-link'], {
+ parent: parentThread,
+ permalink: commentKey,
+ name: parentName
+ });
+ }
+
+ // Check if the logged in user owns the comment
+ if (comment['user-owned'] !== undefined) {
+ // If so, append class to indicate comment is from logged in user
+ classes += ' hashover-user-owned';
+
+ // Define "Reply" link with original poster title
+ var replyTitle = hashover.locale['commenter-tip'];
+ var replyClass = 'hashover-no-email';
+ } else {
+ // Check if commenter is subscribed
+ if (comment.subscribed === true) {
+ // If so, set subscribed title
+ var replyTitle = name + ' ' + hashover.locale['subscribed-tip'];
+ var replyClass = 'hashover-has-email';
+ } else{
+ // If not, set unsubscribed title
+ var replyTitle = name + ' ' + hashover.locale['unsubscribed-tip'];
+ var replyClass = 'hashover-no-email';
+ }
+ }
+
+ // Check if the comment is editable for the user
+ if (comment['editable'] !== undefined) {
+ // If so, add "Edit" hyperlink to template
+ template['edit-link'] = hashover.strings.parseTemplate (hashover.ui['edit-link'], {
+ href: comment.url || hashover.instance['file-path'],
+ permalink: commentKey
+ });
+ }
+
+ // Add like link and count to template if likes are enabled
+ if (hashover.setup['allows-likes'] !== false) {
+ hashover.optionalMethod ('addRatings', [
+ comment, template, 'like', commentKey
+ ], 'comments');
+ }
+
+ // Add dislike link and count to template if dislikes are enabled
+ if (hashover.setup['allows-dislikes'] !== false) {
+ hashover.optionalMethod ('addRatings', [
+ comment, template, 'dislike', commentKey
+ ], 'comments');
+ }
+
+ // Add name HTML to template
+ template.name = hashover.strings.parseTemplate (hashover.ui['name-wrapper'], {
+ class: nameClass,
+ link: nameElement
+ });
+
+ // Check if user timezones is enabled
+ if (hashover.setup['uses-user-timezone'] !== false) {
+ // If so, get local comment post date
+ var postDate = new Date (comment['sort-date'] * 1000);
+
+ // Check if short date format is enabled
+ if (hashover.setup['uses-short-dates'] !== false) {
+ // If so, get local date
+ var localDate = new Date ();
+
+ // Local comment post date to remove time from
+ var postDateCopy = new Date (postDate.getTime ());
+
+ // And format local time if the comment was posted today
+ if (postDateCopy.setHours (0, 0, 0, 0) === localDate.setHours (0, 0, 0, 0)) {
+ commentDate = hashover.strings.sprintf (hashover.locale['today'], [
+ hashover.dateTime.format (hashover.setup['time-format'], postDate)
+ ]);
+ }
+ } else {
+ // If not, format a long local date/time
+ commentDate = hashover.dateTime.format (hashover.locale['date-time'], postDate);
+ }
+ }
+
+ // Append status text to date
+ if (comment['status-text'] !== undefined) {
+ commentDate += ' (' + comment['status-text'] + ')';
+ }
+
+ // Add date from comment as permalink hyperlink to template
+ template.date = hashover.strings.parseTemplate (hashover.ui['date-link'], {
+ href: comment.url || hashover.instance['file-path'],
+ permalink: permalink,
+ date: commentDate
+ });
+
+ // Add "Reply" hyperlink to template
+ template['reply-link'] = hashover.strings.parseTemplate (hashover.ui['reply-link'], {
+ href: comment.url || hashover.instance['file-path'],
+ permalink: commentKey,
+ class: replyClass,
+ title: replyTitle
+ });
+
+ // Add reply count to template
+ if (comment.replies !== undefined) {
+ template['reply-count'] = comment.replies.length;
+
+ if (template['reply-count'] > 0) {
+ if (template['reply-count'] !== 1) {
+ template['reply-count'] += ' ' + hashover.locale['replies'];
+ } else {
+ template['reply-count'] += ' ' + hashover.locale['reply'];
+ }
+ }
+ }
+
+ // Add HTML anchor tag to URLs
+ var body = comment.body.replace (hashover.regex.links, '<a href="$1" rel="noopener noreferrer" target="_blank">$1</a>');
+
+ // Replace [img] tags with external image placeholder if enabled
+ body = body.replace (hashover.regex.imageTags, function (fullURL, url) {
+ // Check if embedded images are enabled
+ if (hashover.setup['allows-images'] !== false) {
+ return hashover.optionalMethod ('embedImage', [ url ], 'comments');
+ }
+
+ // Convert image URL into an anchor tag
+ return '<a href="' + url + '" rel="noopener noreferrer" target="_blank">' + url + '</a>';
+ });
+
+ // Parse markdown in comment if enabled
+ if (hashover.markdown !== undefined) {
+ body = hashover.markdown.parse (body);
+ }
+
+ // Check for code tags
+ if (this.codeOpenRegex.test (body) === true) {
+ // Replace code tags with marker text
+ body = body.replace (this.codeTagRegex, function (fullTag, openTag, innerHTML, closeTag) {
+ var codeMarker = openTag + 'CODE_TAG[' + codeTagCount + ']' + closeTag;
+
+ codeTags[codeTagCount] = hashover.EOLTrim (innerHTML);
+ codeTagCount++;
+
+ return codeMarker;
+ });
+ }
+
+ // Check for pre tags
+ if (this.preOpenRegex.test (body) === true) {
+ // Replace pre tags with marker text
+ body = body.replace (this.preTagRegex, function (fullTag, openTag, innerHTML, closeTag) {
+ var preMarker = openTag + 'PRE_TAG[' + preTagCount + ']' + closeTag;
+
+ preTags[preTagCount] = hashover.EOLTrim (innerHTML);
+ preTagCount++;
+
+ return preMarker;
+ });
+ }
+
+ // Check for various multi-line tags
+ for (var trimTag in this.trimTagRegexes) {
+ if (this.trimTagRegexes.hasOwnProperty (trimTag) === true
+ && this.trimTagRegexes[trimTag]['test'].test (body) === true)
+ {
+ // Trim whitespace
+ body = body.replace (this.trimTagRegexes[trimTag]['replace'], tagTrimmer);
+ }
+ }
+
+ // Break comment into paragraphs
+ var paragraphs = body.split (hashover.regex.paragraphs);
+ var pdComment = '';
+
+ // Wrap comment in paragraph tag
+ // Replace single line breaks with break tags
+ for (var i = 0, il = paragraphs.length; i < il; i++) {
+ pdComment += '<p>' + paragraphs[i].replace (this.lineRegex, '<br>') + '</p>' + hashover.setup['server-eol'];
+ }
+
+ // Replace code tag markers with original code tag HTML
+ if (codeTagCount > 0) {
+ pdComment = pdComment.replace (this.codeTagMarkerRegex, function (marker, number) {
+ return codeTags[number];
+ });
+ }
+
+ // Replace pre tag markers with original pre tag HTML
+ if (preTagCount > 0) {
+ pdComment = pdComment.replace (this.preTagMarkerRegex, function (marker, number) {
+ return preTags[number];
+ });
+ }
+
+ // Add comment data to template
+ template.comment = pdComment;
+ } else {
+ // Append notice class
+ classes += ' hashover-notice ' + comment['notice-class'];
+
+ // Add notice to template
+ template.comment = comment.notice;
+
+ // Add name HTML to template
+ template.name = hashover.strings.parseTemplate (hashover.ui['name-wrapper'], {
+ class: nameClass,
+ link: comment.title
+ });
+ }
+
+ // Comment HTML template
+ var html = hashover.strings.parseTemplate (hashover.ui['theme'], template);
+
+ // Recursively parse replies
+ if (comment.replies !== undefined) {
+ for (var reply = 0, total = comment.replies.length; reply < total; reply++) {
+ replies += this.parse (comment.replies[reply], comment, collapse);
+ }
+ }
+
+ // Wrap comment HTML
+ var wrapper = hashover.strings.parseTemplate (hashover.ui['comment-wrapper'], {
+ permalink: permalink,
+ class: classes,
+ html: html + replies
+ });
+
+ return wrapper;
+ }
+};
diff --git a/bootstrap/comments/frontend/commentvalidator.js b/bootstrap/comments/frontend/commentvalidator.js
new file mode 100644
index 0000000..c129568
--- /dev/null
+++ b/bootstrap/comments/frontend/commentvalidator.js
@@ -0,0 +1,51 @@
+// Validate a comment form (commentvalidator.js)
+HashOver.prototype.commentValidator = function (form, skipComment, isReply)
+{
+ skipComment = skipComment || false;
+
+ // Check each input field for if they are required
+ for (var field in this.setup['field-options']) {
+ // Skip other people's prototypes
+ if (this.setup['field-options'].hasOwnProperty (field) !== true) {
+ continue;
+ }
+
+ // Check if the field is required, and that the input exists
+ if (this.setup['field-options'][field] === 'required' && form[field] !== undefined) {
+ // Check if it has a value
+ if (form[field].value === '') {
+ // If not, add a class indicating a failed post
+ this.classes.add (form[field], 'hashover-emphasized-input');
+
+ // Focus the input
+ form[field].focus ();
+
+ // Return error message to display to the user
+ return this.strings.sprintf (this.locale['field-needed'], [
+ this.locale[field]
+ ]);
+ }
+
+ // Remove class indicating a failed post
+ this.classes.remove (form[field], 'hashover-emphasized-input');
+ }
+ }
+
+ // Check if a comment was given
+ if (skipComment !== true && form.comment.value === '') {
+ // If not, add a class indicating a failed post
+ this.classes.add (form.comment, 'hashover-emphasized-input');
+
+ // Focus the comment textarea
+ form.comment.focus ();
+
+ // Error message to display to the user
+ var localeKey = (isReply === true) ? 'reply-needed' : 'comment-needed';
+ var errorMessage = this.locale[localeKey];
+
+ // Return a error message to display to the user
+ return errorMessage;
+ }
+
+ return true;
+};
diff --git a/bootstrap/comments/frontend/constructor.js b/bootstrap/comments/frontend/constructor.js
new file mode 100644
index 0000000..349ca8e
--- /dev/null
+++ b/bootstrap/comments/frontend/constructor.js
@@ -0,0 +1,51 @@
+// @licstart The following is the entire license notice for the
+// JavaScript code in this page.
+//
+// Copyright (C) 2010-2018 Jacob Barkdull
+// This file is part of HashOver.
+//
+// HashOver is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// HashOver is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with HashOver. If not, see <http://www.gnu.org/licenses/>.
+//
+// @licend The above is the entire license notice for the
+// JavaScript code in this page.
+
+"use strict";
+
+// Initial HashOver constructor (constructor.js)
+function HashOver (options)
+{
+ // Reference to this HashOver object
+ var hashover = this;
+
+ // Self-executing backend wait loop
+ (function backendWait () {
+ // Check if we're on the first instance or if HashOver is prepared
+ if (HashOver.prepared === true || HashOver.instanceCount === 0) {
+ // If so, execute the real constructor
+ HashOver.instantiator.call (hashover, options);
+ } else {
+ // If not, check again in 100 milliseconds
+ setTimeout (backendWait, 100);
+ }
+ }) ();
+};
+
+// Constructor to add HashOver methods to
+var HashOverConstructor = HashOver;
+
+// Indicator that backend information has been received (constructor.js)
+HashOver.prepared = false;
+
+// Initial HashOver instance count (constructor.js)
+HashOver.instanceCount = 0;
diff --git a/bootstrap/comments/frontend/datetime.js b/bootstrap/comments/frontend/datetime.js
new file mode 100644
index 0000000..69425e9
--- /dev/null
+++ b/bootstrap/comments/frontend/datetime.js
@@ -0,0 +1,136 @@
+// Collection of convenient date and time functions (datetime.js)
+HashOverConstructor.prototype.dateTime = {
+ offsetRegex: /[0-9]{2}/g,
+ dashesRegex: /-/g,
+
+ // Simple PHP date function port
+ format: function (format, date)
+ {
+ format = format || 'DATE_ISO8601';
+ date = date || new Date ();
+
+ var hours = date.getHours ();
+ var ampm = (hours >= 12) ? 'pm' : 'am';
+ var day = date.getDate ();
+ var weekDay = date.getDay ();
+ var dayName = this.parent.locale['day-names'][weekDay];
+ var monthIndex = date.getMonth ();
+ var monthName = this.parent.locale['month-names'][monthIndex];
+ var hours12 = (hours % 12) ? hours % 12 : 12;
+ var minutes = date.getMinutes ();
+ var month = monthIndex + 1;
+ var offsetHours = (date.getTimezoneOffset() / 60) * 100;
+ var offset = ((offsetHours < 1000) ? '0' : '') + offsetHours;
+ var offsetColon = offset.match (this.offsetRegex).join (':');
+ var offsetPositivity = (offsetHours > 0) ? '-' : '+';
+ var seconds = date.getSeconds ();
+ var year = date.getFullYear ();
+
+ var characters = {
+ a: ampm,
+ A: ampm.toUpperCase (),
+ d: (day < 10) ? '0' + day : day,
+ D: dayName.substr (0, 3),
+ F: monthName,
+ g: hours12,
+ G: hours,
+ h: (hours12 < 10) ? '0' + hours12 : hours12,
+ H: (hours < 10) ? '0' + hours : hours,
+ i: (minutes < 10) ? '0' + minutes : minutes,
+ j: day,
+ l: dayName,
+ m: (month < 10) ? '0' + month : month,
+ M: monthName.substr (0, 3),
+ n: month,
+ N: weekDay + 1,
+ O: offsetPositivity + offset,
+ P: offsetPositivity + offsetColon,
+ s: (seconds < 10) ? '0' + seconds : seconds,
+ w: weekDay,
+ y: ('' + year).substr (2),
+ Y: year
+ };
+
+ // Convert dashes to underscores
+ var dateConstant = format.replace (this.dashesRegex, '_');
+
+ // Convert constant to uppercase
+ dateConstant = dateConstant.toUpperCase ();
+
+ switch (dateConstant) {
+ case 'DATE_ATOM':
+ case 'DATE_RFC3339':
+ case 'DATE_W3C': {
+ format = 'Y-m-d\TH:i:sP';
+ break;
+ }
+
+ case 'DATE_COOKIE': {
+ format = 'l, d-M-Y H:i:s';
+ break;
+ }
+
+ case 'DATE_ISO8601': {
+ format = 'Y-m-d\TH:i:sO';
+ break;
+ }
+
+ case 'DATE_RFC822':
+ case 'DATE_RFC1036': {
+ format = 'D, d M y H:i:s O';
+ break;
+ }
+
+ case 'DATE_RFC850': {
+ format = 'l, d-M-y H:i:s';
+ break;
+ }
+
+ case 'DATE_RFC1123':
+ case 'DATE_RFC2822':
+ case 'DATE_RSS': {
+ format = 'D, d M Y H:i:s O';
+ break;
+ }
+
+ case 'GNOME_DATE': {
+ format = 'D M d, g:i A';
+ break;
+ }
+
+ case 'US_DATE': {
+ format = 'm/d/Y';
+ break;
+ }
+
+ case 'STANDARD_DATE': {
+ format = 'Y-m-d';
+ break;
+ }
+
+ case '12H_TIME': {
+ format = 'g:ia';
+ break;
+ }
+
+ case '24H_TIME': {
+ format = 'H:i';
+ break;
+ }
+ }
+
+ var formatParts = format.split ('');
+
+ for (var i = 0, c, il = formatParts.length; i < il; i++) {
+ if (i > 0 && formatParts[i - 1] === '\\') {
+ formatParts[i - 1] = '';
+ continue;
+ }
+
+ c = formatParts[i];
+ formatParts[i] = characters[c] || c;
+ }
+
+ return formatParts.join ('');
+ }
+};
diff --git a/bootstrap/comments/frontend/displayerror.js b/bootstrap/comments/frontend/displayerror.js
new file mode 100644
index 0000000..2749a81
--- /dev/null
+++ b/bootstrap/comments/frontend/displayerror.js
@@ -0,0 +1,12 @@
+// Get main HashOver UI element (displayerror.js)
+HashOverConstructor.prototype.displayError = function (json, id)
+{
+ // Get main HashOver element
+ var mainElement = this.getMainElement (id);
+
+ // Error message HTML code
+ var messageHTML = '<b>HashOver</b>: ' + json.message;
+
+ // Display error in main HashOver element
+ mainElement.innerHTML = messageHTML;
+};
diff --git a/bootstrap/comments/frontend/editcomment.js b/bootstrap/comments/frontend/editcomment.js
new file mode 100644
index 0000000..75b1bb3
--- /dev/null
+++ b/bootstrap/comments/frontend/editcomment.js
@@ -0,0 +1,96 @@
+// Displays edit form (editcomment.js)
+HashOver.prototype.editComment = function (comment)
+{
+ if (comment['editable'] !== true) {
+ return false;
+ }
+
+ // Reference to this object
+ var hashover = this;
+
+ // Get permalink from comment JSON object
+ var permalink = comment.permalink;
+
+ // Get edit link element
+ var link = this.elements.get ('edit-link-' + permalink, true);
+
+ // Get file
+ var file = this.permalinks.getFile (permalink);
+
+ // Get name and website
+ var name = comment.name || '';
+ var website = comment.website || '';
+
+ // Get and clean comment body
+ var body = comment.body.replace (this.regex.links, '$1');
+
+ // Create edit form element
+ var form = this.elements.create ('form', {
+ id: 'hashover-edit-' + permalink,
+ className: 'hashover-edit-form',
+ action: '/formactions',
+ method: 'post'
+ });
+
+ // Place edit form fields into form
+ form.innerHTML = hashover.strings.parseTemplate (hashover.ui['edit-form'], {
+ permalink: permalink,
+ file: file,
+ name: name,
+ website: website,
+ body: body
+ });
+
+ // Prevent input submission
+ this.preventSubmit (form);
+
+ // Add edit form to page
+ var editForm = this.elements.get ('placeholder-edit-form-' + permalink, true);
+ editForm.appendChild (form);
+
+ // Set status dropdown menu option to comment status
+ this.elements.exists ('edit-status-' + permalink, function (status) {
+ var statuses = [ 'approved', 'pending', 'deleted' ];
+
+ if (comment.status !== undefined) {
+ status.selectedIndex = statuses.indexOf (comment.status);
+ }
+ });
+
+ // Blank out password field
+ setTimeout (function () {
+ if (form.password !== undefined) {
+ form.password.value = '';
+ }
+ }, 100);
+
+ // Uncheck subscribe checkbox if user isn't subscribed
+ if (comment.subscribed !== true) {
+ this.elements.get ('edit-subscribe-' + permalink, true).checked = null;
+ }
+
+ // Displays onClick confirmation dialog for comment deletion
+ this.elements.get ('edit-delete-' + permalink, true).onclick = function ()
+ {
+ return confirm (hashover.locale['delete-comment']);
+ };
+
+ // Change "Edit" link to "Cancel" link
+ this.cancelSwitcher ('edit', link, editForm, permalink);
+
+ // Attach event listeners to "Save Edit" button
+ var saveEdit = this.elements.get ('edit-post-' + permalink, true);
+
+ // Get the element of comment being replied to
+ var destination = this.elements.get (permalink, true);
+
+ // Attach click event to formatting revealer hyperlink
+ this.formattingOnclick ('edit', permalink);
+
+ // Set onclick and onsubmit event handlers
+ this.elements.duplicateProperties (saveEdit, [ 'onclick', 'onsubmit' ], function () {
+ return hashover.postComment (destination, form, this, hashover.AJAXEdit, 'edit', permalink, link.onclick, false, true);
+ });
+
+ return false;
+};
diff --git a/bootstrap/comments/frontend/elements.js b/bootstrap/comments/frontend/elements.js
new file mode 100644
index 0000000..8d7a510
--- /dev/null
+++ b/bootstrap/comments/frontend/elements.js
@@ -0,0 +1,82 @@
+// Collection of convenient element functions (elements.js)
+HashOverConstructor.prototype.elements = {
+ cache: {},
+
+ // Shorthand for Document.getElementById ()
+ get: function (id, force, prefix)
+ {
+ id = (prefix !== false) ? 'hashover-' + id : id;
+
+ if (force === true || !this.cache[id]) {
+ this.cache[id] = document.getElementById (id);
+ }
+
+ return this.cache[id];
+ },
+
+ // Execute callback function if element isn't false
+ exists: function (element, callback, prefix)
+ {
+ if (element = this.get (element, true, prefix)) {
+ return callback (element);
+ }
+
+ return false;
+ },
+
+ // Adds properties to an element
+ addProperties: function (element, properties)
+ {
+ element = element || document.createElement ('span');
+ properties = properties || {};
+
+ // Add each property to element
+ for (var property in properties) {
+ if (properties.hasOwnProperty (property) === false) {
+ continue;
+ }
+
+ // Property value
+ var value = properties[property];
+
+ // If the property is an object add each item to existing property
+ if (!!value && value.constructor === Object) {
+ this.addProperties (element[property], value);
+ continue;
+ }
+
+ element[property] = value;
+ }
+
+ return element;
+ },
+
+ // Creates an element with attributes
+ create: function (tagName, attributes)
+ {
+ tagName = tagName || 'span';
+ attributes = attributes || {};
+
+ // Create element
+ var element = document.createElement (tagName);
+
+ // Add properties to element
+ element = this.addProperties (element, attributes);
+
+ return element;
+ },
+
+ // Adds duplicate event listeners to an element
+ duplicateProperties: function (element, names, value)
+ {
+ var properties = {};
+
+ // Construct a properties object with duplicate values
+ for (var i = 0, il = names.length; i < il; i++) {
+ properties[(names[i])] = value;
+ }
+
+ // Add the properties to the object
+ return this.addProperties (element, properties);
+ }
+};
diff --git a/bootstrap/comments/frontend/emailvalidator.js b/bootstrap/comments/frontend/emailvalidator.js
new file mode 100644
index 0000000..7e7cf85
--- /dev/null
+++ b/bootstrap/comments/frontend/emailvalidator.js
@@ -0,0 +1,42 @@
+// Handles display of various e-mail warnings (emailvalidator.js)
+HashOver.prototype.emailValidator = function (form, subscribe, type, permalink, isReply, isEdit)
+{
+ if (form.email === undefined) {
+ return true;
+ }
+
+ // Whether the e-mail form is empty
+ if (form.email.value === '') {
+ // Return true if user unchecked the subscribe checkbox
+ if (this.elements.get (subscribe, true).checked === false) {
+ return true;
+ }
+
+ // If so, warn the user that they won't receive reply notifications
+ // if (confirm (this.locale['no-email-warning']) === false) {
+ // form.email.focus ();
+ // return false;
+ // }
+
+ } else {
+ // If not, check if the e-mail is valid
+ if (this.regex.email.test (form.email.value) === false) {
+ // Return true if user unchecked the subscribe checkbox
+ if (this.elements.get (subscribe, true).checked === false) {
+ form.email.value = '';
+ return true;
+ }
+
+ // Get message from locales
+ var message = this.locale['invalid-email'];
+
+ // Show the message and focus the e-mail input
+ this.messages.show (message, type, permalink, true, isReply, isEdit);
+ form.email.focus ();
+
+ return false;
+ }
+ }
+
+ return true;
+};
diff --git a/bootstrap/comments/frontend/embedimage.js b/bootstrap/comments/frontend/embedimage.js
new file mode 100644
index 0000000..e6bb085
--- /dev/null
+++ b/bootstrap/comments/frontend/embedimage.js
@@ -0,0 +1,36 @@
+// Convert URL to embed image HTML (embedimage.js)
+HashOverConstructor.prototype.comments.embedImage = function (url)
+{
+ // Reference to the parent object
+ var hashover = this.parent;
+
+ // Get image extension from URL
+ var urlExtension = url.split ('#')[0];
+ urlExtension = urlExtension.split ('?')[0];
+ urlExtension = urlExtension.split ('.');
+ urlExtension = urlExtension.pop ();
+
+ // Check if the image extension is an allowed type
+ if (hashover.setup['image-extensions'].indexOf (urlExtension) > -1) {
+ // If so, create a wrapper element for the embedded image
+ var embeddedImage = hashover.elements.create ('span', {
+ className: 'hashover-embedded-image-wrapper'
+ });
+
+ // Append an image tag to the embedded image wrapper
+ embeddedImage.appendChild (hashover.elements.create ('img', {
+ className: 'hashover-embedded-image',
+ src: hashover.setup['image-placeholder'],
+ title: hashover.locale['external-image-tip'],
+ alt: 'External Image',
+
+ dataset: {
+ placeholder: hashover.setup['image-placeholder'],
+ url: url
+ }
+ }));
+
+ // And return the embedded image HTML
+ return embeddedImage.outerHTML;
+ }
+};
diff --git a/bootstrap/comments/frontend/eoltrim.js b/bootstrap/comments/frontend/eoltrim.js
new file mode 100644
index 0000000..d42c48d
--- /dev/null
+++ b/bootstrap/comments/frontend/eoltrim.js
@@ -0,0 +1,5 @@
+// Trims leading and trailing newlines from a string (eoltrim.js)
+HashOverConstructor.prototype.EOLTrim = function (string)
+{
+ return string.replace (this.regex.EOLTrim, '');
+};
diff --git a/bootstrap/comments/frontend/formattingonclick.js b/bootstrap/comments/frontend/formattingonclick.js
new file mode 100644
index 0000000..b94294c
--- /dev/null
+++ b/bootstrap/comments/frontend/formattingonclick.js
@@ -0,0 +1,25 @@
+// Attach click event to formatting revealer hyperlinks (formattingonclick.js)
+HashOver.prototype.formattingOnclick = function (type, permalink)
+{
+ permalink = (permalink !== undefined) ? '-' + permalink : '';
+
+ // Reference to this object
+ var hashover = this;
+
+ // Get formatting message elements
+ var formattingID = type + '-formatting';
+ var formatting = this.elements.get (formattingID + permalink, true);
+ var formattingMessage = this.elements.get (formattingID + '-message' + permalink, true);
+
+ // Attach click event to formatting revealer hyperlink
+ formatting.onclick = function ()
+ {
+ if (hashover.classes.contains (formattingMessage, 'hashover-message-open')) {
+ hashover.messages.close (formattingMessage);
+ return false;
+ }
+
+ hashover.messages.open (formattingMessage);
+ return false;
+ }
+};
diff --git a/bootstrap/comments/frontend/formevents.js b/bootstrap/comments/frontend/formevents.js
new file mode 100644
index 0000000..d8770d1
--- /dev/null
+++ b/bootstrap/comments/frontend/formevents.js
@@ -0,0 +1,17 @@
+// Returns false if key event is the enter key (formevents.js)
+HashOver.prototype.enterCheck = function (event)
+{
+ return (event.keyCode === 13) ? false : true;
+};
+
+// Prevents enter key on inputs from submitting form (formevents.js)
+HashOver.prototype.preventSubmit = function (form)
+{
+ // Get login info inputs
+ var infoInputs = form.getElementsByClassName ('hashover-input-info');
+
+ // Set enter key press to return false
+ for (var i = 0, il = infoInputs.length; i < il; i++) {
+ infoInputs[i].onkeypress = this.enterCheck;
+ }
+};
diff --git a/bootstrap/comments/frontend/getallcomments.js b/bootstrap/comments/frontend/getallcomments.js
new file mode 100644
index 0000000..b5df6b0
--- /dev/null
+++ b/bootstrap/comments/frontend/getallcomments.js
@@ -0,0 +1,25 @@
+// "Flatten" the comments object (getallcomments.js)
+HashOver.prototype.getAllComments = function (comments)
+{
+ var commentsCopy = this.cloneObject (comments);
+ var output = [];
+
+ function descend (comment)
+ {
+ output.push (comment);
+
+ if (comment.replies !== undefined) {
+ for (var reply = 0, total = comment.replies.length; reply < total; reply++) {
+ descend (comment.replies[reply]);
+ }
+
+ delete comment.replies;
+ }
+ }
+
+ for (var comment = 0, total = commentsCopy.length; comment < total; comment++) {
+ descend (commentsCopy[comment]);
+ }
+
+ return output;
+};
diff --git a/bootstrap/comments/frontend/getbackendqueries.js b/bootstrap/comments/frontend/getbackendqueries.js
new file mode 100644
index 0000000..811862b
--- /dev/null
+++ b/bootstrap/comments/frontend/getbackendqueries.js
@@ -0,0 +1,28 @@
+// Get supported HashOver backend queries from options (getbackendqueries.js)
+HashOverConstructor.getBackendQueries = function (options)
+{
+ // URL queries array
+ var queries = [];
+
+ // Check if options parameter is an object
+ if (options && options.constructor === Object) {
+ // If so, use URL and title if available
+ var url = options.url || this.getURL (options.canonical);
+ var title = options.title || this.getTitle ();
+
+ // And add thread to request if told to
+ if (options.thread !== undefined) {
+ queries.push ('thread=' + options.thread);
+ }
+ } else {
+ // If not, get the URL and title from the page
+ var url = this.getURL ();
+ var title = this.getTitle ();
+ }
+
+ // Default backend request POST data
+ queries.push ('url=' + encodeURIComponent (url));
+ queries.push ('title=' + encodeURIComponent (title));
+
+ return queries;
+};
diff --git a/bootstrap/comments/frontend/getmainelement.js b/bootstrap/comments/frontend/getmainelement.js
new file mode 100644
index 0000000..58da468
--- /dev/null
+++ b/bootstrap/comments/frontend/getmainelement.js
@@ -0,0 +1,38 @@
+// Get main HashOver UI element (getmainelement.js)
+HashOverConstructor.prototype.getMainElement = function (id)
+{
+ id = id || 'hashover';
+
+ // Attempt to get main HashOver element
+ var element = document.getElementById (id);
+
+ // Check if the HashOver element exists
+ if (element === null) {
+ // If not, get HashOver script tag
+ var script = this.constructor.script;
+
+ // Create div tag for HashOver comments to appear in
+ element = this.elements.create ('div', { id: id });
+
+ // Place the main HashOver element on the page
+ script.parentNode.insertBefore (element, script);
+ }
+
+ // Add main HashOver class
+ this.classes.add (element, 'hashover');
+
+ // Check if HashOver is prepared
+ if (this.constructor.prepared === true) {
+ // If so, add class for differentiating desktop and mobile styling
+ this.classes.add (element, 'hashover-' + this.setup['device-type']);
+
+ // And add class to indicate user login status
+ if (this.setup['user-is-logged-in'] === true) {
+ this.classes.add (element, 'hashover-logged-in');
+ } else {
+ this.classes.add (element, 'hashover-logged-out');
+ }
+ }
+
+ return element;
+};
diff --git a/bootstrap/comments/frontend/gettitle.js b/bootstrap/comments/frontend/gettitle.js
new file mode 100644
index 0000000..1c2e905
--- /dev/null
+++ b/bootstrap/comments/frontend/gettitle.js
@@ -0,0 +1,5 @@
+// Get the page title (gettitle.js)
+HashOverConstructor.getTitle = function ()
+{
+ return document.title;
+};
diff --git a/bootstrap/comments/frontend/geturl.js b/bootstrap/comments/frontend/geturl.js
new file mode 100644
index 0000000..aa8191b
--- /dev/null
+++ b/bootstrap/comments/frontend/geturl.js
@@ -0,0 +1,35 @@
+// Get either the actual page URL or the declared canonical URL (geturl.js)
+HashOverConstructor.getURL = function (canonical)
+{
+ canonical = (canonical !== false);
+
+ // Get the actual page URL
+ var url = window.location.href.split ('#')[0];
+
+ // Return the actual page URL if told to
+ if (canonical === false) {
+ return url;
+ }
+
+ // Otherwise, return the declared canonical URL if available
+ if (typeof (document.querySelector) === 'function') {
+ var canonical = document.querySelector ('link[rel="canonical"]');
+
+ if (canonical !== null && canonical.href !== undefined) {
+ url = canonical.href;
+ }
+ } else {
+ // Fallback for old web browsers without querySelector
+ var head = document.head || document.getElementsByTagName ('head')[0];
+ var links = head.getElementsByTagName ('link');
+
+ for (var i = 0, il = links.length; i < il; i++) {
+ if (links[i].rel === 'canonical') {
+ url = links[i].href;
+ break;
+ }
+ }
+ }
+
+ return url;
+};
diff --git a/bootstrap/comments/frontend/hidemorelink.js b/bootstrap/comments/frontend/hidemorelink.js
new file mode 100644
index 0000000..a9b4c4d
--- /dev/null
+++ b/bootstrap/comments/frontend/hidemorelink.js
@@ -0,0 +1,39 @@
+// For showing more comments, via AJAX or removing a class (hidemorelink.js)
+HashOver.prototype.hideMoreLink = function (finishedCallback)
+{
+ finishedCallback = finishedCallback || null;
+
+ // Reference to this object
+ var hashover = this;
+
+ // Add class to hide the more hyperlink
+ this.classes.add (this.instance['more-link'], 'hashover-hide-more-link');
+
+ setTimeout (function () {
+ // Remove the more hyperlink from page
+ if (hashover.instance['sort-section'].contains (hashover.instance['more-link']) === true) {
+ hashover.instance['sort-section'].removeChild (hashover.instance['more-link']);
+ }
+
+ // Show comment count and sort options
+ hashover.elements.get ('count-wrapper').style.display = '';
+
+ // Show popular comments section
+ hashover.elements.exists ('popular-section', function (popularSection) {
+ popularSection.style.display = '';
+ });
+
+ // Get each hidden comment element
+ var collapsed = hashover.instance['sort-section'].getElementsByClassName ('hashover-hidden');
+
+ // Remove hidden comment class from each comment
+ for (var i = collapsed.length - 1; i >= 0; i--) {
+ hashover.classes.remove (collapsed[i], 'hashover-hidden');
+ }
+
+ // Execute callback function
+ if (finishedCallback !== null) {
+ finishedCallback ();
+ }
+ }, 350);
+};
diff --git a/bootstrap/comments/frontend/htmltonodelist.js b/bootstrap/comments/frontend/htmltonodelist.js
new file mode 100644
index 0000000..9f3d4d7
--- /dev/null
+++ b/bootstrap/comments/frontend/htmltonodelist.js
@@ -0,0 +1,5 @@
+// Converts an HTML string to DOM NodeList (htmltonodelist.js)
+HashOver.prototype.HTMLToNodeList = function (html)
+{
+ return this.elements.create ('div', { innerHTML: html }).childNodes;
+};
diff --git a/bootstrap/comments/frontend/incrementcounts.js b/bootstrap/comments/frontend/incrementcounts.js
new file mode 100644
index 0000000..74e3d8c
--- /dev/null
+++ b/bootstrap/comments/frontend/incrementcounts.js
@@ -0,0 +1,11 @@
+// Increase comment counts (incrementcounts.js)
+HashOver.prototype.incrementCounts = function (isReply)
+{
+ // Count top level comments
+ if (isReply === false) {
+ this.instance['primary-count']++;
+ }
+
+ // Increase all count
+ this.instance['total-count']++;
+};
diff --git a/bootstrap/comments/frontend/init.js b/bootstrap/comments/frontend/init.js
new file mode 100644
index 0000000..b6feb53
--- /dev/null
+++ b/bootstrap/comments/frontend/init.js
@@ -0,0 +1,250 @@
+// HashOver UI initialization process (init.js)
+HashOver.prototype.init = function ()
+{
+ // Store start time
+ this.execStart = Date.now ();
+
+ // Reference to this object
+ var hashover = this;
+
+ // Get the main HashOver element
+ var mainElement = this.getMainElement ();
+
+ // Form events that get the same listeners
+ var formEvents = [ 'onclick', 'onsubmit' ];
+
+ // Current page URL without the hash
+ var pageURL = window.location.href.split ('#')[0];
+
+ // Current page URL hash
+ var pageHash = window.location.hash.substr (1);
+
+ // Scrolls to a specified element
+ function scrollToElement (id)
+ {
+ hashover.elements.exists (id, function (element) {
+ element.scrollIntoView ({ behavior: 'smooth' });
+ }, false);
+ }
+
+ // Callback for scrolling a comment into view on page load
+ function scrollCommentIntoView ()
+ {
+ // Check if the comments are collapsed
+ if (hashover.setup['collapses-comments'] !== false) {
+ // Check if comment exists on the page
+ var linkedHidden = hashover.elements.exists (pageHash, function (comment) {
+ // Check if the comment is visable
+ if (hashover.classes.contains (comment, 'hashover-hidden') === false) {
+ // If so, scroll to the comment
+ scrollToElement (pageHash);
+ return true;
+ }
+
+ return false;
+ }, false);
+
+ // Check if the linked comment is hidden
+ if (linkedHidden === false) {
+ // If not, show more comments
+ hashover.showMoreComments (hashover.instance['more-link'], function () {
+ // Then scroll to comment
+ scrollToElement (pageHash);
+ });
+ }
+ } else {
+ // If not, scroll to comment normally
+ scrollToElement (pageHash);
+ }
+ }
+
+ // Callback for scrolling a comment into view on page load
+ function prepareScroll ()
+ {
+ // Scroll the main HashOver element into view
+ if (pageHash.match (/comments|hashover/)) {
+ scrollToElement (pageHash);
+ }
+
+ // Check if we're scrolling to a comment
+ if (pageHash.match (/hashover-c[0-9]+r*/)) {
+ // If so, check if the user interface is collapsed
+ if (hashover.setup['collapses-interface'] !== false) {
+ // If so, scroll to it after uncollapsing the interface
+ hashover.uncollapseInterface (scrollCommentIntoView);
+ } else {
+ // If not, scroll to the comment directly
+ scrollCommentIntoView ();
+ }
+ }
+
+ // Open the message element if there's a message
+ if (hashover.elements.get ('message').textContent !== '') {
+ hashover.messages.show ();
+ }
+ }
+
+ // Page load event handler
+ function onLoad ()
+ {
+ setTimeout (prepareScroll, 500);
+ }
+
+ // Append theme CSS if enabled
+ this.optionalMethod ('appendCSS');
+
+ // Put number of comments into "hashover-comment-count" identified HTML element
+ if (this.instance['total-count'] !== 0) {
+ this.elements.exists ('comment-count', function (countElement) {
+ countElement.textContent = hashover.instance['total-count'];
+ });
+
+ // Append RSS feed if enabled
+ this.optionalMethod ('appendRSS');
+ }
+
+ // Add initial HTML to page
+ if ('insertAdjacentHTML' in mainElement) {
+ mainElement.textContent = '';
+ mainElement.insertAdjacentHTML ('beforeend', this.instance['initial-html']);
+ } else {
+ mainElement.innerHTML = this.instance['initial-html'];
+ }
+
+ // Add main HashOver element to this HashOver instance
+ this.instance['main-element'] = mainElement;
+
+ // Templatify UI HTML strings
+ for (var element in this.ui) {
+ this.ui[element] = this.strings.templatify (this.ui[element]);
+ }
+
+ // Get sort div element
+ this.instance['sort-section'] = this.elements.get ('sort-section');
+
+ // Display most popular comments
+ this.elements.exists ('top-comments', function (topComments) {
+ if (hashover.instance.comments.popular[0] !== undefined) {
+ hashover.parseAll (hashover.instance.comments.popular, topComments, false, true);
+ }
+ });
+
+ // Add initial event handlers
+ this.parseAll (this.instance.comments.primary, this.instance['sort-section'], this.setup['collapses-comments']);
+
+ // Create uncollapse UI hyperlink if enabled
+ this.optionalMethod ('uncollapseInterfaceLink');
+
+ // Create uncollapse comments hyperlink if enabled
+ this.optionalMethod ('uncollapseCommentsLink');
+
+ // Attach click event to formatting revealer hyperlink
+ this.formattingOnclick ('main');
+
+ // Get some various form elements
+ var postButton = this.elements.get ('post-button');
+ var formElement = this.elements.get ('form');
+
+ // Set onclick and onsubmit event handlers
+ this.elements.duplicateProperties (postButton, formEvents, function () {
+ return hashover.postComment (hashover.instance['sort-section'], formElement, postButton, hashover.AJAXPost);
+ });
+
+ // Check if login is enabled
+ if (this.setup['allows-login'] !== false) {
+ // Attach event listeners to "Login" button
+ if (this.setup['user-is-logged-in'] !== true) {
+ var loginButton = this.elements.get ('login-button');
+
+ // Set onclick and onsubmit event handlers
+ this.elements.duplicateProperties (loginButton, formEvents, function () {
+ return hashover.validateComment (true, formElement);
+ });
+ }
+ }
+
+ // Five method sort
+ this.elements.exists ('sort-select', function (sortSelect) {
+ sortSelect.onchange = function ()
+ {
+ // Check if the comments are collapsed
+ if (hashover.setup['collapses-comments'] !== false) {
+ // If so, get the select div
+ var sortSelectDiv = hashover.elements.get ('sort');
+
+ // And uncollapse the comments before sorting
+ hashover.showMoreComments (sortSelectDiv, function () {
+ hashover.instance['sort-section'].textContent = '';
+ hashover.sortComments (sortSelect.value);
+ });
+ } else {
+ // If not, sort the comments normally
+ hashover.instance['sort-section'].textContent = '';
+ hashover.sortComments (sortSelect.value);
+ }
+ };
+ });
+
+ // Display reply or edit form when the proper URL queries are set
+ if (pageURL.match (/hashover-(reply|edit)=/)) {
+ var permalink = pageURL.replace (/.*?hashover-(edit|reply)=(c[0-9r\-pop]+).*?/, '$2');
+
+ if (!pageURL.match ('hashover-edit=')) {
+ // Check if the comments are collapsed
+ if (hashover.setup['collapses-comments'] !== false) {
+ // If so, show more comments
+ this.showMoreComments (this.instance['more-link'], function () {
+ // Then display and scroll to reply form
+ hashover.replyToComment (permalink);
+ scrollToElement (pageHash);
+ });
+ } else {
+ // If not, display and scroll to reply form
+ this.replyToComment (permalink);
+ scrollToElement (pageHash);
+ }
+ } else {
+ var isPop = permalink.match ('-pop');
+ var comments = (isPop) ? this.instance.comments.popular : this.instance.comments.primary;
+
+ // Check if the comments are collapsed
+ if (hashover.setup['collapses-comments'] !== false) {
+ // If so, show more comments
+ this.showMoreComments (this.instance['more-link'], function () {
+ // Then display and scroll to edit form
+ hashover.editComment (hashover.permalinks.getComment (permalink, comments));
+ scrollToElement (pageHash);
+ });
+ } else {
+ // If not, display and scroll to edit form
+ this.editComment (this.permalinks.getComment (permalink, comments));
+ scrollToElement (pageHash);
+ }
+ }
+ }
+
+ // Store end time
+ this.execEnd = Date.now ();
+
+ // Store execution time
+ this.execTime = this.execEnd - this.execStart;
+
+ // Log execution time and memory usage in JavaScript console
+ if (window.console) {
+ console.log (this.strings.sprintf ('HashOver: front-end %d ms, backend %d ms, %s', [
+ this.execTime, this.statistics['execution-time'], this.statistics['script-memory']
+ ]));
+ }
+
+ // Page onload compatibility wrapper
+ if (window.addEventListener) {
+ // Rest of the world
+ window.addEventListener ('load', onLoad, false);
+ } else {
+ // IE ~8
+ window.attachEvent ('onload', onLoad);
+ }
+
+ // Execute page load event handler manually
+ onLoad ();
+};
diff --git a/bootstrap/comments/frontend/instantiate.js b/bootstrap/comments/frontend/instantiate.js
new file mode 100644
index 0000000..15044cd
--- /dev/null
+++ b/bootstrap/comments/frontend/instantiate.js
@@ -0,0 +1,4 @@
+// Instantiate after the DOM is parsed
+HashOver.onReady (function () {
+ window.hashover = new HashOver ();
+});
diff --git a/bootstrap/comments/frontend/instantiator.js b/bootstrap/comments/frontend/instantiator.js
new file mode 100644
index 0000000..830321b
--- /dev/null
+++ b/bootstrap/comments/frontend/instantiator.js
@@ -0,0 +1,66 @@
+// Real constructor (instantiator.js)
+HashOver.instantiator = function (options)
+{
+ // Get backend queries
+ var queries = HashOver.getBackendQueries (options);
+
+ // Check if we're instantiating the first HashOver object
+ if (HashOver.prepared !== true) {
+ // If so, set query indicating a request for backend information
+ queries.push ('prepare=true');
+ }
+
+ // Reference to this object
+ var hashover = this;
+
+ // Increment HashOver instance count
+ HashOver.instanceCount++;
+
+ // Backend request path
+ var requestPath = '/commentsajax';
+
+ // Handle backend request
+ this.ajax ('POST', requestPath, queries, function (json) {
+ // Handle error messages
+ if (json.message !== undefined) {
+ hashover.displayError (json);
+ return;
+ }
+
+ // Set the backend information
+ if (HashOver.prepared !== true) {
+ // Locales from HashOver backend
+ HashOver.prototype.locale = json.locale;
+
+ // Setup information from HashOver back-end
+ HashOver.prototype.setup = json.setup;
+
+ // UI HTML from HashOver back-end
+ HashOver.prototype.ui = json.ui;
+
+ // Mark HashOver as prepared
+ HashOver.prepared = true;
+ }
+
+ // Thread information from HashOver back-end
+ hashover.instance = json.instance;
+
+ // Backend execution time and memory usage statistics
+ hashover.statistics = json.statistics;
+
+ // Initiate HashOver
+ hashover.init ();
+ }, true);
+
+ // Set instance number to current instance count
+ this.instanceNumber = HashOver.instanceCount;
+
+ // Add parent proterty to all prototype objects
+ for (var name in this) {
+ var value = this[name];
+
+ if (value && value.constructor === Object) {
+ value.parent = this;
+ }
+ }
+};
diff --git a/bootstrap/comments/frontend/likecomment.js b/bootstrap/comments/frontend/likecomment.js
new file mode 100644
index 0000000..15a0d25
--- /dev/null
+++ b/bootstrap/comments/frontend/likecomment.js
@@ -0,0 +1,78 @@
+// For liking comments (likecomment.js)
+HashOver.prototype.likeComment = function (action, permalink)
+{
+ // Reference to this object
+ var hashover = this;
+
+ var file = this.permalinks.getFile (permalink);
+ var actionLink = this.elements.get (action + '-' + permalink, true);
+ var likesElement = this.elements.get (action + 's-' + permalink, true);
+ var likePath = '/likecomment';
+
+ // Set request queries
+ var queries = [
+ 'url=' + encodeURIComponent (this.instance['page-url']),
+ 'thread=' + this.instance['thread-name'],
+ 'comment=' + file,
+ 'action=' + action
+ ];
+
+ // When loaded update like count
+ this.ajax ('POST', likePath, queries, function (likeResponse) {
+ // If a message is returned display it to the user
+ if (likeResponse.message !== undefined) {
+ alert (likeResponse.message);
+ return;
+ }
+
+ // If an error is returned display a standard error to the user
+ if (likeResponse.error !== undefined) {
+ alert ('Error! Something went wrong!');
+ return;
+ }
+
+ // Get number of likes
+ var likesKey = (action !== 'dislike') ? 'likes' : 'dislikes';
+ var likes = likeResponse[likesKey] || 0;
+
+ // Change "Like" button title and class
+ if (hashover.classes.contains (actionLink, 'hashover-' + action) === true) {
+ // Change class to indicate the comment has been liked/disliked
+ hashover.classes.add (actionLink, 'hashover-' + action + 'd');
+ hashover.classes.remove (actionLink, 'hashover-' + action);
+ actionLink.title = (action === 'like') ? hashover.locale['liked-comment'] : hashover.locale['disliked-comment'];
+ actionLink.textContent = (action === 'like') ? hashover.locale['liked'] : hashover.locale['disliked'];
+
+ // Add listener to change link text to "Unlike" on mouse over
+ if (action === 'like') {
+ hashover.mouseOverChanger (actionLink, 'unlike', 'liked');
+ }
+ } else {
+ // Change class to indicate the comment is unliked
+ hashover.classes.add (actionLink, 'hashover-' + action);
+ hashover.classes.remove (actionLink, 'hashover-' + action + 'd');
+ actionLink.title = (action === 'like') ? hashover.locale['like-comment'] : hashover.locale['dislike-comment'];
+ actionLink.textContent = (action === 'like') ? hashover.locale['like'][0] : hashover.locale['dislike'][0];
+
+ // Add listener to change link text to "Unlike" on mouse over
+ if (action === 'like') {
+ hashover.mouseOverChanger (actionLink, null, null);
+ }
+ }
+
+ if (likes > 0) {
+ // Decide if locale is pluralized
+ var plural = (likes !== 1) ? 1 : 0;
+ var likeLocale = (action !== 'like') ? 'dislike' : 'like';
+ var likeCount = likes + ' ' + hashover.locale[likeLocale][plural];
+
+ // Change number of likes; set font weight bold
+ likesElement.textContent = likeCount;
+ likesElement.style.fontWeight = 'bold';
+ } else {
+ // Remove like count; set font weight normal
+ likesElement.textContent = '';
+ likesElement.style.fontWeight = '';
+ }
+ }, true);
+};
diff --git a/bootstrap/comments/frontend/markdown.js b/bootstrap/comments/frontend/markdown.js
new file mode 100644
index 0000000..3d26f4c
--- /dev/null
+++ b/bootstrap/comments/frontend/markdown.js
@@ -0,0 +1,97 @@
+// Parses a string as markdown (markdown.js)
+HashOverConstructor.prototype.markdown = {
+ blockCodeRegex: /```([\s\S]+?)```/g,
+ inlineCodeRegex: /(^|[^a-z0-9`])`([^`]+?[\s\S]+?)`([^a-z0-9`]|$)/ig,
+ blockCodeMarker: /CODE_BLOCK\[([0-9]+)\]/g,
+ inlineCodeMarker: /CODE_INLINE\[([0-9]+)\]/g,
+
+ // Array for inline code and code block markers
+ codeMarkers: {
+ block: { marks: [], count: 0 },
+ inline: { marks: [], count: 0 }
+ },
+
+ // Markdown patterns to search for
+ markdownSearch: [
+ /\*\*([^ *])([\s\S]+?)([^ *])\*\*/g,
+ /\*([^ *])([\s\S]+?)([^ *])\*/g,
+ /(^|\W)_([^_]+?[\s\S]+?)_(\W|$)/g,
+ /__([^ _])([\s\S]+?)([^ _])__/g,
+ /~~([^ ~])([\s\S]+?)([^ ~])~~/g
+ ],
+
+ // HTML replacements for markdown patterns
+ markdownReplace: [
+ '<strong>$1$2$3</strong>',
+ '<em>$1$2$3</em>',
+ '$1<u>$2</u>$3',
+ '<u>$1$2$3</u>',
+ '<s>$1$2$3</s>'
+ ],
+
+ // Replaces markdown for inline code with a marker
+ codeReplace: function (fullTag, first, second, third, display)
+ {
+ var markName = 'CODE_' + display.toUpperCase ();
+ var markCount = this.codeMarkers[display].count++;
+
+ if (display !== 'block') {
+ var codeMarker = first + markName + '[' + markCount + ']' + third;
+ this.codeMarkers[display].marks[markCount] = this.parent.EOLTrim (second);
+ } else {
+ var codeMarker = markName + '[' + markCount + ']';
+ this.codeMarkers[display].marks[markCount] = this.parent.EOLTrim (first);
+ }
+
+ return codeMarker;
+ },
+
+ parse: function (string)
+ {
+ // Reference to this object
+ var markdown = this;
+
+ // Reset marker arrays
+ this.codeMarkers = {
+ block: { marks: [], count: 0 },
+ inline: { marks: [], count: 0 }
+ };
+
+ // Replace code blocks with markers
+ string = string.replace (this.blockCodeRegex, function (fullTag, first, second, third) {
+ return markdown.codeReplace (fullTag, first, second, third, 'block');
+ });
+
+ // Break string into paragraphs
+ var paragraphs = string.split (this.parent.regex.paragraphs);
+
+ // Run through each paragraph replacing markdown patterns
+ for (var i = 0, il = paragraphs.length; i < il; i++) {
+ // Replace code tags with marker text
+ paragraphs[i] = paragraphs[i].replace (this.inlineCodeRegex, function (fullTag, first, second, third) {
+ return markdown.codeReplace (fullTag, first, second, third, 'inline');
+ });
+
+ // Perform each markdown regular expression on the current paragraph
+ for (var r = 0, rl = this.markdownSearch.length; r < rl; r++) {
+ // Replace markdown patterns
+ paragraphs[i] = paragraphs[i].replace (this.markdownSearch[r], this.markdownReplace[r]);
+ }
+
+ // Return the original markdown code with HTML replacement
+ paragraphs[i] = paragraphs[i].replace (this.inlineCodeMarker, function (marker, number) {
+ return '<code class="hashover-inline">' + markdown.codeMarkers.inline.marks[number] + '</code>';
+ });
+ }
+
+ // Join paragraphs
+ string = paragraphs.join (this.parent.setup['server-eol'] + this.parent.setup['server-eol']);
+
+ // Replace code block markers with original markdown code
+ string = string.replace (this.blockCodeMarker, function (marker, number) {
+ return '<code>' + markdown.codeMarkers.block.marks[number] + '</code>';
+ });
+
+ return string;
+ }
+};
diff --git a/bootstrap/comments/frontend/messages.js b/bootstrap/comments/frontend/messages.js
new file mode 100644
index 0000000..3d34ff2
--- /dev/null
+++ b/bootstrap/comments/frontend/messages.js
@@ -0,0 +1,178 @@
+// Collection of HashOver message element related functions (messages.js)
+HashOver.prototype.messages = {
+ timeouts: {},
+
+ // Gets a computed element style by property
+ computeStyle: function (element, proterty, type)
+ {
+ // Check for modern browser support (Mozilla Firefox, Google Chrome)
+ if (window.getComputedStyle !== undefined) {
+ // If found, get the computed styles for the element
+ var computedStyle = window.getComputedStyle (element, null);
+
+ // And get the specific property
+ computedStyle = computedStyle.getPropertyValue (proterty);
+ } else {
+ // Otherwise, assume we're in IE
+ var computedStyle = element.currentStyle[proterty];
+ }
+
+ // Cast value to specified type
+ switch (type) {
+ case 'int': {
+ computedStyle = computedStyle.replace (/px|em/, '');
+ computedStyle = parseInt (computedStyle) || 0;
+ break;
+ }
+
+ case 'float': {
+ computedStyle = computedStyle.replace (/px|em/, '');
+ computedStyle = parseFloat (computedStyle) || 0.0;
+ break;
+ }
+ }
+
+ return computedStyle;
+ },
+
+ // Gets the client height of a message element
+ getHeight: function (element, setChild)
+ {
+ setChild = setChild || false;
+
+ var firstChild = element.children[0];
+ var maxHeight = 80;
+
+ // If so, set max-height style to initial
+ firstChild.style.maxHeight = 'initial';
+
+ // Get various computed styles
+ var borderTop = this.computeStyle (firstChild, 'border-top-width', 'int');
+ var borderBottom = this.computeStyle (firstChild, 'border-bottom-width', 'int');
+ var marginBottom = this.computeStyle (firstChild, 'margin-bottom', 'int');
+ var border = borderTop + borderBottom;
+
+ // Calculate its client height
+ maxHeight = firstChild.clientHeight + border + marginBottom;
+
+ // Set its max-height style as well if told to
+ if (setChild === true) {
+ firstChild.style.maxHeight = maxHeight + 'px';
+ } else {
+ firstChild.style.maxHeight = '';
+ }
+
+ return maxHeight;
+ },
+
+ // Open a message element
+ open: function (element)
+ {
+ // Add classes to indicate message element is open
+ this.parent.classes.remove (element, 'hashover-message-animated');
+ this.parent.classes.add (element, 'hashover-message-open');
+
+ var maxHeight = this.getHeight (element);
+ var firstChild = element.children[0];
+
+ // Reference to the parent object
+ var parent = this.parent;
+
+ // Remove class indicating message element is open
+ this.parent.classes.remove (element, 'hashover-message-open');
+
+ setTimeout (function () {
+ // Add class to indicate message element is open
+ parent.classes.add (element, 'hashover-message-open');
+ parent.classes.add (element, 'hashover-message-animated');
+
+ // Set max-height styles
+ element.style.maxHeight = maxHeight + 'px';
+ firstChild.style.maxHeight = maxHeight + 'px';
+
+ // Set max-height style to initial after transition
+ setTimeout (function () {
+ element.style.maxHeight = 'initial';
+ firstChild.style.maxHeight = 'initial';
+ }, 150);
+ }, 150);
+ },
+
+ // Close a message element
+ close: function (element)
+ {
+ // Set max-height style to specific height before transition
+ element.style.maxHeight = this.getHeight (element, true) + 'px';
+
+ // Reference to the parent object
+ var parent = this.parent;
+
+ setTimeout (function () {
+ // Remove max-height style from message elements
+ element.children[0].style.maxHeight = '';
+ element.style.maxHeight = '';
+
+ // Remove classes indicating message element is open
+ parent.classes.remove (element, 'hashover-message-open');
+ parent.classes.remove (element, 'hashover-message-error');
+ }, 150);
+ },
+
+ // Handle message element(s)
+ show: function (messageText, type, permalink, error, isReply, isEdit)
+ {
+ type = type || 'main';
+ permalink = permalink || '';
+ error = error || false;
+ isReply = isReply || false;
+ isEdit = isEdit || false;
+
+ // Reference to this object
+ var messages = this;
+
+ // Decide which message element to use
+ if (isEdit === true) {
+ // An edit form message
+ var container = this.parent.elements.get ('edit-message-container-' + permalink, true);
+ var message = this.parent.elements.get ('edit-message-' + permalink, true);
+ } else {
+ if (isReply !== true) {
+ // The primary comment form message
+ var container = this.parent.elements.get ('message-container', true);
+ var message = this.parent.elements.get ('message', true);
+ } else {
+ // Of a reply form message
+ var container = this.parent.elements.get ('reply-message-container-' + permalink, true);
+ var message = this.parent.elements.get ('reply-message-' + permalink, true);
+ }
+ }
+
+ if (messageText !== undefined && messageText !== '') {
+ // Add message text to element
+ message.textContent = messageText;
+
+ // Add class to indicate message is an error if set
+ if (error === true) {
+ this.parent.classes.add (container, 'hashover-message-error');
+ }
+ }
+
+ // Add class to indicate message element is open
+ this.open (container);
+
+ // Add the comment to message counts
+ if (this.timeouts[permalink] === undefined) {
+ this.timeouts[permalink] = {};
+ }
+
+ // Clear necessary timeout
+ if (this.timeouts[permalink][type] !== undefined) {
+ clearTimeout (this.timeouts[permalink][type]);
+ }
+
+ // Add timeout to close message element after 10 seconds
+ this.timeouts[permalink][type] = setTimeout (function () {
+ messages.close (container);
+ }, 10000);
+ }
+};
diff --git a/bootstrap/comments/frontend/mouseoverchanger.js b/bootstrap/comments/frontend/mouseoverchanger.js
new file mode 100644
index 0000000..6da0dfe
--- /dev/null
+++ b/bootstrap/comments/frontend/mouseoverchanger.js
@@ -0,0 +1,23 @@
+// Changes Element.textContent onmouseover and reverts onmouseout (mouseoverchanger.js)
+HashOver.prototype.mouseOverChanger = function (element, over, out)
+{
+ // Reference to this object
+ var hashover = this;
+
+ if (over === null || out === null) {
+ element.onmouseover = null;
+ element.onmouseout = null;
+
+ return false;
+ }
+
+ element.onmouseover = function ()
+ {
+ this.textContent = hashover.locale[over];
+ };
+
+ element.onmouseout = function ()
+ {
+ this.textContent = hashover.locale[out];
+ };
+};
diff --git a/bootstrap/comments/frontend/onready.js b/bootstrap/comments/frontend/onready.js
new file mode 100644
index 0000000..f740f31
--- /dev/null
+++ b/bootstrap/comments/frontend/onready.js
@@ -0,0 +1,14 @@
+// Execute a callback when the page HTML is parsed and ready (onready.js)
+HashOverConstructor.onReady = function (callback)
+{
+ // Check if document HTML has been parsed
+ if (document.readyState === 'interactive') {
+ // If so, execute callback immediately
+ callback ();
+ } else {
+ // If not, execute callback after the DOM is parsed
+ document.addEventListener ('DOMContentLoaded', function () {
+ callback ();
+ }, false);
+ }
+};
diff --git a/bootstrap/comments/frontend/openembeddedimage.js b/bootstrap/comments/frontend/openembeddedimage.js
new file mode 100644
index 0000000..fc95e16
--- /dev/null
+++ b/bootstrap/comments/frontend/openembeddedimage.js
@@ -0,0 +1,50 @@
+// Callback to close the embedded image (openembeddedimage.js)
+HashOverConstructor.prototype.closeEmbeddedImage = function (image)
+{
+ // Reset source
+ image.src = image.dataset.placeholder;
+
+ // Reset title
+ image.title = this.locale['external-image-tip'];
+
+ // Remove loading class from wrapper
+ this.classes.remove (image.parentNode, 'hashover-loading');
+};
+
+// Onclick callback function for embedded images (openembeddedimage.js)
+HashOverConstructor.prototype.openEmbeddedImage = function (image)
+{
+ // Reference to this object
+ var hashover = this;
+
+ // If embedded image is open, close it and return false
+ if (image.src === image.dataset.url) {
+ this.closeEmbeddedImage (image);
+ return false;
+ }
+
+ // Set title
+ image.title = this.locale['loading'];
+
+ // Add loading class to wrapper
+ this.classes.add (image.parentNode, 'hashover-loading');
+
+ // Change title and remove load event handler once image is loaded
+ image.onload = function ()
+ {
+ image.title = hashover.locale['click-to-close'];
+ image.onload = null;
+
+ // Remove loading class from wrapper
+ hashover.classes.remove (image.parentNode, 'hashover-loading');
+ };
+
+ // Close embedded image if any error occurs
+ image.onerror = function ()
+ {
+ hashover.closeEmbeddedImage (this);
+ };
+
+ // Set placeholder image to embedded source
+ image.src = image.dataset.url;
+};
diff --git a/bootstrap/comments/frontend/optionalmethod.js b/bootstrap/comments/frontend/optionalmethod.js
new file mode 100644
index 0000000..86415a8
--- /dev/null
+++ b/bootstrap/comments/frontend/optionalmethod.js
@@ -0,0 +1,11 @@
+// Calls a method that may or may not exist (optionalmethod.js)
+HashOverConstructor.prototype.optionalMethod = function (name, args, object)
+{
+ var method = object ? this[object][name] : this[name];
+ var context = object ? this[object] : this;
+
+ // Check if the method exists
+ if (method && typeof (method) === 'function') {
+ return method.apply (context, args);
+ }
+};
diff --git a/bootstrap/comments/frontend/parseall.js b/bootstrap/comments/frontend/parseall.js
new file mode 100644
index 0000000..eb6f907
--- /dev/null
+++ b/bootstrap/comments/frontend/parseall.js
@@ -0,0 +1,27 @@
+// Run all comments in array data through comments.parse function (parseall.js)
+HashOver.prototype.parseAll = function (comments, element, collapse, popular, sort, method)
+{
+ popular = popular || false;
+ sort = sort || false;
+ method = method || 'ascending';
+
+ // Comments HTML
+ var html = '';
+
+ // Parse every comment
+ for (var i = 0, il = comments.length; i < il; i++) {
+ html += this.comments.parse (comments[i], null, collapse, sort, method, popular);
+ }
+
+ // Add comments to element's innerHTML
+ if ('insertAdjacentHTML' in element) {
+ element.insertAdjacentHTML ('beforeend', html);
+ } else {
+ element.innerHTML = html;
+ }
+
+ // Add control events
+ for (var i = 0, il = comments.length; i < il; i++) {
+ this.addControls (comments[i]);
+ }
+};
diff --git a/bootstrap/comments/frontend/permalinks.js b/bootstrap/comments/frontend/permalinks.js
new file mode 100644
index 0000000..ffd6659
--- /dev/null
+++ b/bootstrap/comments/frontend/permalinks.js
@@ -0,0 +1,57 @@
+// Collection of permalink-related functions (permalinks.js)
+HashOverConstructor.prototype.permalinks = {
+ // Returns the permalink of a comment's parent
+ getParent: function (permalink, flatten)
+ {
+ flatten = flatten || false;
+
+ var parent = permalink.split ('r');
+ var length = parent.length - 1;
+
+ // Limit depth if in stream mode
+ if (this.parent.setup['stream-mode'] === true && flatten === true) {
+ length = Math.min (this.parent.setup['stream-depth'], length);
+ }
+
+ // Check if there is a parent after flatten
+ if (length > 0) {
+ // If so, remove child from permalink
+ parent = parent.slice (0, length);
+
+ // Return parent permalink as string
+ return parent.join ('r');
+ }
+
+ return null;
+ },
+
+ // Find a comment by its permalink
+ getComment: function (permalink, comments)
+ {
+ // Run through all comments
+ for (var i = 0, il = comments.length; i < il; i++) {
+ // Return comment if its permalink matches
+ if (comments[i].permalink === permalink) {
+ return comments[i];
+ }
+
+ // Recursively check replies when present
+ if (comments[i].replies !== undefined) {
+ var comment = this.getComment (permalink, comments[i].replies);
+
+ if (comment !== null) {
+ return comment;
+ }
+ }
+ }
+
+ // Otherwise return null
+ return null;
+ },
+
+ // Generate file from permalink
+ getFile: function (permalink)
+ {
+ return permalink.slice(1).replace(/r/g, '-').replace ('-pop', '');
+ }
+};
diff --git a/bootstrap/comments/frontend/postcomment.js b/bootstrap/comments/frontend/postcomment.js
new file mode 100644
index 0000000..9934af9
--- /dev/null
+++ b/bootstrap/comments/frontend/postcomment.js
@@ -0,0 +1,30 @@
+// For posting comments, both traditionally and via AJAX (postcomment.js)
+HashOver.prototype.postComment = function (destination, form, button, callback, type, permalink, close, isReply, isEdit)
+{
+ type = type || 'main';
+ permalink = permalink || '';
+ isReply = isReply || false;
+ isEdit = isEdit || false;
+
+ // Return false if comment is invalid
+ if (this.validateComment (false, form, type, permalink, isReply, isEdit) === false) {
+ return false;
+ }
+
+ // Disable button
+ setTimeout (function () {
+ button.disabled = true;
+ }, 500);
+
+ // Post by sending an AJAX request if enabled
+ if (this.postRequest) {
+ return this.postRequest.apply (this, arguments);
+ }
+
+ // Re-enable button after 20 seconds
+ setTimeout (function () {
+ button.disabled = false;
+ }, 20000);
+
+ return true;
+};
diff --git a/bootstrap/comments/frontend/postrequest.js b/bootstrap/comments/frontend/postrequest.js
new file mode 100644
index 0000000..43aea82
--- /dev/null
+++ b/bootstrap/comments/frontend/postrequest.js
@@ -0,0 +1,102 @@
+// Posts comments via AJAX (postrequest.js)
+HashOver.prototype.postRequest = function (destination, form, button, callback, type, permalink, close, isReply, isEdit)
+{
+ close = close || null;
+
+ var formElements = form.elements;
+ var elementsLength = formElements.length;
+ var queries = [];
+
+ // Reference to this object
+ var hashover = this;
+
+ // AJAX response handler
+ function commentHandler (json)
+ {
+ // Check if JSON includes a comment
+ if (json.comment !== undefined) {
+ // If so, execute callback function
+ callback.apply (hashover, [ json, permalink, destination, isReply ]);
+
+ // Execute callback function if one was provided
+ if (close !== null) {
+ close ();
+ }
+
+ // Get the comment element by its permalink
+ var scrollToElement = hashover.elements.get (json.comment.permalink, true);
+
+ // Scroll comment into view
+ scrollToElement.scrollIntoView ({ behavior: 'smooth' });
+
+ // And clear the comment form
+ form.comment.value = '';
+ } else {
+ // If not, display the message return instead
+ hashover.messages.show (json.message, type, permalink, (json.type === 'error'), isReply, isEdit);
+ return false;
+ }
+
+ // Re-enable button on success
+ setTimeout (function () {
+ button.disabled = false;
+ }, 1000);
+ }
+
+ // Sends a request to post a comment
+ function sendRequest ()
+ {
+ hashover.ajax ('POST', form.action, queries, commentHandler, true);
+ }
+
+ // Get all form input names and values
+ for (var i = 0; i < elementsLength; i++) {
+ // Skip login/logout input
+ if (formElements[i].name === 'login' || formElements[i].name === 'logout') {
+ continue;
+ }
+
+ // Skip unchecked checkboxes
+ if (formElements[i].type === 'checkbox' && formElements[i].checked !== true) {
+ continue;
+ }
+
+ // Skip delete input
+ if (formElements[i].name === 'delete') {
+ continue;
+ }
+
+ // Add query to queries array
+ queries.push (formElements[i].name + '=' + encodeURIComponent (formElements[i].value));
+ }
+
+ // Add AJAX query to queries array
+ queries.push ('ajax=yes');
+
+ // Check if autologin is enabled and user isn't admin
+ if (this.setup['user-is-admin'] !== true
+ && this.setup['uses-auto-login'] !== false)
+ {
+ // If so, check if the user is logged in
+ if (this.setup['user-is-logged-in'] !== true || isEdit === true) {
+ // If not, send a login request
+ var loginQueries = queries.concat (['login=Login']);
+
+ // Send post comment request after login
+ this.ajax ('POST', form.action, loginQueries, sendRequest, true);
+ } else {
+ // If so, send post comment request normally
+ sendRequest ();
+ }
+ } else {
+ // If not, send post comment request
+ sendRequest ();
+ }
+
+ // Re-enable button after 20 seconds
+ setTimeout (function () {
+ button.disabled = false;
+ }, 20000);
+
+ return false;
+};
diff --git a/bootstrap/comments/frontend/regex.js b/bootstrap/comments/frontend/regex.js
new file mode 100644
index 0000000..6fc27b2
--- /dev/null
+++ b/bootstrap/comments/frontend/regex.js
@@ -0,0 +1,10 @@
+// Pre-compiled regular expressions (regex.js)
+HashOverConstructor.prototype.regex = new (function () {
+ this.urls = '((http|https|ftp):\/\/[a-z0-9-@:;%_\+.~#?&\/=]+)',
+ this.links = new RegExp (this.urls + '( {0,1})', 'ig'),
+ this.thread = /^(c[0-9r]+)r[0-9\-pop]+$/,
+ this.imageTags = new RegExp ('\\[img\\]<a.*?>' + this.urls + '</a>\\[/img\\]', 'ig'),
+ this.EOLTrim = /^[\r\n]+|[\r\n]+$/g,
+ this.paragraphs = /(?:\r\n|\r|\n){2}/g,
+ this.email = /\S+@\S+/
+}) ();
diff --git a/bootstrap/comments/frontend/replytocomment.js b/bootstrap/comments/frontend/replytocomment.js
new file mode 100644
index 0000000..2e0fc45
--- /dev/null
+++ b/bootstrap/comments/frontend/replytocomment.js
@@ -0,0 +1,55 @@
+// Displays reply form (replytocomment.js)
+HashOver.prototype.replyToComment = function (permalink)
+{
+ // Reference to this object
+ var hashover = this;
+
+ // Get reply link element
+ var link = this.elements.get ('reply-link-' + permalink, true);
+
+ // Get file
+ var file = this.permalinks.getFile (permalink);
+
+ // Create reply form element
+ var form = this.elements.create ('form', {
+ id: 'hashover-reply-' + permalink,
+ className: 'hashover-reply-form',
+ action: '/formactions',
+ method: 'post'
+ });
+
+ // Place reply fields into form
+ form.innerHTML = hashover.strings.parseTemplate (hashover.ui['reply-form'], {
+ permalink: permalink,
+ file: file
+ });
+
+ // Prevent input submission
+ this.preventSubmit (form);
+
+ // Add form to page
+ var replyForm = this.elements.get ('placeholder-reply-form-' + permalink, true);
+ replyForm.appendChild (form);
+
+ // Change "Reply" link to "Cancel" link
+ this.cancelSwitcher ('reply', link, replyForm, permalink);
+
+ // Attach event listeners to "Post Reply" button
+ var postReply = this.elements.get ('reply-post-' + permalink, true);
+
+ // Get the element of comment being replied to
+ var destination = this.elements.get (permalink, true);
+
+ // Attach click event to formatting revealer hyperlink
+ this.formattingOnclick ('reply', permalink);
+
+ // Set onclick and onsubmit event handlers
+ this.elements.duplicateProperties (postReply, [ 'onclick', 'onsubmit' ], function () {
+ return hashover.postComment (destination, form, this, hashover.AJAXPost, 'reply', permalink, link.onclick, true, false);
+ });
+
+ // Focus comment field
+ form.comment.focus ();
+
+ return true;
+};
diff --git a/bootstrap/comments/frontend/script.js b/bootstrap/comments/frontend/script.js
new file mode 100644
index 0000000..8e5c4d5
--- /dev/null
+++ b/bootstrap/comments/frontend/script.js
@@ -0,0 +1,13 @@
+// Get the current HashOver script tag (script.js)
+HashOverConstructor.script = (function ()
+{
+ // Get various scripts
+ var loaderScript = document.getElementById ('hashover-loader');
+ var scripts = document.getElementsByTagName ('script');
+
+ // Use either the current script or an identified loader script
+ var currentScript = document.currentScript || loaderScript;
+
+ // Otherwise, fallback to the last script encountered
+ return currentScript || scripts[scripts.length - 1];
+}) ();
diff --git a/bootstrap/comments/frontend/showmorecomments.js b/bootstrap/comments/frontend/showmorecomments.js
new file mode 100644
index 0000000..35a1ce0
--- /dev/null
+++ b/bootstrap/comments/frontend/showmorecomments.js
@@ -0,0 +1,69 @@
+// onClick event for more button (showmorecomments.js)
+HashOver.prototype.showMoreComments = function (element, finishedCallback)
+{
+ finishedCallback = finishedCallback || null;
+
+ // Reference to this object
+ var hashover = this;
+
+ // Do nothing if already showing all comments
+ if (this.instance['showing-more'] === true) {
+ // Execute callback function
+ if (finishedCallback !== null) {
+ finishedCallback ();
+ }
+
+ return false;
+ }
+
+ // Check if AJAX is enabled
+ if (this.setup['uses-ajax'] !== false) {
+ // If so, set request path
+ var requestPath = '/loadcomments';
+
+ // Set URL queries
+ var queries = [
+ 'url=' + encodeURIComponent (this.instance['page-url']),
+ 'title=' + encodeURIComponent (this.instance['page-title']),
+ 'thread=' + encodeURIComponent (this.instance['thread-name']),
+ 'start=' + encodeURIComponent (this.setup['collapse-limit']),
+ 'ajax=yes'
+ ];
+
+ // Handle AJAX request return data
+ this.ajax ('POST', requestPath, queries, function (json) {
+ // Store start time
+ var execStart = Date.now ();
+
+ // Display the comments
+ hashover.appendComments (json.primary);
+
+ // Remove loading class from element
+ hashover.classes.remove (element, 'hashover-loading');
+
+ // Hide the more hyperlink and display the comments
+ hashover.hideMoreLink (finishedCallback);
+
+ // Store execution time
+ var execTime = Date.now () - execStart;
+
+ // Log execution time and memory usage in JavaScript console
+ if (window.console) {
+ console.log (hashover.strings.sprintf ('HashOver: front-end %d ms, backend %d ms, %s', [
+ execTime, json.statistics['execution-time'], json.statistics['script-memory']
+ ]));
+ }
+ }, true);
+
+ // And set class to indicate loading to element
+ this.classes.add (element, 'hashover-loading');
+ } else {
+ // If not, hide the more hyperlink and display the comments
+ this.hideMoreLink (finishedCallback);
+ }
+
+ // Set all comments as shown
+ this.instance['showing-more'] = true;
+
+ return false;
+};
diff --git a/bootstrap/comments/frontend/sortcomments.js b/bootstrap/comments/frontend/sortcomments.js
new file mode 100644
index 0000000..26d0c58
--- /dev/null
+++ b/bootstrap/comments/frontend/sortcomments.js
@@ -0,0 +1,235 @@
+// Comment sorting (sortcomments.js)
+HashOver.prototype.sortComments = function (method)
+{
+ var sortArray = [];
+ var defaultName = this.setup['default-name'];
+
+ // Returns the sum number of replies in a comment thread
+ function replyPropertySum (comment, callback)
+ {
+ var sum = 0;
+
+ // Check if there are replies to the current comment
+ if (comment.replies !== undefined) {
+ // If so, run through them adding up the number of replies
+ for (var i = 0, il = comment.replies.length; i < il; i++) {
+ sum += replyPropertySum (comment.replies[i], callback);
+ }
+ }
+
+ // Calculate the sum based on the give callback
+ sum += callback (comment);
+
+ return sum;
+ }
+
+ // Calculation callback for `replyPropertySum` function
+ function replyCounter (comment)
+ {
+ return (comment.replies) ? comment.replies.length : 0;
+ }
+
+ // Calculation callback for `replyPropertySum` function
+ function netLikes (comment)
+ {
+ return (comment.likes || 0) - (comment.dislikes || 0);
+ }
+
+ // Sort methods
+ switch (method) {
+ // Sort all comment in reverse order
+ case 'descending': {
+ // Get all comments
+ var tmpArray = this.getAllComments (this.instance.comments.primary);
+
+ // And reverse the comments
+ sortArray = tmpArray.reverse ();
+
+ break;
+ }
+
+ // Sort all comments by date
+ case 'by-date': {
+ sortArray = this.getAllComments (this.instance.comments.primary).sort (function (a, b) {
+ if (a['sort-date'] === b['sort-date']) {
+ return 1;
+ }
+
+ return b['sort-date'] - a['sort-date'];
+ });
+
+ break;
+ }
+
+ // Sort all comment by net number of likes
+ case 'by-likes': {
+ sortArray = this.getAllComments (this.instance.comments.primary).sort (function (a, b) {
+ a.likes = a.likes || 0;
+ b.likes = b.likes || 0;
+ a.dislikes = a.dislikes || 0;
+ b.dislikes = b.dislikes || 0;
+
+ return (b.likes - b.dislikes) - (a.likes - a.dislikes);
+ });
+
+ break;
+ }
+
+ // Sort all comment by number of replies
+ case 'by-replies': {
+ // Clone the primary comments
+ var tmpArray = this.cloneObject (this.instance.comments.primary);
+
+ // And sort them by number of replies
+ sortArray = tmpArray.sort (function (a, b) {
+ var ac = (!!a.replies) ? a.replies.length : 0;
+ var bc = (!!b.replies) ? b.replies.length : 0;
+
+ return bc - ac;
+ });
+
+ break;
+ }
+
+ // Sort threads by the sum of replies to its comments
+ case 'by-discussion': {
+ // Clone the primary comments
+ var tmpArray = this.cloneObject (this.instance.comments.primary);
+
+ // And sort them by the sum of each comment's replies
+ sortArray = tmpArray.sort (function (a, b) {
+ var replyCountA = replyPropertySum (a, replyCounter);
+ var replyCountB = replyPropertySum (b, replyCounter);
+
+ return replyCountB - replyCountA;
+ });
+
+ break;
+ }
+
+ // Sort threads by the sum of likes to it's comments
+ case 'by-popularity': {
+ // Clone the primary comments
+ var tmpArray = this.cloneObject (this.instance.comments.primary);
+
+ // And sort them by the sum of each comment's net likes
+ sortArray = tmpArray.sort (function (a, b) {
+ var likeCountA = replyPropertySum (a, netLikes);
+ var likeCountB = replyPropertySum (b, netLikes);
+
+ return likeCountB - likeCountA;
+ });
+
+ break;
+ }
+
+ // Sort all comment by the commenter names
+ case 'by-name': {
+ // Get all comments
+ var tmpArray = this.getAllComments (this.instance.comments.primary);
+
+ // And sort them alphabetically by the commenter names
+ sortArray = tmpArray.sort (function (a, b) {
+ var nameA = (a.name || defaultName).toLowerCase ();
+ var nameB = (b.name || defaultName).toLowerCase ();
+
+ nameA = (nameA.charAt (0) === '@') ? nameA.slice (1) : nameA;
+ nameB = (nameB.charAt (0) === '@') ? nameB.slice (1) : nameB;
+
+ if (nameA > nameB) {
+ return 1;
+ }
+
+ if (nameA < nameB) {
+ return -1;
+ }
+
+ return 0;
+ });
+
+ break;
+ }
+
+ // Sort threads in reverse order
+ case 'threaded-descending': {
+ // Clone the primary comments
+ var tmpArray = this.cloneObject (this.instance.comments.primary);
+
+ // And reverse the comments
+ sortArray = tmpArray.reverse ();
+
+ break;
+ }
+
+ // Sort threads by date
+ case 'threaded-by-date': {
+ // Clone the primary comments
+ var tmpArray = this.cloneObject (this.instance.comments.primary);
+
+ // And sort them by date
+ sortArray = tmpArray.sort (function (a, b) {
+ if (a['sort-date'] === b['sort-date']) {
+ return 1;
+ }
+
+ return b['sort-date'] - a['sort-date'];
+ });
+
+ break;
+ }
+
+ // Sort threads by net likes
+ case 'threaded-by-likes': {
+ // Clone the primary comments
+ var tmpArray = this.cloneObject (this.instance.comments.primary);
+
+ // And sort them by the net number of likes
+ sortArray = tmpArray.sort (function (a, b) {
+ a.likes = a.likes || 0;
+ b.likes = b.likes || 0;
+ a.dislikes = a.dislikes || 0;
+ b.dislikes = b.dislikes || 0;
+
+ return (b.likes - b.dislikes) - (a.likes - a.dislikes);
+ });
+
+ break;
+ }
+
+ // Sort threads by commenter names
+ case 'threaded-by-name': {
+ // Clone the primary comments
+ var tmpArray = this.cloneObject (this.instance.comments.primary);
+
+ // And sort them alphabetically by the commenter names
+ sortArray = tmpArray.sort (function (a, b) {
+ var nameA = (a.name || defaultName).toLowerCase ();
+ var nameB = (b.name || defaultName).toLowerCase ();
+
+ nameA = (nameA.charAt (0) === '@') ? nameA.slice (1) : nameA;
+ nameB = (nameB.charAt (0) === '@') ? nameB.slice (1) : nameB;
+
+ if (nameA > nameB) {
+ return 1;
+ }
+
+ if (nameA < nameB) {
+ return -1;
+ }
+
+ return 0;
+ });
+
+ break;
+ }
+
+ // By default simply use the primary comments as-is
+ default: {
+ sortArray = this.instance.comments.primary;
+ break;
+ }
+ }
+
+ // Parse the sorted comments
+ this.parseAll (sortArray, this.instance['sort-section'], false, false, true, method);
+};
diff --git a/bootstrap/comments/frontend/strings.js b/bootstrap/comments/frontend/strings.js
new file mode 100644
index 0000000..270ce92
--- /dev/null
+++ b/bootstrap/comments/frontend/strings.js
@@ -0,0 +1,104 @@
+// Collection of convenient string related functions (strings.js)
+HashOverConstructor.prototype.strings = {
+ // sprintf specifiers regular expression
+ specifiers: /%([cdfs])/g,
+
+ // Curly-brace variable regular expression
+ curlyBraces: /(\{\{.+?\}\})/g,
+
+ // Curly-brace variable name regular expression
+ curlyNames: /\{\{(.+?)\}\}/,
+
+ // Simplistic JavaScript port of sprintf function in C
+ sprintf: function (string, args)
+ {
+ var string = string || '';
+ var args = args || [];
+ var count = 0;
+
+ // Replace specifiers with array items
+ return string.replace (this.specifiers, function (match, type)
+ {
+ // Return the original specifier if there isn't an item for it
+ if (args[count] === undefined) {
+ return match;
+ }
+
+ // Switch through each specific type
+ switch (type) {
+ // Single characters
+ case 'c': {
+ // Use only the first character
+ return args[count++][0];
+ }
+
+ // Integer numbers
+ case 'd': {
+ // Parse item as integer
+ return parseInt (args[count++]);
+ }
+
+ // Floating point numbers
+ case 'f': {
+ // Parse item as float
+ return parseFloat (args[count++]);
+ }
+
+ // Strings
+ case 's': {
+ // Use string as-is
+ return args[count++];
+ }
+ }
+ });
+ },
+
+ templatify: function (text)
+ {
+ var template = text.split (this.curlyBraces);
+ var indexes = {};
+
+ for (var i = 0, il = template.length, curly, name; i < il; i++) {
+ curly = template[i].match (this.curlyNames);
+
+ if (curly && curly.length > 0) {
+ name = curly[1];
+ template[i] = '';
+
+ if (indexes[name] !== undefined) {
+ indexes[name].push (i);
+ } else {
+ indexes[name] = [ i ];
+ }
+ }
+ }
+
+ return {
+ text: template,
+ indexes: indexes
+ }
+ },
+
+ // Parses an HTML template
+ parseTemplate: function (template, data)
+ {
+ if (!template || !template.indexes || !template.text) {
+ return;
+ }
+
+ var textClone = template.text.slice ();
+
+ for (var name in data) {
+ if (template.indexes[name] === undefined) {
+ continue;
+ }
+
+ for (var i = 0, il = template.indexes[name].length, index; i < il; i++) {
+ index = template.indexes[name][i];
+ textClone[index] = data[name];
+ }
+ }
+
+ return textClone.join ('');
+ }
+};
diff --git a/bootstrap/comments/frontend/uncollapsecommentslink.js b/bootstrap/comments/frontend/uncollapsecommentslink.js
new file mode 100644
index 0000000..48fbeb9
--- /dev/null
+++ b/bootstrap/comments/frontend/uncollapsecommentslink.js
@@ -0,0 +1,35 @@
+// Creates the "Show X Other Comments" button (uncollapsecommentslink.js)
+HashOver.prototype.uncollapseCommentsLink = function ()
+{
+ // Check whether there are more than the collapse limit
+ if (this.instance['total-count'] > this.setup['collapse-limit']) {
+ // Create element for the comments
+ this.instance['more-section'] = this.elements.create ('div', {
+ className: 'hashover-more-section'
+ });
+
+ // If so, create "More Comments" hyperlink
+ this.instance['more-link'] = this.elements.create ('a', {
+ href: '#',
+ className: 'hashover-more-link',
+ title: this.instance['more-link-text'],
+ textContent: this.instance['more-link-text'],
+
+ onclick: function () {
+ return hashover.showMoreComments (this);
+ }
+ });
+
+ // Add more button link to sort div
+ this.instance['sort-section'].appendChild (this.instance['more-section']);
+
+ // Add more button link to sort div
+ this.instance['sort-section'].appendChild (this.instance['more-link']);
+
+ // And consider comments collapsed
+ this.instance['showing-more'] = false;
+ } else {
+ // If not, consider all comments shown
+ this.instance['showing-more'] = true;
+ }
+};
diff --git a/bootstrap/comments/frontend/uncollapseinterface.js b/bootstrap/comments/frontend/uncollapseinterface.js
new file mode 100644
index 0000000..7d90b3a
--- /dev/null
+++ b/bootstrap/comments/frontend/uncollapseinterface.js
@@ -0,0 +1,37 @@
+// Uncollapses the user interface (uncollapseinterface.js)
+HashOver.prototype.uncollapseInterface = function (callback)
+{
+ // Elements to unhide
+ var uncollapseIDs = [ 'form-section', 'comments-section', 'end-links' ];
+
+ // Check if the uncollapse interface link exists
+ this.elements.exists ('uncollapse-interface-link', function (uncollapseLink) {
+ // If so, add class to hide the uncollapse interface hyperlink
+ hashover.classes.add (uncollapseLink, 'hashover-hide-more-link');
+
+ // Wait for the default CSS transition
+ setTimeout (function () {
+ // Remove the uncollapse interface hyperlink from page
+ uncollapseLink.parentNode.removeChild (uncollapseLink);
+
+ // Show hidden form elements
+ for (var i = 0, il = uncollapseIDs.length; i < il; i++) {
+ hashover.elements.exists (uncollapseIDs[i], function (element) {
+ element.style.display = '';
+ });
+ }
+
+ // Show popular comments section
+ if (hashover.setup['collapse-limit'] > 0) {
+ hashover.elements.exists ('popular-section', function (popularSection) {
+ popularSection.style.display = '';
+ });
+ }
+
+ // Execute callback
+ if (typeof (callback) === 'function') {
+ callback ();
+ }
+ }, 350);
+ });
+};
diff --git a/bootstrap/comments/frontend/uncollapseinterfacelink.js b/bootstrap/comments/frontend/uncollapseinterfacelink.js
new file mode 100644
index 0000000..0fc3912
--- /dev/null
+++ b/bootstrap/comments/frontend/uncollapseinterfacelink.js
@@ -0,0 +1,24 @@
+// Creates the interface uncollapse button (uncollapseinterfacelink.js)
+HashOver.prototype.uncollapseInterfaceLink = function ()
+{
+ // Decide button text
+ var uncollapseLocale = (this.instance['total-count'] >= 1) ? 'show-number-comments' : 'post-comment-on';
+ var uncollapseText = this.instance[uncollapseLocale];
+
+ // Create hyperlink to uncollapse the comment interface
+ var uncollapseLink = this.elements.create ('a', {
+ id: 'hashover-uncollapse-interface-link',
+ className: 'hashover-more-link',
+ href: '#',
+ title: uncollapseText,
+ textContent: uncollapseText,
+
+ onclick: function () {
+ hashover.uncollapseInterface ();
+ return false;
+ }
+ });
+
+ // Add uncollapse hyperlink to HashOver div
+ hashover.instance['main-element'].appendChild (uncollapseLink);
+};
diff --git a/bootstrap/comments/frontend/validatecomment.js b/bootstrap/comments/frontend/validatecomment.js
new file mode 100644
index 0000000..a852e81
--- /dev/null
+++ b/bootstrap/comments/frontend/validatecomment.js
@@ -0,0 +1,28 @@
+// Validate required comment credentials (validatecomment.js)
+HashOver.prototype.validateComment = function (skipComment, form, type, permalink, isReply, isEdit)
+{
+ skipComment = skipComment || false;
+ type = type || 'main';
+ permalink = permalink || null;
+ isReply = isReply || false;
+ isEdit = isEdit || false;
+
+ // Validate comment form
+ var message = this.commentValidator (form, skipComment, isReply);
+
+ // Display the validator's message
+ if (message !== true) {
+ this.messages.show (message, type, permalink, true, isReply, isEdit);
+ return false;
+ }
+
+ // Validate e-mail if user isn't logged in or is editing
+ if (this.setup['user-is-logged-in'] === false || isEdit === true) {
+ // Return false on any failure
+ if (this.validateEmail (type, permalink, form, isReply, isEdit) === false) {
+ return false;
+ }
+ }
+
+ return true;
+};
diff --git a/bootstrap/comments/frontend/validateemail.js b/bootstrap/comments/frontend/validateemail.js
new file mode 100644
index 0000000..6fedf28
--- /dev/null
+++ b/bootstrap/comments/frontend/validateemail.js
@@ -0,0 +1,20 @@
+// Validate a comment form e-mail field (validateemail.js)
+HashOver.prototype.validateEmail = function (type, permalink, form, isReply, isEdit)
+{
+ type = type || 'main';
+ permalink = permalink || null;
+ isReply = isReply || false;
+ isEdit = isEdit || false;
+
+ // Subscribe checkbox ID
+ var subscribe = type + '-subscribe';
+
+ // Check whether comment is an reply or edit
+ if (isReply === true || isEdit === true) {
+ // If so, use form subscribe checkbox
+ subscribe += '-' + permalink;
+ }
+
+ // Validate form fields
+ return this.emailValidator (form, subscribe, type, permalink, isReply, isEdit);
+};
diff --git a/bootstrap/comments/images/avatar.png b/bootstrap/comments/images/avatar.png
new file mode 100644
index 0000000..e3e979a
--- /dev/null
+++ b/bootstrap/comments/images/avatar.png
Binary files differ
diff --git a/bootstrap/comments/images/avatar.svg b/bootstrap/comments/images/avatar.svg
new file mode 100644
index 0000000..5325e7b
--- /dev/null
+++ b/bootstrap/comments/images/avatar.svg
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="45" width="45" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<metadata>
+<rdf:RDF>
+<cc:Work rdf:about="">
+<dc:format>image/svg+xml</dc:format>
+<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
+<dc:title/>
+</cc:Work>
+</rdf:RDF>
+</metadata>
+<path d="m4.5e-8 22.5v-22.5h22.5 22.5v22.5 22.5h-22.5-22.5z" fill-opacity=".19608"/>
+<path d="m22.066 12.715c-3.6302 0.2471-5.5178 1.9584-6.9805 4.3164-1.3618 2.1954-1.416 5.3582-0.36719 7.7422 0.78622 1.7949 2.5366 2.7908 3.9219 4.0547 1.4968 1.6561-0.18996 4.034-2.1445 4.1992-1.9331 0.16633-3.796 0.96137-5.1875 2.3223-0.69361 0.8852-1.627 1.8393-1.627 4.2734v7.377h25.637v-7.377c0-2.4342-0.93333-3.3882-1.627-4.2734-1.3915-1.3609-3.2563-2.1559-5.1895-2.3223-1.9546-0.16517-3.6395-2.5432-2.1426-4.1992 1.3853-1.2639 3.1356-2.2598 3.9219-4.0547 1.0488-2.3839 0.9927-5.5468-0.36914-7.7422-1.4627-2.358-3.3482-4.0693-6.9785-4.3164z" fill="#fafafa"/>
+</svg>
diff --git a/bootstrap/comments/images/deleted-icon.png b/bootstrap/comments/images/deleted-icon.png
new file mode 100644
index 0000000..fea9b06
--- /dev/null
+++ b/bootstrap/comments/images/deleted-icon.png
Binary files differ
diff --git a/bootstrap/comments/images/deleted-icon.svg b/bootstrap/comments/images/deleted-icon.svg
new file mode 100644
index 0000000..6b8a8d4
--- /dev/null
+++ b/bootstrap/comments/images/deleted-icon.svg
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="45" width="45" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<defs>
+<linearGradient id="linearGradient4934" y2="145.22" gradientUnits="userSpaceOnUse" x2="149.57" gradientTransform="matrix(0 -.059560 15.735 0 -2262.5 30.916)" y1="145.22" x1="140.43">
+<stop stop-color="#e10000" offset="0"/>
+<stop stop-color="#e10000" stop-opacity="0" offset="1"/>
+</linearGradient>
+</defs>
+<metadata>
+<rdf:RDF>
+<cc:Work rdf:about="">
+<dc:format>image/svg+xml</dc:format>
+<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
+<dc:title/>
+</cc:Work>
+</rdf:RDF>
+</metadata>
+<rect fill-opacity=".098039" height="45" width="45" y="-8.1766e-7" x="-4.7401e-7" fill="#f00"/>
+<g>
+<path d="m10.92 9.2905c-1.5743 0.1783-2.6456 2.0965-1.5506 3.2225l10.322 9.9922-10.322 9.9902c-1.6546 2.0253 1.3427 4.0545 2.8086 2.7188l10.322-9.9922 10.322 9.9922c1.9706 1.6587 4.2165-1.271 2.8086-2.7188l-10.322-9.99 10.322-9.992c1.654-2.025-1.343-4.0515-2.808-2.7167l-10.323 9.9907-10.322-9.9907c-0.348-0.3371-0.803-0.5058-1.258-0.5058z" fill="#f00"/>
+<path d="m19.691 22.505-10.322 9.992c-1.6536 2.0234 1.3447 4.053 2.8088 2.7188l10.323-9.9921 10.323 9.9921c1.9689 1.6578 4.2151-1.2729 2.8088-2.7188l-10.323-9.9922z" fill="#e10000"/>
+<path d="m19.18 22.008 0.51132 0.49718h5.618l0.49959-0.48937z" fill="url(#linearGradient4934)"/>
+</g>
+</svg>
diff --git a/bootstrap/comments/images/error-icon.png b/bootstrap/comments/images/error-icon.png
new file mode 100644
index 0000000..20b0117
--- /dev/null
+++ b/bootstrap/comments/images/error-icon.png
Binary files differ
diff --git a/bootstrap/comments/images/error-icon.svg b/bootstrap/comments/images/error-icon.svg
new file mode 100644
index 0000000..ba1cc04
--- /dev/null
+++ b/bootstrap/comments/images/error-icon.svg
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="45" width="45" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<metadata>
+<rdf:RDF>
+<cc:Work rdf:about="">
+<dc:format>image/svg+xml</dc:format>
+<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
+<dc:title/>
+</cc:Work>
+</rdf:RDF>
+</metadata>
+<path d="m4.5e-8 22.5v-22.5h45v45h-45z" fill-opacity=".19608"/>
+<path d="m20.026 9.293 1 17 3 0.24414 1-16.244zm2.4648 21.148c-0.37333 0-0.72216 0.06898-1.0488 0.20898-0.32667 0.14-0.60651 0.32721-0.83984 0.56055-0.23333 0.23333-0.42055 0.51318-0.56055 0.83984-0.12833 0.32667-0.19336 0.66958-0.19336 1.0312 0 0.37333 0.06503 0.72412 0.19336 1.0508 0.14 0.315 0.32722 0.58893 0.56055 0.82226s0.51318 0.41464 0.83984 0.54297c0.32667 0.14 0.6755 0.20898 1.0488 0.20898 0.37333 0.000001 0.7182-0.06898 1.0332-0.20898 0.32667-0.12833 0.60651-0.30964 0.83984-0.54297 0.245-0.23333 0.43812-0.50726 0.57812-0.82226 0.14-0.32667 0.20898-0.67745 0.20898-1.0508 0-0.36167-0.06898-0.70458-0.20898-1.0312-0.14-0.32667-0.33312-0.60651-0.57812-0.83984-0.23333-0.23333-0.51318-0.42055-0.83984-0.56055-0.315-0.14-0.65987-0.20898-1.0332-0.20898z" fill="#fff"/>
+</svg>
diff --git a/bootstrap/comments/images/favicon.ico b/bootstrap/comments/images/favicon.ico
new file mode 100644
index 0000000..4e5fa6d
--- /dev/null
+++ b/bootstrap/comments/images/favicon.ico
Binary files differ
diff --git a/bootstrap/comments/images/favicon.png b/bootstrap/comments/images/favicon.png
new file mode 100644
index 0000000..3bdf170
--- /dev/null
+++ b/bootstrap/comments/images/favicon.png
Binary files differ
diff --git a/bootstrap/comments/images/first-comment.png b/bootstrap/comments/images/first-comment.png
new file mode 100644
index 0000000..e2d6895
--- /dev/null
+++ b/bootstrap/comments/images/first-comment.png
Binary files differ
diff --git a/bootstrap/comments/images/first-comment.svg b/bootstrap/comments/images/first-comment.svg
new file mode 100644
index 0000000..676e97d
--- /dev/null
+++ b/bootstrap/comments/images/first-comment.svg
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="45" width="45" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<metadata>
+<rdf:RDF>
+<cc:Work rdf:about="">
+<dc:format>image/svg+xml</dc:format>
+<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
+<dc:title/>
+</cc:Work>
+</rdf:RDF>
+</metadata>
+<path d="m4.5e-8 22.5v-22.5h22.5 22.5v22.5 22.5h-22.5-22.5z" fill-opacity=".19608"/>
+<path d="m27 31.895v-4.8h4.8c5.2184 0 7.2-1.2385 7.2-4.5s-1.9816-4.5-7.2-4.5h-4.8v-4.8c0-5.2184-1.2385-7.2-4.5-7.2s-4.5 1.9816-4.5 7.2v4.8h-4.8c-5.2184 0-7.2 1.2385-7.2 4.5s1.9816 4.5 7.2 4.5h4.8v4.8c0 5.2184 1.2862 7.0093 4.5477 7.0093 3.395-0.01844 4.4294-2.102 4.4523-7.0093z" fill="#fff"/>
+</svg>
diff --git a/bootstrap/comments/images/hashover-logo.png b/bootstrap/comments/images/hashover-logo.png
new file mode 100644
index 0000000..e241423
--- /dev/null
+++ b/bootstrap/comments/images/hashover-logo.png
Binary files differ
diff --git a/bootstrap/comments/images/hashover-logo.svg b/bootstrap/comments/images/hashover-logo.svg
new file mode 100644
index 0000000..dce9692
--- /dev/null
+++ b/bootstrap/comments/images/hashover-logo.svg
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" enable-background="" xml:space="preserve" height="512" viewBox="0 0 512 512" width="512" version="1.1" y="0px" x="0px" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/"><metadata><rdf:RDF><cc:Work rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/><dc:title/></cc:Work></rdf:RDF></metadata><g fill="#41a7fa"><path d="m288.22 252-95.907 132h116.62l17.422-23.996h-86.738l21.806-30h39.584l17.42-24h-39.57l21.795-30h138.23c8.2843 0 15 6.7157 15 15s-6.7157 15-15 15h-58.48l-3.3808 4.6636-0.005 73.34h24v-54l23.75-0.008 29 54h27.25l-30-55.996s0.04-0.10299 0.041-0.10547c15.989-5.2291 26.812-20.135 26.834-36.957-0.03-20.48-15.91-37.57-36.35-38.94z"/><path d="m481 46c0-17.269-11.177-27-29-27h-392c-19.42 0-29 9.74-29 27v168c0 17.612 9.7167 26 29 26h109v121l87.912-121h195.09c17.586 0 29.063-8.4318 29-26zm-394 149h-24l0.0015-131h23.999l0.000012 51.945h45v-51.945l24-0.00004-0.00074 82.965-24 33.04v-40h-45zm362 0h-24.002l0.00074-55h-45l-0.00073 55h-23.998l0.00073-106.89-52-0.22592c-11.977 0-18.734 2.3954-19.464 10.953-0.72997 8.5578 6.8814 12.022 22.654 18.423 21.599 8.5353 31.74 20.572 31.74 39.001 0 21.918-15.401 38.735-48.93 38.735h-47.669l-24.331 33.625v-33.625h-20l17.433-24h2.5663v-43.5l-57.031 78.5h-29.969l111-152.78 0.22485 117.78h52.676c12.677-0.22593 19.047-4.1336 19.745-12.605 0.69761-8.471-5.8048-12.812-20.501-18.438-20.296-7.5494-33.545-19.595-33.545-38.601 0-22.321 13.498-37.356 42.324-37.356h77.078l-0.00074 52h45l0.00074-52h23.998z"/><path d="m85.982 252c-30.982 0-54.982-0.0123-54.982 26.571v80.857c0 24.575 24 24.571 54.982 24.571 30.018 0 54.018 0.003 54.018-26.561v-78.878c0-26.58-24-26.56-54.018-26.56zm30.018 96c-0.0731 12-12 11.997-30.018 11.997s-30.982 0.003-30.982-11.997v-60c0-12 13-12.094 31-12 18 0.09 30 0 30 12z"/><path d="m365 327.18-50 68.816h-131.4l-43.595 60v-70l-12 10h-86l-11-8-0.000046 14.918 7 5.082h90l-0.10547 86 61.518-86h132l31.588-43.469v52.194l12-8.7255h109l7-5.0853v-6.9147h-116z"/></g></svg>
diff --git a/bootstrap/comments/images/inputs-and-buttons.png b/bootstrap/comments/images/inputs-and-buttons.png
new file mode 100644
index 0000000..537d19b
--- /dev/null
+++ b/bootstrap/comments/images/inputs-and-buttons.png
Binary files differ
diff --git a/bootstrap/comments/images/inputs-and-buttons.svg b/bootstrap/comments/images/inputs-and-buttons.svg
new file mode 100644
index 0000000..39513da
--- /dev/null
+++ b/bootstrap/comments/images/inputs-and-buttons.svg
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="476" width="28" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<metadata>
+<rdf:RDF>
+<cc:Work rdf:about="">
+<dc:format>image/svg+xml</dc:format>
+<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
+<dc:title/>
+</cc:Work>
+</rdf:RDF>
+</metadata>
+<path fill-opacity=".50196" stroke-width=".88872" d="m13.995 90c-3.0776-0.04332-6.0609 1.8815-7.2937 4.7012-1.3372 2.888-0.73018 6.5431 1.4991 8.8218 2.1232 2.2856 5.6359 3.0871 8.55 1.991 2.9582-1.0404 5.1175-3.9671 5.2322-7.1022 0.19702-3.0471-1.5366-6.1034-4.2546-7.4946-1.145-0.60472-2.4388-0.91921-3.7331-0.91709zm0.04036 1.0272c-0.36291-0.05329-1.3329 0.29735-1.2624 0.18971 0.37394-0.23995 0.84359-0.14838 1.2624-0.18971zm-1.5742 0.14272c0.37524 0.11145-0.97714 0.37021-0.26269 0.32098-0.89644 0.57421 0.75694-0.1043 1.103-0.11207 0.53066-0.07369 1.6114 0.04149 1.7137 0.30382-0.26226 0.35478 1.0874 0.2086 1.0149 0.50222 0.38172-0.05752 0.97338 0.22239 0.21822 0.04794-0.41653 0.21263 0.54107 0.04374 0.3698 0.45708-0.000476 0.28593 0.72275 0.94495 0.2027 0.80089 0.0031-0.61237-1.0679 0.14947-0.46451 0.58245 0.12703 0.35192 0.21681 1.205 0.59662 0.73051 0.60847 0.55254 0.25534-0.365-0.0979-0.5581-0.39074-0.50124 0.09998-0.37366 0.18376 0.04076 0.35146 0.04613 0.29566-0.39892 0.41851 0.10791 0.38807 0.46539-0.09232 1.0955-0.45697 0.66911 0.72136 0.46252-0.64896 1.3248 0.2565 1.5789-0.07173 0.34725-0.78509 0.50255-0.34672 1.0012 0.78177 0.51639 0.67867-0.52139 0.78625-0.96896 0.11202-0.61717 1.5647 0.049 0.68757-0.40985-0.78186-0.36416 0.57601-0.1912 0.72628 0.18057-0.03212-0.4461-0.1457-0.90936-0.37548-1.4062 0.47016 0.09853-0.20013-0.69145 0.31783-0.2103 0.76462-0.09409-0.51856 1.5123 0.40367 1.1599 0.40052-0.81158 0.73906 0.70519 0.04873 0.78474-0.01492 0.44418-0.81125 0.96624-0.95537 0.32976-0.66935 0.26879-1.3243 0.83328-1.4185 1.5695-0.38718 0.70301-0.61528 1.7102-0.15262 2.3368 0.66946 0.79842 1.4574 0.12307 2.0726-0.31983 0.26566-0.11762 0.48396 0.0251 0.42187 0.37299-0.1187 0.84768-0.12209 1.521-0.66692 2.0277-1.2335 1.1522-2.6644 1.7216-4.1103 1.8776-1.3266 0.0915-1.5695-0.0107-1.2289-0.33639 0.46199-0.44172 0.7663-0.60431 0.91886-1.0567 0.22495-0.65901-0.88604-0.70724-1.294-0.93464-0.395-0.0118-0.17938 0.15072-0.19588-0.0679-0.46084 0.14639 0.08488-0.55163-0.52489-0.6217-0.7073-0.25328-1.027-0.86782-1.7023-1.0853-0.32811-0.12322-0.26077 0.20455-0.33566-0.16363-0.42187-0.0119-0.95826 0.14476-1.1578-0.4458 0.41904-0.4817-0.66544-0.85802-0.06233-1.3231-0.26906-0.0854-0.91643 0.01-0.71294-0.62498-0.0085-0.60958 0.59019-0.85613 0.92509-0.46082 0.62119-0.0276 0.34189 1.5273 0.62635 0.49514 0.02022-0.5297 0.81708-0.33188 0.80156-0.95736-0.15025 0.48128 0.29052-0.1749 0.52524-0.13076 0.29978-0.23265 0.83155-0.35617 0.58213-0.055 0.52935 0.0484 0.03232-0.43675 0.1057-0.6221-0.86592 0.002 0.47982-0.0706 0.78053-0.1536 0.17816-0.26949-0.277-0.75-0.2199-1.0229-0.48733 0.28933-0.1294-0.89158-0.74031-0.30693-0.10479 0.33983-0.35012 0.63231-0.63245 0.82622 0.1116-0.5775-0.45382-1.4134 0.42-1.5739 0.2106 0.03159 1.121-0.20598 0.47891-0.29564 0.11946-0.43295 0.14079-0.35063-0.26321-0.16474-0.48804-0.25121-0.26795-0.91969-0.17999-1.3515 0.36389-0.27391 0.41872-0.6561 0.02257-0.57959-0.21434-0.087-0.97125 0.13246-1.0754 0.30155 0.47735 0.37584-0.55606 1.0888-0.8755 1.4819-0.26538 0.68696-1.1063 0.75428-1.3305 1.505-0.21017 0.57894-0.56391 1.5544-0.48306 1.9576 0.04433-0.42286 0.38359-1.5772 0.20056-0.55712-0.10765 0.81175-0.31053 1.733 0.2946 2.4099 0.51972 0.63149 0.65055 1.452 1.3346 1.9074-0.19327 0.51756-0.53711 1.1094-0.0044 1.6444 0.30287 0.57791 0.61052 0.94843 0.91687 1.524-2.0466-1.15-3.2769-3.11-3.5605-5.243-0.2996-2.538 0.7417-4.663 2.1048-5.948 0.832-0.774 1.8817-1.444 3.3077-1.769zm1.0722 0.31424c0.24777 0.12602 0.41399 0.04677 0 0zm2.0932 0.85752c0.21192 0.34572 0.67387 0.3228 0.10626 0.05467zm-3.6397 0.53292c-0.37478 0.01468 0.02864 0.36094-0.39823 0.42638 0.1689 0.89191 0.70343-0.2191 0.39823-0.42638zm0.37664 0.08332c-0.35441 0.05081 0.26466 0.36021 0 0zm1.1594 0.0486c-0.36065-0.03906-0.32742 0.33243-0.72725 0.41314 0.18442 0.10838 1.0107-0.14887 0.72725-0.41314zm1.6055 0.03646c-0.21077 0.2941 0.58485 0.16227 0 0zm-0.98414 0.04686c-0.44229 0.15994-1.2433 0.28259-0.66477 0.83322 0.21545 0.48897-0.37954 1.4478 0.36421 1.5366-0.02702-0.54142 0.80558-0.81817 0.83956-1.0914-0.46721-0.16679 0.46071-0.03001-0.18664-0.33766 0.40001-0.04968-0.10369-0.83664-0.08691-0.80244-0.15626 0.08532-0.15529-0.10989-0.26544-0.1384zm-1.6142 0.1111c-0.19744 0.03188 0.12519 0.17157 0 0zm0.16489 0.07985c-0.38182 0.26251 0.42039 0.60474 0 0zm-0.14406 0.34718c-0.66742 0.13866 0.55746 0.86213-0.31001 0.99127 0.1188 0.54778 0.448 0.4855 0.50645 0.01727 0.39333 0.23958-0.04945-1.0496-0.25517-0.89532l0.05872-0.11321zm-3.0652 0.27774c-0.26011 0.2084-0.14622 0.53094 0 0zm2.4265 0.44265c-0.46789 0.201 0.38779 0.42682 0 0zm3.3291 0.3194c-1.0005 0.27256 0.5285 0.54798 0 0zm5.0591 0.40014c0.19214 0.21212 0.16945 0.90099-0.18225 0.39922 0.09822-0.11062 0.21437-0.23567 0.18225-0.39922zm-4.0524 0.19527c-0.04142 0.41745 0.57026 0.51881 0.43219 0.95473 0.69233-0.37566-0.21174-0.65002-0.43219-0.95473zm0.06942 0.44612c-0.58138 0.47521 0.5337 0.5916 0 0zm-3.4974 0.74122c-0.49473 0.16483-0.23051 0.68093 0.12229 0.4899-0.06122-0.14053-0.30291-0.33467-0.12229-0.4899zm7.2986 0.0278c0.43311 0.10466 0.69418 0.58513 0.79371 0.60186 0.13955 0.63439-0.88172-0.57955-0.79371-0.60186zm-10.606 2.474c0.07361 0.10889 0.93988 1.1392 0.43205 0.31644-0.1127-0.139-0.245-0.286-0.432-0.316zm0.87132 0.81586c-0.37225 0.26413 0.63734 0.43782 0.16112 0.0525z"/>
+<path d="m17.594 147c-0.89424 0.89432-1.7885 1.7886-2.6828 2.6831 1.1353 1.1352 2.2705 2.2703 3.406 3.4056l2.6826-2.6825-3.406-3.406zm-3.8247 3.8248-6.7695 6.7695v3.4058h3.4057c2.2568-2.2565 4.5134-4.5128 6.7699-6.7695-1.1353-1.1351-2.2706-2.2706-3.4059-3.4057zm-5.6342 6.7695h1.1355v1.1352h1.1351v1.1353c-0.75657 0.0001-1.5134 0.0001-2.2702 0.00033-0.0000852-0.75702-0.0000426-1.5138-0.0002554-2.2707z"/>
+<path fill-opacity=".50196" stroke-width="1.0168" d="m14.322 62c-1.2354 0.000013-2.3251 0.19845-3.3141 0.56667-0.98901 0.36823-1.869 0.91835-2.6395 1.6667-0.7597 0.7424-1.3731 1.5868-1.7733 2.5666-0.40022 0.97997-0.58923 2.0597-0.59677 3.2-0.00151 1.1403 0.1857 2.1808 0.58204 3.1667 0.39518 0.97996 0.9715 1.8636 1.7231 2.6 0.76598 0.74833 1.6579 1.2925 2.6283 1.6667 0.97662 0.373 2.0367 0.566 3.205 0.566 0.86771-0.000003 1.7253-0.11723 2.5628-0.36667 0.83734-0.24945 1.643-0.63445 2.4139-1.1333l-0.524-0.832c-0.62174 0.39792-1.2754 0.69807-1.9653 0.9-0.6839 0.20193-1.3795 0.3-2.1148 0.3-0.98222-0.000002-1.8959-0.17105-2.7375-0.53333-0.83564-0.36228-1.5769-0.89325-2.2203-1.6-0.53227-0.59985-0.94682-1.3197-1.2374-2.1333-0.29174-0.8196-0.44108-1.6795-0.43906-2.6 0.00177-1.17 0.22743-2.2593 0.68904-3.2333 0.46767-0.974 1.1433-1.7705 2.0033-2.4 0.56518-0.40979 1.1774-0.72545 1.8601-0.93333 0.68272-0.20786 1.4301-0.29999 2.2134-0.3 0.8436 0.000012 1.6284 0.11724 2.3759 0.36667 0.74743 0.24945 1.4042 0.60342 1.952 1.0667 0.68317 0.57017 1.1935 1.2051 1.5214 1.8999 0.32775 0.69489 0.48984 1.4841 0.48604 2.3333-0.0018 1.0453-0.23963 1.9052-0.71328 2.6-0.46765 0.69488-1.1349 1.1684-1.9998 1.4l-0.0042-6.6334h-1.9755l-0.01035 1.1333c-0.26186-0.422-0.58042-0.742-1.0098-0.967-0.42955-0.226-0.94495-0.367-1.4867-0.367-1.1509 0.000008-2.0537 0.38732-2.7166 1.0999-0.65816 0.70676-0.9741 1.6802-0.97787 2.9333-0.0037 1.2531 0.30812 2.2207 0.96473 2.9333 0.65662 0.71269 1.5585 1.0667 2.7094 1.0667 0.54232 0.000001 1.0236-0.10765 1.4498-0.33333 0.4322-0.22569 0.79693-0.53905 1.0634-0.96667l-0.01092 1.3001h0.29632c1.7113 0.000001 3.0326-0.45204 3.9817-1.3666 0.95517-0.91462 1.4545-2.1941 1.4616-3.8334 0.0042-1.0334-0.21082-1.9566-0.6096-2.8-0.39277-0.84335-0.99321-1.6075-1.79-2.2667-0.69153-0.58203-1.4788-1.0149-2.3597-1.3001-0.87855-0.29227-1.8448-0.43413-2.9174-0.43413zm-0.06321 5.0999c0.6086 0.000008 1.091 0.26415 1.4593 0.73333 0.37316 0.46325 0.55542 1.0672 0.55358 1.8334l-0.0033 0.66667c-0.0067 0.77208-0.18537 1.4034-0.55937 1.8667-0.37518 0.45731-0.87523 0.66667-1.4838 0.66667-0.59656 0.000002-1.0663-0.24402-1.4329-0.76667-0.36047-0.5216-0.54356-1.2203-0.53935-2.0992 0.0041-0.87899 0.18334-1.5833 0.54698-2.1001 0.36851-0.52263 0.86136-0.79999 1.4579-0.8z"/>
+<path fill-opacity=".50196" d="m15.567 35.59c-1.6008 0.95346-2.3088 2.8232-1.8577 4.5327l-7.709 4.327 2.7008 4.549 1.8072-1.014-1.555-2.619 1.5575-0.8744 1.5548 2.6186 1.8056-1.0137-1.5548-2.6186 2.5392-1.4255c1.3217 1.2582 3.3932 1.5865 5.0595 0.65099 2.0081-1.1274 2.678-3.669 1.5145-5.6285-1.1634-1.9595-3.7611-2.6649-5.7693-1.5376-0.03138 0.01761-0.0623 0.03397-0.09305 0.05224zm1.2142 1.836c0.94053-0.52801 2.1399-0.21453 2.7063 0.73949 0.56645 0.95403 0.24675 2.1221-0.69378 2.6501-0.94053 0.52801-2.1393 0.21246-2.7058-0.74157-0.56645-0.95403-0.24732-2.12 0.69322-2.648z"/>
+<path d="m4.9999 293.56c-0.00495 2.2132 1.557 4.0272 3.2764 5.032 3.0785 1.7698 6.8612 1.8988 10.132 0.67592 1.5292 0.57714 3.0583 1.1543 4.5874 1.7314-0.71916-1.0678-1.4383-2.1356-2.1575-3.2033 1.5761-1.2351 2.6123-3.5039 1.9681-5.5783-0.69386-2.3114-2.7481-3.793-4.8076-4.5245-3.1669-1.1318-6.8014-0.89412-9.7485 0.82436-1.7182 1.0157-3.2499 2.8274-3.249 5.0423z"/>
+<path d="m14.289 259c-2.0967-0.0556-4.2072 0.43141-6.0488 1.5059-1.7179 1.016-3.2412 2.8449-3.2402 5.0605-0.00495 2.2139 1.5525 4.0223 3.2715 5.0274 3.078 1.7704 6.8533 1.8971 10.123 0.67382 1.5289 0.57732 3.0688 1.1512 4.5977 1.7285-0.71903-1.0681-1.441-2.135-2.1602-3.2031 1.5758-1.2355 2.6187-3.4972 1.9746-5.5723-0.69374-2.3121-2.7553-3.784-4.8145-4.5156-1.1874-0.42455-2.4452-0.67167-3.7031-0.70507zm-4.7891 4.9961a1.5 1.5 0 0 1 1.5 1.5 1.5 1.5 0 0 1 -1.5 1.5 1.5 1.5 0 0 1 -1.5 -1.5 1.5 1.5 0 0 1 1.5 -1.5zm4.5 0a1.5 1.5 0 0 1 1.5 1.5 1.5 1.5 0 0 1 -1.5 1.5 1.5 1.5 0 0 1 -1.5 -1.5 1.5 1.5 0 0 1 1.5 -1.5zm4.5 0a1.5 1.5 0 0 1 1.5 1.5 1.5 1.5 0 0 1 -1.5 1.5 1.5 1.5 0 0 1 -1.5 -1.5 1.5 1.5 0 0 1 1.5 -1.5z"/>
+<path d="m7.6602 175.73c-1.6158 1.2295-1.9186 3.4637-1.4759 5.3113 0.61078 2.5841 2.5871 4.5964 4.6784 6.1511 0.95092 0.67454 3.1234 1.8076 3.1372 1.8076 0.01391 0 2.1864-1.1331 3.1373-1.8076 2.0913-1.5547 4.0675-3.567 4.6784-6.1511 0.44256-1.8476 0.13985-4.0818-1.4759-5.3113-0.88805-0.72454-2.1052-0.84913-3.2024-0.63643-1.3185 0.32528-3.1372 2.3062-3.1372 2.3062s-1.8187-1.981-3.1373-2.3062c-1.1028-0.21229-2.325-0.082-3.2024 0.63643z"/>
+<path fill="#ff4040" d="m7.6602 203.73c-1.6158 1.2297-1.9186 3.4639-1.4759 5.3113 0.61076 2.5841 2.5871 4.5964 4.6784 6.1512 0.9509 0.67456 3.1234 1.8076 3.1372 1.8076 0.01388 0 2.1864-1.1331 3.1374-1.8076 2.0913-1.5548 4.0675-3.567 4.6784-6.1512 0.44256-1.8474 0.13986-4.0817-1.4759-5.3113-0.88804-0.72444-2.1052-0.84903-3.2024-0.63632-1.3185 0.32521-3.1372 2.3062-3.1372 2.3062s-1.8187-1.981-3.1374-2.3062c-1.1028-0.21236-2.325-0.0823-3.2024 0.63632z"/>
+<path d="m13.842 318.01c-0.05597 0.0131-0.10881 0.0249-0.16122 0.0571l-8.4104 5.1657c-0.27417 0.16812-0.34638 0.53645-0.18809 0.82766l0.88671 1.6267c0.15829 0.2912 0.50506 0.39644 0.77923 0.22832l7.2549-4.4522 7.2549 4.4522c0.27417 0.16812 0.62094 0.0629 0.77924-0.22832l0.88671-1.6267c0.15829-0.29121 0.05921-0.65954-0.21496-0.82766l-8.3835-5.1657c-0.06854-0.042-0.14129-0.0473-0.21496-0.0571-0.0816-0.0109-0.19147-0.0257-0.2687 0z"/>
+<path d="m14.158 353.99c0.05598-0.0131 0.10881-0.0249 0.16122-0.0571l8.4104-5.1657c0.27417-0.16813 0.34638-0.53645 0.1881-0.82766l-0.88671-1.6268c-0.15829-0.29119-0.50507-0.39644-0.77924-0.22831l-7.2549 4.4522-7.2549-4.4522c-0.27417-0.16813-0.62094-0.0629-0.77923 0.22831l-0.88671 1.6268c-0.15829 0.29121-0.059209 0.65953 0.21496 0.82766l8.3835 5.1657c0.06854 0.042 0.14129 0.0473 0.21496 0.0571 0.0816 0.0109 0.19147 0.0257 0.2687 0z"/>
+<path d="m13.842 374.01c-0.05597 0.0131-0.10881 0.0249-0.16122 0.0571l-8.4104 5.1657c-0.27417 0.16812-0.34638 0.53645-0.18809 0.82766l0.88671 1.6267c0.15829 0.2912 0.50506 0.39644 0.77923 0.22832l7.2549-4.4522 7.2549 4.4522c0.27417 0.16812 0.62094 0.0629 0.77924-0.22832l0.88671-1.6267c0.15829-0.29121 0.05921-0.65954-0.21496-0.82766l-8.3835-5.1657c-0.06854-0.042-0.14129-0.0473-0.21496-0.0571-0.0816-0.0109-0.19147-0.0257-0.2687 0z" fill="#0030ff"/>
+<path d="m14.158 409.99c0.05598-0.0131 0.10881-0.0249 0.16122-0.0571l8.4104-5.1657c0.27417-0.16813 0.34638-0.53645 0.1881-0.82766l-0.88671-1.6268c-0.15829-0.29119-0.50507-0.39644-0.77924-0.22831l-7.2549 4.4522-7.2549-4.4522c-0.27417-0.16813-0.62094-0.0629-0.77923 0.22831l-0.88671 1.6268c-0.15829 0.29121-0.059209 0.65953 0.21496 0.82766l8.3835 5.1657c0.06854 0.042 0.14129 0.0473 0.21496 0.0571 0.0816 0.0109 0.19147 0.0257 0.2687 0z" fill="#ff0030"/>
+<path fill-opacity=".50196" stroke-width=".99998" d="m13.797 5.9987c-1.6992 0.1204-2.5827 0.95418-3.2674 2.103-0.63742 1.0697-0.66279 2.6106-0.17187 3.7722 0.36801 0.87451 1.1873 1.3598 1.8357 1.9755 0.70061 0.80689-0.08892 1.9654-1.0038 2.0459-0.90483 0.08104-1.7768 0.4684-2.4281 1.1315-0.3246 0.43199-0.7615 0.89697-0.7615 2.0829v2.8889h12v-2.8891c0-1.186-0.43687-1.6508-0.76156-2.0821-0.65132-0.66306-1.5242-1.0504-2.4291-1.1315-0.9149-0.08048-1.7036-1.2391-1.0029-2.0459 0.64842-0.6158 1.4677-1.1011 1.8357-1.9755 0.49092-1.1615 0.46466-2.7025-0.17278-3.7722-0.68465-1.1489-1.5672-1.9826-3.2664-2.103z"/>
+<g transform="matrix(1.0212 0 0 1.0091 -.35479 -1.0715)">
+<path d="m16.34 122.36c-1.3812 0.81718-1.9921 2.4199-1.6029 3.8851l-6.6518 3.7098 2.3302 3.8989 1.5591-0.86953-1.3415-2.2445 1.3439-0.74948 1.3415 2.2445 1.5579-0.86888-1.3415-2.2445 2.1909-1.2219c1.1404 1.0785 2.9277 1.3599 4.3654 0.55799 1.7327-0.96631 2.3106-3.1448 1.3067-4.8244-1.0038-1.6795-3.2452-2.2842-4.978-1.3179-0.02708 0.0151-0.05377 0.0291-0.08028 0.0448zm1.0476 1.5737c0.81152-0.45258 1.8464-0.18388 2.3351 0.63385 0.48876 0.81774 0.2129 1.8189-0.59862 2.2715-0.81152 0.45258-1.8459 0.18211-2.3346-0.63563-0.48876-0.81774-0.21339-1.8171 0.59812-2.2697z"/>
+<path fill-opacity=".50196" d="m10.57 118c-1.2744 0.0903-1.9377 0.71644-2.4512 1.5781-0.47807 0.80227-0.4971 1.9569-0.12891 2.8281 0.27601 0.65592 0.89064 1.0205 1.377 1.4824 0.52546 0.60519-0.065806 1.4728-0.75195 1.5332-0.67863 0.0607-1.3318 0.35228-1.8203 0.8496-0.2435 0.32348-0.57227 0.67299-0.57227 1.5625v2.166h3.7227l4.9297-2.7598c-0.0272-0.10279 0.037-0.20281 0.01953-0.30664-0.10209-0.18541-0.13897-0.525-0.24219-0.66211-0.48849-0.49732-1.1436-0.78878-1.8223-0.8496-0.68617-0.0603-1.2774-0.92806-0.75195-1.5332 0.48632-0.46188 1.1009-0.8265 1.377-1.4824 0.36819-0.87115 0.34722-2.0259-0.13086-2.8281-0.513-0.86-1.175-1.49-2.449-1.58h-0.30469z"/>
+</g>
+<path fill="#c00" d="m13.507 429c-0.40504 0-0.53152 0.052-0.53152 0.3694v0.16016l0.3288 3.7019-3.2658-1.4802c-0.12658-0.0529-0.22813-0.0543-0.30408-0.0543-0.15189 0-0.22754 0.13205-0.3288 0.4495l-0.3288 0.9791c-0.050624 0.18522-0.076637 0.34497-0.076637 0.39787 0 0.13228 0.15229 0.23901 0.40544 0.2919l3.4438 0.81891-2.3288 2.8029c-0.12658 0.13227-0.178 0.23972-0.178 0.372 0 0.0794 0.10096 0.2107 0.3288 0.36942l0.78616 0.60967c0.20252 0.15872 0.32761 0.21182 0.37824 0.21182 0.10126 0 0.22754-0.10675 0.3288-0.29191l1.8492-3.2007 1.822 3.2007c0.10126 0.21162 0.20223 0.29191 0.3288 0.29191 0.07595 0 0.1782-0.0795 0.38072-0.21182l0.811-0.61c0.22784-0.15872 0.30161-0.29006 0.30161-0.36942 0-0.13228-0.07426-0.23973-0.17552-0.372l-2.3288-2.8029 3.4685-0.81891c0.25315-0.0529 0.37824-0.15902 0.37824-0.31772 0-0.0529-0.02601-0.1604-0.07664-0.37201l-0.3016-0.9791c-0.10126-0.31744-0.15407-0.4495-0.33127-0.4495-0.05063 0-0.17503 0.0271-0.30161 0.0801l-3.2658 1.4544 0.30408-3.7019v-0.16018c0-0.31742-0.12895-0.3694-0.53399-0.3694h-0.9864z"/>
+<path d="m7.6602 231.73c-1.6158 1.2295-1.9186 3.4637-1.4759 5.3113 0.61078 2.5841 2.5871 4.5964 4.6784 6.1511 0.95092 0.67454 3.1234 1.8076 3.1372 1.8076 0.01391 0 2.1864-1.1331 3.1373-1.8076 2.0913-1.5547 4.0675-3.567 4.6784-6.1511 0.44256-1.8476 0.13985-4.0818-1.4759-5.3113-0.88805-0.72454-2.1052-0.84913-3.2024-0.63643 0.379 2.8867-1.6266 4.5462-1.6266 4.5462l0.92851 2.0545-3.4335 3.9919 1.2414-3.9919-2.1497-2.0545s0.65709-3.5612-1.2347-4.5462c-1.1028-0.21229-2.325-0.082-3.2024 0.63643z"/>
+<path d="m11.84 462.47c1.182 0.67345 1.1944 0.669 1.8204 1.7392l6.7395-7.1775c0.7453-0.7598 0.58545-2.0224 0.58545-2.0224s-1.2793-0.15764-2.0604 0.6133zm-1.7278 1.4581c-0.80525 0.21566-1.2696 0.9362-1.5609 2.422-0.22661 1.156-0.56705 1.854-1.1216 2.3l-0.43003 0.34577 0.60335-0.008c1.7704-0.0242 4.2591-1.3716 4.7371-2.5648 0.5661-1.4132-0.7526-2.8898-2.228-2.4948z" stroke-width=".5"/>
+</svg>
diff --git a/bootstrap/comments/images/loading-bltr.gif b/bootstrap/comments/images/loading-bltr.gif
new file mode 100644
index 0000000..ceb7df7
--- /dev/null
+++ b/bootstrap/comments/images/loading-bltr.gif
Binary files differ
diff --git a/bootstrap/comments/images/loading-ctrl.gif b/bootstrap/comments/images/loading-ctrl.gif
new file mode 100644
index 0000000..ee4cf1f
--- /dev/null
+++ b/bootstrap/comments/images/loading-ctrl.gif
Binary files differ
diff --git a/bootstrap/comments/images/loading-ltr.gif b/bootstrap/comments/images/loading-ltr.gif
new file mode 100644
index 0000000..c51fdf3
--- /dev/null
+++ b/bootstrap/comments/images/loading-ltr.gif
Binary files differ
diff --git a/bootstrap/comments/images/loading.gif b/bootstrap/comments/images/loading.gif
new file mode 100644
index 0000000..fa272e5
--- /dev/null
+++ b/bootstrap/comments/images/loading.gif
Binary files differ
diff --git a/bootstrap/comments/images/pending-icon.png b/bootstrap/comments/images/pending-icon.png
new file mode 100644
index 0000000..433a1fc
--- /dev/null
+++ b/bootstrap/comments/images/pending-icon.png
Binary files differ
diff --git a/bootstrap/comments/images/pending-icon.svg b/bootstrap/comments/images/pending-icon.svg
new file mode 100644
index 0000000..cffd140
--- /dev/null
+++ b/bootstrap/comments/images/pending-icon.svg
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="45" width="45" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<metadata>
+<rdf:RDF>
+<cc:Work rdf:about="">
+<dc:format>image/svg+xml</dc:format>
+<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
+<dc:title/>
+</cc:Work>
+</rdf:RDF>
+</metadata>
+<path fill-opacity=".5" fill="#fcfc80" d="m0 22.5v-22.5h45v45h-45z"/>
+<path d="m22.5 13c-2.3929 0-4.3184 1.8533-4.3184 4.1566v3.5579h-1.9824c-0.666 0-1.2 0.504-1.2 1.129v9.0277c0 0.626 0.534 1.128 1.199 1.128h12.602c0.665 0 1.199-0.503 1.199-1.128v-9.0277c0-0.625-0.534-1.13-1.199-1.13h-1.9824v-3.5579c-0.001-2.303-1.926-4.156-4.319-4.156zm0.01563 1.9364c0.64952 0 1.2362 0.25856 1.6602 0.67867 0.42396 0.42012 0.68555 1.0011 0.68555 1.645v3.4545h-4.6895v-3.4545c0-0.64384 0.26194-1.2249 0.68555-1.645 0.42361-0.42012 1.0087-0.67867 1.6582-0.67867z" fill-opacity=".50196"/>
+</svg>
diff --git a/bootstrap/comments/images/place-holder.png b/bootstrap/comments/images/place-holder.png
new file mode 100644
index 0000000..37cb674
--- /dev/null
+++ b/bootstrap/comments/images/place-holder.png
Binary files differ
diff --git a/bootstrap/comments/images/place-holder.svg b/bootstrap/comments/images/place-holder.svg
new file mode 100644
index 0000000..5a9ede7
--- /dev/null
+++ b/bootstrap/comments/images/place-holder.svg
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="84" width="100" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/">
+<defs>
+<linearGradient id="linearGradient11689" y2="537.18" gradientUnits="userSpaceOnUse" x2="461.64" gradientTransform="matrix(.26914 .0035656 .0042065 .26063 -61.844 -87.106)" y1="424.05" x1="517.2">
+<stop stop-opacity=".12195" offset="0"/>
+<stop stop-color="#fff" stop-opacity="0" offset="1"/>
+</linearGradient>
+</defs>
+<metadata>
+<rdf:RDF>
+<cc:Work rdf:about="">
+<dc:format>image/svg+xml</dc:format>
+<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
+<dc:title/>
+</cc:Work>
+</rdf:RDF>
+</metadata>
+<g>
+<g stroke-linejoin="round" stroke="#000" stroke-linecap="round">
+<path d="m62.785 13.574c11.936 6.0188 23.872 12.038 35.808 18.056 2.1938 1.5448-0.17624 3.8461-0.81728 5.6153-5.572 11.048-11.143 22.096-16.714 33.144-1.545 2.194-3.846-0.176-5.615-0.817-10.87-5.481-21.739-10.962-32.608-16.443-2.1938-1.5448 0.17624-3.8461 0.81728-5.6153l16.713-33.143c0.41107-0.87274 1.5664-1.2535 2.4158-0.79613z" stroke-width=".99093" fill="#fff"/>
+<path d="m62.968 17.172c10.897 5.3071 21.947 10.317 32.691 15.921 1.3457 0.70185-0.29235 3.1484-0.98742 4.5321-4.156 8.236-8.108 16.595-12.391 24.754-1.666 0.527-3.293-1.182-4.919-1.704-9.768-4.816-19.652-9.452-29.347-14.381-0.69445-1.6004 1.0166-2.9929 1.4518-4.4754 4.1212-8.1663 8.0389-16.455 12.287-24.545 0.316-0.25913 0.81693-0.30188 1.2143-0.1012z" stroke-width="1.0149" fill="url(#linearGradient11689)"/>
+<path d="m4.8038 23.847c14.242-3.002 28.484-6.005 42.725-9.007 2.3658 0.05116 1.8185 2.9314 2.5198 4.4274 1.3912 5.8179 3.0644 11.603 4.2775 17.442-0.89612 1.6361-3.4266 1.2455-5.1056 1.8615-12.888 2.717-25.775 5.434-38.663 8.151-2.3656-0.051-1.8184-2.931-2.5197-4.427-1.3911-5.818-3.0643-11.603-4.2775-17.442 0.086165-0.4864 0.51563-0.90198 1.043-1.0051z" stroke-width=".36706" fill="#00e2ff"/>
+</g>
+<g stroke-linejoin="round" stroke-linecap="round">
+<path d="m54.047 35.243c-0.58083 0.50605-5.5037-0.7438-7.9754-0.06791-3.3787-0.41056-6.6038 0.90416-9.8849 1.1789-5.0601 0.90499-9.6462 3.0849-14.448 4.7622-2.9991 1.2843-9.1396 3.1276-4.5747 6.5675 3.6398 3.0701 8.3754 5.0118 13.403 4.5247 4.4315-0.1338 21.31-6.7395 25.647-7.3982-0.75197-3.9676-0.45332-5.8221-2.1673-9.5671z" stroke="#002a00" stroke-width=".36706" fill="#004800"/>
+<path d="m8.1976 43.725c0.67522-0.53725 6.3177-0.50762 9.2724-0.82202 3.8345 0.34283 8.0266-0.90641 11.499-0.38446 2.8876 0.42153 6.9854 0.10121 8.5189 2.6835 2.8157 4.2324-5.0925 4.706-8.2953 5.6808-6.464 1.256-12.85 3.014-19.393 3.655-0.1927 0.412-0.3685-0.69-1.0179-2.661-0.267-2.803-0.646-4.908-0.8149-7.502 0.0768-0.217 0.1536-0.433 0.2304-0.65z" stroke="#003b00" stroke-width=".36706" fill="#006a00"/>
+<path d="m11.201 52.47c3.9486-1.8366 7.8373-3.8366 12.053-5.1168 4.9708-1.9028 10.522-1.3235 15.641-2.6707 4.5826-2.1672 9.8884-1.9153 14.84-2.7617 2.1437-0.30734 4.9672 0.39386 5.3923 2.6414 0.13243 2.45 0.40569 4.8996 0.75046 7.3411 0.34438 2.3944-0.39321 5.2435-3.0278 6.3074-5.7509 3.2734-12.704 3.5547-19.079 5.1872-5.7295 0.80562-11.459 1.7073-17.256 1.9858-2.8662 0.58589-6.3405 1.4633-8.902-0.33605-1.8444-1.686-1.475-4.366-2.8201-6.348-0.9062-1.699-1.6952-4.653 0.9675-5.383 0.6906-0.121 1.1166-0.269 1.4416-0.847z" stroke="#004e00" stroke-width=".39328" fill="#090"/>
+</g>
+<path opacity=".65714" d="m4.7192 23.698c14.556-3.352 29.111-6.704 43.667-10.056 2.1674 0.05205 1.6655 2.9354 2.3084 4.433l8.8497 38.428c-0.05206 2.1674-2.9354 1.6655-4.433 2.3084-13.392 3.084-26.783 6.168-40.175 9.252-2.167-0.052-1.665-2.935-2.308-4.433-2.9501-12.809-5.9-25.619-8.8499-38.428-0.1686-0.651 0.2815-1.371 0.9411-1.504z" fill="#fff"/>
+<path stroke-linejoin="round" d="m49.318 10.824c-1.2417 0.0052-2.7141 0.61808-3.8581 0.78416l-43.231 9.959c-2.8223 0.96506-1.1124 4.0361-0.81553 6.0694l10.116 43.945c0.97054 2.8202 4.0418 1.1053 6.0773 0.80769l43.239-9.951c2.8202-0.97055 1.1131-4.0418 0.81553-6.0773l-10.125-43.946c-0.42572-1.2311-1.2534-1.5959-2.2192-1.5918zm-1.7565 3.9208c0.85033-0.03456 1.563 0.21099 1.8898 1.0978 2.3992 10.606 4.7994 21.207 7.1986 31.813 0.18278 1.4796 1.5715 3.6752-0.94884 4.4776-12.816 2.9-25.632 5.7963-38.447 8.6964-1.8033 0.28435-4.572 1.6397-5.3166-0.39208-2.4005-10.607-4.8007-21.215-7.1999-31.822-0.1822-1.478-1.5732-3.671 0.9488-4.469 12.816-2.9 25.632-5.797 38.447-8.697 1.0135-0.15895 2.3335-0.66131 3.4268-0.70575z" stroke="#000" stroke-linecap="round" stroke-width="1.2582" fill="#fff"/>
+</g>
+</svg>
diff --git a/bootstrap/comments/images/white-noise.png b/bootstrap/comments/images/white-noise.png
new file mode 100644
index 0000000..f44ce16
--- /dev/null
+++ b/bootstrap/comments/images/white-noise.png
Binary files differ
diff --git a/bootstrap/comments/themes/1.0-ported/comments.css b/bootstrap/comments/themes/1.0-ported/comments.css
new file mode 100644
index 0000000..e8d6099
--- /dev/null
+++ b/bootstrap/comments/themes/1.0-ported/comments.css
@@ -0,0 +1,801 @@
+/*-----------------------------------------------------------------------------
+
+Copyright (C) 2016-2018 Jacob Barkdull
+This file is part of HashOver.
+
+I, Jacob Barkdull, hereby release this work into the public domain.
+This applies worldwide. If this is not legally possible, I grant any
+entity the right to use this work for any purpose, without any
+conditions, unless such conditions are required by law.
+
+-----------------------------------------------------------------------------*/
+
+#hashover {
+ text-indent: initial;
+}
+
+#hashover #hashover-requiredFields,
+#hashover .hashover-hidden {
+ display: none ! important;
+}
+
+#hashover,
+#hashover-top-comments,
+#hashover-sort-section,
+#hashover .hashover-inputs {
+ clear: both;
+}
+
+#hashover #hashover-form-section,
+#hashover #hashover-popular-section,
+#hashover #hashover-comments-section {
+ margin-bottom: 36px;
+}
+
+#hashover .hashover-dashed-title,
+#hashover .hashover-title,
+#hashover #hashover-count,
+#hashover .hashover-comment-name {
+ font-size: 16px;
+ display: inline-block;
+ padding-top: 2px;
+ font-weight: bold;
+}
+
+#hashover .hashover-dashed-title {
+ display: inline-block;
+ margin-bottom: 16px;
+ width: 100%;
+}
+
+#hashover #hashover-form {
+ position: relative;
+ overflow: inherit;
+}
+
+#hashover .hashover-form-footer {
+ overflow: hidden;
+}
+
+#hashover form,
+#hashover .hashover-comment form {
+ display: block;
+}
+
+#hashover .hashover-inputs {
+ display: table;
+ width: 100%;
+}
+
+#hashover .hashover-message {
+ display: block;
+ width: 100%;
+ max-height: 0px;
+ padding: 0px 12px;
+ text-align: center;
+ overflow: hidden;
+}
+
+#hashover .hashover-message.hashover-message-open {
+ max-height: 85px;
+ padding: 12px;
+ margin-bottom: 12px;
+ color: #0000CC;
+ background-color: rgba(225, 225, 255, 0.25);
+ -moz-box-shadow: 0px 0px 0px 1px #00AACC inset;
+ -webkit-box-shadow: 0px 0px 0px 1px #00AACC inset;
+ box-shadow: 0px 0px 0px 1px #00AACC inset;
+}
+
+#hashover .hashover-message.hashover-message-error {
+ color: #CC0000;
+ background-color: rgba(255, 225, 225, 0.25);
+ -moz-box-shadow: 0px 0px 0px 1px #CC0000 inset;
+ -webkit-box-shadow: 0px 0px 0px 1px #CC0000 inset;
+ box-shadow: 0px 0px 0px 1px #CC0000 inset;
+}
+
+#hashover .hashover-comment .hashover-message.hashover-message-open {
+ margin-bottom: 0px;
+ margin-top: 12px;
+}
+
+#hashover .hashover-inputs > * {
+ display: table-cell;
+ vertical-align: middle;
+ padding: 2px 3px 6px 3px;
+ line-height: 16px;
+}
+
+#hashover .hashover-comment,
+#hashover .hashover-reply-form,
+#hashover .hashover-comment .hashover-avatar,
+#hashover .hashover-comment .hashover-comment-name,
+#hashover .hashover-avatar-image {
+ position: relative;
+}
+
+#hashover .hashover-avatar-image {
+ position: absolute;
+ left: 0px;
+ top: 0px;
+ width: 65px;
+}
+
+#hashover #hashover-form .hashover-avatar-image {
+ left: -66px;
+ top: -1px;
+}
+
+#hashover .hashover-comment .hashover-avatar-image {
+ width: 52px;
+}
+
+#hashover .hashover-avatar-image * {
+ vertical-align: middle;
+}
+
+#hashover .hashover-login-input,
+#hashover .hashover-login-input input {
+ width: 36px;
+}
+
+#hashover form input,
+#hashover form textarea,
+#hashover form .hashover-inputs input,
+#hashover .hashover-submit {
+ padding: 2px;
+ background-color: #FAFAFA;
+ font-size: 13px;
+ font-family: "Arial","Helvetica","FreeSans",sans-serif;
+ color: #222222;
+ border: 1px solid #808080;
+ outline-offset: none;
+ outline: -webkit-focus-ring-color none;
+}
+
+#hashover form input,
+#hashover form textarea,
+#hashover .hashover-submit,
+#hashover form .hashover-inputs input,
+#hashover .hashover-comment label {
+ transition: border 150ms linear 0ms, background-color 150ms linear 0ms, box-shadow 150ms linear 0ms;
+ -moz-transition: border 150ms linear 0ms, background-color 150ms linear 0ms, box-shadow 150ms linear 0ms;
+ -webkit-transition: border 150ms linear 0ms, background-color 150ms linear 0ms, box-shadow 150ms linear 0ms;
+ -o-transition: border 150ms linear 0ms, background-color 150ms linear 0ms, box-shadow 150ms linear 0ms;
+}
+
+#hashover form input,
+#hashover form textarea,
+#hashover .hashover-title,
+#hashover .hashover-inputs,
+#hashover .hashover-inputs input,
+#hashover .hashover-submit,
+#hashover .hashover-inputs > *,
+#hashover .hashover-comment pre,
+#hashover .hashover-comment code,
+#hashover .hashover-comment label,
+#hashover .hashover-message,
+#hashover #hashover-more-link {
+ box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+ -o-box-sizing: border-box;
+}
+
+#hashover form input,
+#hashover form textarea,
+#hashover form .hashover-inputs input,
+#hashover .hashover-balloon,
+#hashover .hashover-comment input.hashover-delete,
+#hashover img,
+#hashover .hashover-comment pre,
+#hashover .hashover-comment code,
+#hashover .hashover-avatar-image *,
+#hashover .hashover-avatar *,
+#hashover .hashover-comment .hashover-form-buttons a,
+#hashover .hashover-comment label,
+#hashover .hashover-message,
+#hashover #hashover-more-link {
+ border-radius: 5px 5px 5px 5px;
+ -moz-border-radius: 5px 5px 5px 5px;
+ -webkit-border-radius: 5px 5px 5px 5px;
+ -o-border-radius: 5px 5px 5px 5px;
+}
+
+#hashover form textarea {
+ width: 100%;
+ margin-bottom: 8px;
+ padding: 5px 5px 8px 5px;
+ vertical-align: top;
+ resize: vertical;
+}
+
+#hashover .hashover-form-buttons {
+ float: right;
+}
+
+#hashover .hashover-submit {
+ padding: 6px 4px 6px 4px;
+ margin-left: 8px;
+}
+
+#hashover .hashover-submit:hover {
+ background-color: rgba(196, 196, 255, 0.10) ! important;
+ cursor: pointer;
+}
+
+#hashover .hashover-submit:focus,
+#hashover .hashover-submit[disabled] {
+ background-color: rgba(196, 196, 255, 0.25) ! important;
+ border-color: #000000 ! important;
+}
+
+#hashover .hashover-submit[disabled] {
+ background-color: #E8E8E8 ! important;
+}
+
+#hashover textarea:hover,
+#hashover input:hover,
+#hashover .hashover-submit:hover {
+ background-color: #FCFCFC;
+ border-color: #000000;
+ text-decoration: none;
+}
+
+#hashover .hashover-edit-delete {
+ margin: 0px 8px 0px 0px;
+ float: left;
+}
+
+#hashover textarea:hover {
+ border-color: #909090;
+}
+
+#hashover textarea:focus,
+#hashover input:focus {
+ background-color: #FCFCFC;
+ border-color: #0055FF;
+}
+
+#hashover textarea:focus,
+#hashover input[type="text"]:focus,
+#hashover input[type="password"]:focus {
+ box-shadow: 0px 0px 2px rgba(85, 255, 255, 0.8) ! important;
+ -moz-box-shadow: 0px 0px 2px rgba(85, 255, 255, 0.8) ! important;
+ -webkit-box-shadow: 0px 0px 2px rgba(85, 255, 255, 0.8) ! important;
+ -o-box-shadow: 0px 0px 2px rgba(85, 255, 255, 0.8) ! important;
+}
+
+#hashover form .hashover-inputs input {
+ width: 100%;
+ height: 32px;
+ font-size: 14px;
+ padding-left: 28px;
+ padding-right: 6px;
+ background-image: url('./images/inputs-and-buttons.png');
+ background-repeat: no-repeat;
+ background-attachment: scroll;
+ color: #808080;
+}
+
+#hashover form .hashover-inputs input:focus {
+ color: #222222;
+}
+
+#hashover .hashover-inputs > :first-child {
+ padding: 0px;
+}
+
+#hashover .hashover-inputs > :nth-last-child(1) {
+ padding-right: 0px;
+}
+
+#hashover .hashover-name-input input {
+ background-position: left 1px;
+}
+
+#hashover .hashover-password-input input {
+ background-position: left -27px;
+}
+
+#hashover .hashover-login-input input {
+ background-position: center -110px;
+ cursor: pointer;
+}
+
+#hashover .hashover-email-input input {
+ background-position: left -55px;
+}
+
+#hashover .hashover-website-input input {
+ background-position: left -83px;
+}
+
+#hashover .hashover-comment .hashover-inputs {
+ padding-bottom: 6px;
+}
+
+#hashover form label {
+ display: inline-block;
+ padding-top: 8px;
+ vertical-align: middle;
+ cursor: pointer;
+}
+
+#hashover label input[type="checkbox"] {
+ margin: 0px;
+ margin-right: 2px;
+ vertical-align: middle;
+ padding: 0px;
+ margin-top: -1px;
+}
+
+#hashover #hashover-count {
+ float: left;
+}
+
+#hashover #hashover-sort {
+ float: right;
+}
+
+#hashover select {
+ height: 24px;
+ padding: 1px;
+}
+
+#hashover .hashover-comment .hashover-footer .hashover-buttons {
+ float: right;
+ color: transparent;
+ font-size: 0px;
+}
+
+#hashover .hashover-comment .hashover-footer .hashover-buttons a {
+ display: inline-block;
+ border-top: 1px solid #C1C1C1;
+ border-left: 1px solid #C1C1C1;
+ height: 25px;
+ width: 31px;
+ background-color: #ECEEFF;
+ margin: 0px;
+ background-image: url('./images/inputs-and-buttons.png');
+ background-repeat: no-repeat;
+ background-attachment: scroll;
+ color: transparent;
+ font-size: 0px;
+ overflow: hidden ! important;
+}
+
+#hashover .hashover-comment .hashover-footer .hashover-buttons a:first-child {
+ border-top-left-radius: 6px;
+}
+
+#hashover .hashover-comment .hashover-footer .hashover-buttons a:hover {
+ background-color: #E4E6F6;
+}
+
+#hashover .hashover-comment .hashover-footer .hashover-buttons a:hover {
+ opacity: 1.0;
+ filter: alpha(opacity=100);
+}
+
+#hashover a.hashover-has-email {
+ background-position: center -225px;
+}
+
+#hashover a.hashover-no-email {
+ background-position: center -253px;
+}
+
+#hashover a.hashover-has-email,
+#hashover a.hashover-no-email {
+ width: 32px ! important;
+}
+
+#hashover .hashover-comment .hashover-footer .hashover-buttons a.hashover-like,
+#hashover .hashover-comment .hashover-footer .hashover-buttons a.hashover-liked:active {
+ background-position: center -169px;
+}
+
+#hashover .hashover-comment .hashover-footer .hashover-buttons a.hashover-like.dislikes {
+ background-position: center -281px;
+}
+
+#hashover .hashover-comment .hashover-footer .hashover-buttons a.hashover-dislike {
+ background-position: center -309px;
+}
+
+#hashover .hashover-comment .hashover-footer .hashover-buttons a.hashover-liked,
+#hashover .hashover-comment .hashover-footer .hashover-buttons a.hashover-like:active {
+ background-position: center -197px;
+}
+
+#hashover .hashover-comment .hashover-footer .hashover-buttons a.hashover-disliked,
+#hashover .hashover-comment .hashover-footer .hashover-buttons a.hashover-dislike:active {
+ background-position: center -365px;
+}
+
+#hashover .hashover-comment .hashover-footer .hashover-buttons a.hashover-liked.dislikes,
+#hashover .hashover-comment .hashover-footer .hashover-buttons a.hashover-like.dislikes:active {
+ background-position: center -337px;
+}
+
+#hashover .hashover-comment .hashover-footer .hashover-buttons a.hashover-comment-edit {
+ background-position: center -141px;
+}
+
+#hashover hr {
+ border: 0px;
+ height: 1px;
+ background-color: #808080;
+ margin-top: 0px;
+ margin-bottom: 5px;
+}
+
+#hashover input::-moz-focus-inner {
+ border: 0px;
+}
+
+#hashover a,
+#hashover a:link {
+ text-decoration: none;
+ color: #0000CC;
+ outline: none;
+}
+
+#hashover a:visited {
+ color: #00268F;
+}
+
+#hashover a:hover {
+ text-decoration: underline;
+ cursor: pointer;
+ color: #005EFF;
+}
+
+#hashover a.hashover-submit {
+ color: #222222;
+}
+
+#hashover .hashover-comment,
+#hashover .hashover-reply-form {
+ margin-bottom: 16px;
+}
+
+#hashover .hashover-comment > .hashover-comment,
+#hashover .hashover-reply-form {
+ margin-top: 16px;
+}
+
+#hashover .hashover-comment.hashover-reply,
+#hashover .hashover-reply-form {
+ margin-left: 65px;
+}
+
+#hashover .hashover-comment.hashover-reply .hashover-comment,
+#hashover .hashover-comment.hashover-reply .hashover-reply-form {
+ margin-left: 52px;
+}
+
+#hashover .hashover-balloon {
+ overflow: hidden;
+ margin-left: 65px;
+ padding: 6px 8px 8px 8px;
+ background-color: #F6F8FF;
+ border: 1px solid #808080 ! important;
+ min-height: 45px;
+ text-align: left;
+ height: auto;
+ width: auto;
+}
+
+#hashover .hashover-comment .hashover-balloon {
+ margin-left: 64px;
+ padding: 0px;
+}
+
+#hashover .hashover-comment.hashover-reply .hashover-balloon,
+#hashover .hashover-reply-form .hashover-balloon {
+ margin-left: 52px;
+}
+
+#hashover .hashover-comment .hashover-content {
+ padding: 0px 10px 0px 10px;
+ margin: 10px 0px 10px 0px;
+ line-height: 1.5em;
+}
+
+#hashover .hashover-comment .hashover-content p {
+ margin: 0px 0px 1em 0px;
+}
+
+#hashover .hashover-comment .hashover-content img {
+ display: block;
+ max-width: 100%;
+ max-height: 640px;
+ cursor: pointer;
+}
+
+#hashover .hashover-comment.hashover-reply .hashover-content {
+ padding: 0px 8px 0px 8px;
+}
+
+#hashover .hashover-comment hr {
+ background-color: #C1C1C1;
+ margin: 0px;
+}
+
+#hashover .hashover-comment .hashover-inputs > * {
+ padding: 0px 2px 0px 2px;
+}
+
+#hashover .hashover-comment .hashover-inputs > :first-child {
+ padding-left: 0px;
+}
+
+#hashover .hashover-comment .hashover-inputs > :nth-last-child(1) {
+ padding-right: 0px;
+}
+
+#hashover #hashover-form .hashover-comment-name,
+#hashover .hashover-comment .hashover-comment-name {
+ font-size: 20px;
+ font-weight: bold;
+}
+
+#hashover #hashover-form .hashover-comment-name {
+ display: block;
+ margin-bottom: 8px;
+}
+
+#hashover .hashover-comment.hashover-reply .hashover-comment-name {
+ font-size: 16px;
+ margin-top: -1px;
+}
+
+#hashover .hashover-comment .hashover-comment-name.at {
+ color: #00268F;
+}
+
+#hashover .hashover-comment .hashover-title {
+ display: inline-block;
+ clear: right;
+}
+
+#hashover .hashover-comment form .hashover-title {
+ float: left;
+}
+
+#hashover .hashover-comment .hashover-header {
+ line-height: normal;
+ padding: 8px 10px 10px 10px;
+ overflow: hidden;
+}
+
+#hashover .hashover-comment.hashover-reply .hashover-header {
+ padding: 8px 8px 6px 8px;
+}
+
+#hashover .hashover-thread-link {
+ float: right;
+}
+
+#hashover .hashover-avatar-image *,
+#hashover .hashover-avatar *,
+#hashover .hashover-avatar a {
+ display: inline-block;
+ width: 45px;
+ height: 45px;
+ border: 1px solid #808080;
+ vertical-align: top;
+ background-color: rgba(255, 255, 255, 0.8);
+ font-size: 18px;
+ text-align: center;
+ line-height: 45px;
+ font-weight: bold;
+ color: #808080;
+ -webkit-background-size: 100% 100%;
+ -moz-background-size: 100% 100%;
+ -o-background-size: 100% 100%;
+ background-size: 100% 100%;
+}
+
+#hashover .hashover-comment.hashover-reply .hashover-avatar *,
+#hashover .hashover-comment form .hashover-avatar-image * {
+ width: 35px;
+ height: 35px;
+ line-height: 35px;
+ font-size: 15px;
+}
+
+#hashover .hashover-avatar {
+ display: inline-block;
+ position: relative;
+ float: left;
+ padding-right: 18px;
+}
+
+#hashover .hashover-avatar-image:before,
+#hashover .hashover-avatar-image:after,
+#hashover .hashover-avatar:before,
+#hashover .hashover-avatar:after {
+ content: " ";
+ position: absolute;
+ display: block;
+ top: 14px;
+ right: 0px;
+ width: 0px;
+ height: 0px;
+ border-width: 10px;
+ border-color: transparent;
+ border-style: solid solid outset;
+ pointer-events: none;
+}
+
+#hashover .hashover-comment.hashover-reply .hashover-avatar:before,
+#hashover .hashover-comment.hashover-reply .hashover-avatar:after,
+#hashover .hashover-comment form .hashover-avatar-image:before,
+#hashover .hashover-comment form .hashover-avatar-image:after {
+ top: 11px;
+ border-width: 8px;
+}
+
+#hashover .hashover-comment form .hashover-balloon {
+ padding: 8px;
+}
+
+#hashover .hashover-comment .hashover-balloon form {
+ padding: 8px;
+ overflow: hidden;
+}
+
+#hashover .hashover-avatar-image:before,
+#hashover .hashover-avatar:before {
+ border-right-color: #808080;
+ margin-right: -1px;
+}
+
+#hashover .hashover-avatar-image:after,
+#hashover .hashover-avatar:after {
+ border-right-color: #F7F9FF;
+ margin-right: -2px;
+}
+
+#hashover .hashover-comment.hashover-reply .hashover-avatar {
+ padding-right: 15px;
+}
+
+#hashover .hashover-comment pre,
+#hashover .hashover-comment code,
+#hashover .hashover-comment blockquote,
+#hashover .hashover-comment ol,
+#hashover .hashover-comment ul {
+ vertical-align: top;
+}
+
+#hashover .hashover-comment pre,
+#hashover .hashover-comment code {
+ width: 100%;
+ max-height: 200px;
+ display: inline-block;
+ overflow: auto;
+ border: 1px solid #C1C1C1;
+ background-color: #F5F5F5;
+ white-space: pre;
+ padding: 5px;
+ margin: 0px;
+}
+
+#hashover code.hashover-inline {
+ display: inline;
+ padding: 1px 4px;
+ border-radius: 0px;
+ font-size: 12px;
+ line-height: 1.5em;
+}
+
+#hashover .hashover-comment blockquote,
+#hashover .hashover-comment ol,
+#hashover .hashover-comment ul {
+ padding-left: 10px;
+ margin: 0px 20px 8px 20px;
+}
+
+#hashover .hashover-comment blockquote {
+ border-left: 3px solid rgba(0, 0, 0, 0.3);
+ margin: 8px 20px 4px 20px;
+}
+
+#hashover .hashover-comment .hashover-date {
+ display: inline-block;
+ margin-top: 4px;
+}
+
+#hashover .hashover-comment .hashover-date a,
+#hashover .hashover-comment .hashover-date a:visited,
+#hashover .hashover-comment .hashover-date span {
+ color: #606060;
+}
+
+#hashover .hashover-comment .hashover-date a:hover,
+#hashover .hashover-comment .hashover-date a:active {
+ color: #101010;
+}
+
+#hashover .hashover-comment .hashover-footer {
+ padding: 0px 0px 0px 8px;
+ margin-top: 10px;
+ width: auto;
+ overflow: hidden;
+}
+
+#hashover .hashover-comment.hashover-reply .hashover-footer {
+ margin-top: -3px;
+}
+
+#hashover .hashover-comment .hashover-form-buttons input.hashover-delete:hover,
+#hashover .hashover-comment .hashover-form-buttons input.hashover-delete:focus {
+ border: 1px solid #BB0000 ! important;
+ background-color: #FF0000 ! important;
+ color: #FCFCFC ! important;
+ cursor: pointer;
+}
+
+#hashover .hashover-comment input.hashover-delete:focus {
+ box-shadow: 0px 0px 3px #FF5555 ! important;
+ -moz-box-shadow: 0px 0px 3px #FF5555 ! important;
+ -webkit-box-shadow: 0px 0px 3px #FF5555 ! important;
+ -o-box-shadow: 0px 0px 3px #FF5555 ! important;
+}
+
+#hashover .hashover-comment.hashover-first .hashover-content,
+#hashover .hashover-comment.hashover-first .hashover-footer,
+#hashover .hashover-comment.hashover-notice .hashover-footer {
+ display: none;
+}
+
+#hashover .hashover-comment.hashover-first .hashover-header {
+ margin-bottom: 0px;
+}
+
+#hashover .hashover-comment.hashover-first .hashover-title,
+#hashover .hashover-comment.hashover-deleted .hashover-title {
+ font-size: 16px;
+ display: inline-block;
+ position: relative;
+ top: 12px;
+ left: 10px;
+ vertical-align: top;
+}
+
+#hashover .hashover-comment.hashover-deleted .hashover-title {
+ color: #FF0000;
+}
+
+#hashover #hashover-end-links {
+ border-top: 1px solid #AAAAAA;
+ margin-top: -12px;
+ padding: 18px 0px 5px 0px;
+ text-align: center;
+}
+
+#hashover #hashover-more-link {
+ display: block;
+ padding: 12px;
+ margin-bottom: -16px;
+ background-color: rgba(210, 210, 255, 0.2);
+ border: 1px dashed #AAAAFF;
+ text-align: center;
+ color: #404040;
+ opacity: 1.0;
+ filter: alpha(opacity=100);
+}
+
+#hashover #hashover-more-link:hover {
+ background-color: rgba(210, 210, 255, 0.5);
+}
+
+#hashover #hashover-more-link.hashover-hide-morelink {
+ opacity: 0.0;
+ filter: alpha(opacity=0);
+}
diff --git a/bootstrap/comments/themes/1.0-ported/comments.html b/bootstrap/comments/themes/1.0-ported/comments.html
new file mode 100644
index 0000000..8e420db
--- /dev/null
+++ b/bootstrap/comments/themes/1.0-ported/comments.html
@@ -0,0 +1,30 @@
+{hashover:avatar}
+
+<div class="hashover-balloon">
+ <div class="hashover-header">
+ {hashover:name} {hashover:thread-link}
+ </div>
+
+ <div id="hashover-content-{hashover:permalink}" class="hashover-content">
+ {hashover:comment}
+ </div>
+
+ {placeholder:edit-form}
+
+ <div id="hashover-footer-{hashover:permalink}" class="hashover-footer">
+ <span class="hashover-date">
+ {hashover:date}&nbsp;
+ {hashover:like-count}&nbsp;
+ {hashover:dislike-count}
+ </span>
+
+ <span class="hashover-buttons">
+ {hashover:like-link}
+ {hashover:dislike-link}
+ {hashover:edit-link}
+ {hashover:reply-link}
+ </span>
+ </div>
+</div>
+
+{placeholder:reply-form}
diff --git a/bootstrap/comments/themes/1.0-ported/images/inputs-and-buttons.png b/bootstrap/comments/themes/1.0-ported/images/inputs-and-buttons.png
new file mode 100644
index 0000000..45dfc78
--- /dev/null
+++ b/bootstrap/comments/themes/1.0-ported/images/inputs-and-buttons.png
Binary files differ
diff --git a/bootstrap/comments/themes/default-borderless/comments.css b/bootstrap/comments/themes/default-borderless/comments.css
new file mode 100644
index 0000000..6262bcc
--- /dev/null
+++ b/bootstrap/comments/themes/default-borderless/comments.css
@@ -0,0 +1,1172 @@
+/*-----------------------------------------------------------------------------
+
+Copyright (C) 2016-2018 Jacob Barkdull
+This file is part of HashOver.
+
+I, Jacob Barkdull, hereby release this work into the public domain.
+This applies worldwide. If this is not legally possible, I grant any
+entity the right to use this work for any purpose, without any
+conditions, unless such conditions are required by law.
+
+-----------------------------------------------------------------------------*/
+
+.hashover,
+.hashover input,
+.hashover textarea,
+.hashover select {
+ text-indent: initial;
+ font-size: 14px;
+ font-family: "Cantarell", "Arial", "Helvetica", "FreeSans", sans-serif;
+ color: #222222;
+}
+
+.hashover > div {
+ display: block;
+}
+
+.hashover :before,
+.hashover :after {
+ position: relative;
+ z-index: 3;
+}
+
+#hashover-form {
+ border: none;
+}
+
+.hashover #hashover-requiredFields,
+.hashover .hashover-hidden,
+.hashover .hashover-notice > .hashover-balloon > .hashover-footer,
+.hashover .hashover-first .hashover-balloon,
+.hashover .hashover-first .hashover-avatar:before,
+.hashover .hashover-first .hashover-avatar:after {
+ display: none ! important;
+}
+
+.hashover,
+#hashover-top-comments,
+#hashover-sort-section,
+.hashover .hashover-inputs {
+ clear: both;
+}
+
+.hashover #hashover-form-section,
+.hashover #hashover-popular-section,
+.hashover #hashover-comments-section {
+ margin-bottom: 36px;
+}
+
+.hashover .hashover-title,
+.hashover #hashover-count,
+.hashover .hashover-comment-name {
+ display: inline-block;
+ font-size: 18px;
+}
+
+.hashover .hashover-reply .hashover-comment-name {
+ font-weight: bold;
+ font-size: 14px;
+}
+
+.hashover form input,
+.hashover form textarea,
+.hashover .hashover-select-wrapper,
+.hashover .hashover-submit,
+.hashover .hashover-avatar-image *,
+.hashover .hashover-avatar *,
+.hashover .hashover-inputs input,
+.hashover .hashover-comment textarea,
+.hashover .hashover-balloon,
+.hashover .hashover-reply > .hashover-balloon,
+.hashover .hashover-reply-form,
+.hashover .hashover-reply-form .hashover-inputs,
+.hashover .hashover-reply .hashover-header,
+.hashover .hashover-form-footer {
+ border: 1px solid #AAAAAA;
+}
+
+.hashover .hashover-dashed-title {
+ display: inline-block;
+ width: 100%;
+ margin-bottom: 12px;
+ padding-bottom: 10px;
+ vertical-align: top;
+ border-bottom: 1px dashed rgb(190, 190, 190);
+}
+
+.hashover #hashover-message {
+ margin-bottom: 24px;
+}
+
+.hashover #hashover-message,
+.hashover .hashover-more-link {
+ -moz-border-radius: 6px 6px 6px 6px;
+ -webkit-border-radius: 6px 6px 6px 6px;
+ border-radius: 6px 6px 6px 6px;
+}
+
+.hashover .hashover-message {
+ position: relative;
+ z-index: 1;
+}
+
+.hashover .hashover-formatting {
+ background-position: 0px -454px;
+}
+
+.hashover .hashover-message,
+.hashover .hashover-message > *,
+.hashover .hashover-formatting-message,
+.hashover .hashover-formatting-message > * {
+ display: block;
+ width: 100%;
+ max-height: 0px;
+}
+
+.hashover .hashover-message > *,
+.hashover .hashover-formatting-message > * {
+ color: transparent;
+}
+
+.hashover .hashover-message > * {
+ padding: 0px 12px;
+ margin-bottom: 12px;
+ text-align: center;
+ border: 1px solid #AAAAAA;
+}
+
+.hashover .hashover-message.hashover-message-open > *,
+.hashover .hashover-formatting-message.hashover-message-open > * {
+ color: initial;
+}
+
+.hashover .hashover-message.hashover-message-open > * {
+ padding: 12px;
+ color: #0000CC;
+ background-color: rgba(225, 225, 255, 0.25);
+ border-color: #00AACC;
+}
+
+.hashover .hashover-php-message-open,
+.hashover .hashover-php-message-open > * {
+ max-height: initial;
+}
+
+.hashover .hashover-message.hashover-message-error > * {
+ color: #CC0000;
+ background-color: rgba(255, 225, 225, 0.25);
+ border-color: #CC0000;
+}
+
+.hashover .hashover-formatting-message > * {
+ margin-top: -1px;
+ border: 1px solid #AAAAAA;
+}
+
+.hashover .hashover-formatting-table {
+ display: table;
+ padding: 12px 0px 12px 0px;
+ background-color: #E5E5E5;
+}
+
+.hashover .hashover-formatting-table > * {
+ display: table-cell;
+ width: 50%;
+ padding: 0px 12px 0px 12px;
+ border-right: 1px solid #AAAAAA;
+ vertical-align: top;
+}
+
+.hashover .hashover-formatting-table > *:last-child {
+ border-right: none;
+}
+
+.hashover .hashover-comment .hashover-message {
+ margin-top: -1px;
+}
+
+.hashover .hashover-comment .hashover-message > * {
+ margin-bottom: 0px;
+}
+
+.hashover #hashover-form,
+.hashover .hashover-comment,
+.hashover .hashover-reply-form,
+.hashover .hashover-message,
+.hashover .hashover-message > *,
+.hashover .hashover-formatting-message,
+.hashover .hashover-formatting-message > *,
+.hashover .hashover-comment form,
+.hashover .hashover-comment form .hashover-balloon {
+ overflow: hidden;
+}
+
+.hashover form,
+.hashover .hashover-comment form {
+ display: block;
+}
+
+.hashover .hashover-inputs {
+ display: table;
+ width: 100%;
+ padding: 0px 0px 12px 0px;
+}
+
+.hashover.hashover-logged-out .hashover-reply-form .hashover-inputs {
+ padding: 12px;
+ border-bottom: none;
+}
+
+.hashover form textarea,
+.hashover .hashover-reply .hashover-header,
+.hashover.hashover-logged-out .hashover-reply-form .hashover-inputs {
+ -moz-border-radius: 6px 6px 0px 0px;
+ -webkit-border-radius: 6px 6px 0px 0px;
+ border-radius: 6px 6px 0px 0px;
+}
+
+.hashover .hashover-inputs > * {
+ display: table-cell;
+ vertical-align: middle;
+ line-height: 16px;
+}
+
+.hashover .hashover-name-input input,
+.hashover .hashover-password-input input,
+.hashover .hashover-email-input input,
+.hashover .hashover-website-input input {
+ background-image: -webkit-linear-gradient(#F0F0F0, #FCFCFC, #FCFCFC, #FCFCFC);
+ background-image: -moz-linear-gradient(#F0F0F0, #FCFCFC, #FCFCFC, #FCFCFC);
+ background-image: linear-gradient(#F0F0F0, #FCFCFC, #FCFCFC, #FCFCFC);
+}
+
+.hashover .hashover-name-input,
+.hashover .hashover-password-input,
+.hashover .hashover-email-input,
+.hashover .hashover-website-input {
+ position: relative;
+}
+
+.hashover .hashover-name-input:before,
+.hashover .hashover-password-input:before,
+.hashover .hashover-email-input:before,
+.hashover .hashover-website-input:before,
+.hashover .hashover-required-input:after {
+ content: " ";
+ display: inline-block;
+ position: absolute;
+ top: 50%;
+ left: 0px;
+ width: 28px;
+ height: 32px;
+ margin-top: -16px;
+ background-image: url('../../images/inputs-and-buttons.png');
+ background-repeat: no-repeat;
+ background-attachment: scroll;
+ border: 1px solid transparent;
+ pointer-events: none;
+}
+
+.hashover .hashover-required-input:after {
+ left: auto;
+ right: 0px;
+ background-position: 0px -419px;
+}
+
+.hashover.hashover-logged-out .hashover-inputs > *,
+.hashover .hashover-comment .hashover-inputs > * {
+ padding: 0px 3px 0px 3px;
+}
+
+.hashover .hashover-comment,
+.hashover .hashover-reply-form,
+.hashover .hashover-comment .hashover-avatar,
+.hashover .hashover-comment .hashover-comment-name,
+.hashover .hashover-avatar-image {
+ position: relative;
+}
+
+.hashover #hashover-form .hashover-avatar-image {
+ width: 45px;
+ padding-right: 10px;
+}
+
+.hashover .hashover-avatar-image *,
+.hashover .hashover-avatar * {
+ vertical-align: middle;
+}
+
+.hashover form input,
+.hashover form textarea,
+.hashover .hashover-inputs input,
+.hashover .hashover-submit {
+ padding: 2px;
+ margin: 0px;
+ background-color: #FCFCFC;
+ outline-offset: none;
+ outline: -webkit-focus-ring-color none;
+}
+
+.hashover form input,
+.hashover .hashover-submit {
+ -moz-border-radius: 4px;
+ -webkit-border-radius: 4px;
+ border-radius: 4px;
+}
+
+.hashover form input:focus,
+.hashover form textarea:focus,
+.hashover .hashover-submit:focus,
+.hashover .hashover-inputs input:focus,
+.hashover .hashover-comment label:focus,
+.hashover .hashover-select-wrapper:focus {
+ -o-transition: border 150ms linear 0ms, background-color 150ms linear 0ms, box-shadow 150ms linear 0ms;
+ -webkit-transition: border 150ms linear 0ms, background-color 150ms linear 0ms, box-shadow 150ms linear 0ms;
+ -moz-transition: border 150ms linear 0ms, background-color 150ms linear 0ms, box-shadow 150ms linear 0ms;
+ transition: border 150ms linear 0ms, background-color 150ms linear 0ms, box-shadow 150ms linear 0ms;
+}
+
+.hashover *,
+.hashover *:before,
+.hashover *:after {
+ -o-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+
+.hashover form textarea {
+ position: relative;
+ width: 100%;
+ padding: 12px;
+ vertical-align: top;
+ resize: vertical;
+}
+
+.hashover form textarea:focus {
+ z-index: 2;
+}
+
+.hashover.hashover-logged-out .hashover-reply-textarea {
+ -moz-border-radius: 0px;
+ -webkit-border-radius: 0px;
+ border-radius: 0px;
+}
+
+.hashover .hashover-comment > .hashover-balloon,
+.hashover .hashover-edit-form,
+.hashover #hashover-end-links,
+.hashover .hashover-border-top {
+ border-top: 1px solid #AAAAAA;
+}
+
+.hashover .hashover-comment > .hashover-balloon {
+ padding: 12px;
+
+ -moz-border-radius: 6px;
+ -webkit-border-radius: 6px;
+ border-radius: 6px;
+}
+
+.hashover .hashover-comment form .hashover-balloon {
+ border: none;
+}
+
+.hashover textarea:hover,
+.hashover input:hover,
+.hashover .hashover-submit:hover {
+ background-color: #FCFCFC;
+ border-color: #606060;
+ text-decoration: none;
+}
+
+.hashover input:focus,
+.hashover textarea:focus,
+.hashover .hashover-submit:focus {
+ border-color: #0055FF ! important;
+
+ -o-box-shadow: 0px 0px 2px #BFEFFF ! important;
+ -webkit-box-shadow: 0px 0px 2px #BFEFFF ! important;
+ -moz-box-shadow: 0px 0px 2px #BFEFFF ! important;
+ box-shadow: 0px 0px 2px #BFEFFF ! important;
+}
+
+.hashover .hashover-submit {
+ background-color: #F5F8FC;
+ padding: 6px 10px 6px 10px;
+ margin-left: 5px;
+}
+
+.hashover .hashover-post-button,
+.hashover .hashover-reply-post,
+.hashover .hashover-edit-post {
+ background-color: #8CB1FF;
+ border-color: #0055AA;
+ color: #FCFCFC;
+}
+
+.hashover .hashover-footer *,
+.hashover .hashover-form-footer * {
+ vertical-align: top;
+ line-height: 16px;
+}
+
+.hashover .hashover-post-button:hover,
+.hashover .hashover-reply-post:hover,
+.hashover .hashover-edit-post:hover {
+ background-color: #73A1FF;
+ border-color: #0055AA;
+}
+
+.hashover .hashover-post-button:focus,
+.hashover .hashover-reply-post:focus,
+.hashover .hashover-edit-post:focus {
+ background-color: #6698FF ! important;
+ border-color: #000088 ! important;
+}
+
+.hashover .hashover-edit-delete {
+ float: left;
+ margin-right: 12px;
+ background-color: #FF8C8C;
+ border-color: #AA0000;
+ color: #FCFCFC;
+}
+
+.hashover .hashover-edit-delete:hover {
+ background-color: #FF6666;
+ border-color: #AA0000;
+}
+
+.hashover .hashover-edit-delete:focus {
+ border-color: #880000 ! important;
+ background-color: #FF5959 ! important;
+
+ -o-box-shadow: 0px 0px 2px #FFEFBF ! important;
+ -webkit-box-shadow: 0px 0px 2px #FFEFBF ! important;
+ -moz-box-shadow: 0px 0px 2px #FFEFBF ! important;
+ box-shadow: 0px 0px 2px #FFEFBF ! important;
+}
+
+.hashover .hashover-submit:hover {
+ cursor: pointer;
+}
+
+.hashover .hashover-submit:focus,
+.hashover .hashover-submit[disabled] {
+ border-color: #000000;
+}
+
+.hashover .hashover-submit[disabled] {
+ background-color: #E0E0E0 ! important;
+ color: #222222 ! important;
+
+ -o-box-shadow: none ! important;
+ -webkit-box-shadow: none ! important;
+ -moz-box-shadow: none ! important;
+ box-shadow: none ! important;
+}
+
+.hashover .hashover-inputs input {
+ width: 100%;
+ height: 32px;
+ font-size: 14px;
+ padding-left: 30px;
+ padding-right: 6px;
+ color: #808080;
+}
+
+.hashover .hashover-required-input input {
+ padding-right: 30px;
+}
+
+.hashover .hashover-inputs input:focus {
+ color: #222222;
+}
+
+.hashover .hashover-inputs > :first-child {
+ padding-left: 0px;
+}
+
+.hashover .hashover-inputs > :nth-last-child(1) {
+ padding-right: 0px;
+}
+
+.hashover .hashover-name-input:before {
+ background-position: 0px 1px;
+}
+
+.hashover .hashover-password-input:before {
+ background-position: 0px -27px;
+}
+
+.hashover .hashover-email-input:before {
+ background-position: 0px -55px;
+}
+
+.hashover .hashover-website-input:before {
+ background-position: 0px -83px;
+}
+
+.hashover .hashover-emphasized-input,
+.hashover .hashover-emphasized-input input {
+ border-color: #BB0000 ! important;
+}
+
+.hashover .hashover-emphasized-input:focus,
+.hashover .hashover-emphasized-input input:focus {
+ border-color: #DD0000 ! important;
+
+ -o-box-shadow: 0px 0px 2px #FFBFBF !important;
+ -webkit-box-shadow: 0px 0px 2px #FFBFBF !important;
+ -moz-box-shadow: 0px 0px 2px #FFBFBF !important;
+ box-shadow: 0px 0px 2px #FFBFBF !important;
+}
+
+.hashover form label {
+ cursor: pointer;
+}
+
+.hashover #hashover-form .hashover-comment-label,
+.hashover .hashover-comment-label:nth-child(2) {
+ background-color: #FCFCFC;
+}
+
+.hashover .hashover-inputs label,
+.hashover .hashover-comment-label {
+ padding: 5px;
+ display: inline-block;
+ background-color: rgba(200, 200, 200, 0.25);
+}
+
+.hashover .hashover-comment-label {
+ width: 100%;
+ display: block;
+ border: 1px solid #AAA;
+ margin-bottom: -1px;
+}
+
+.hashover .hashover-form-links {
+ display: inline-block;
+ vertical-align: middle;
+ margin-bottom: 12px;
+}
+
+.hashover .hashover-form-links > * {
+ display: inline-block;
+ margin-top: 7px;
+ margin-right: 6px;
+}
+
+.hashover .hashover-form-links > *:last-child {
+ margin-right: 0px;
+}
+
+.hashover .hashover-form-buttons {
+ margin-bottom: 12px;
+}
+
+.hashover label input[type="checkbox"] {
+ margin: -2px 4px 0px 0px;
+ vertical-align: middle;
+ padding: 0px;
+}
+
+.hashover .hashover-form-footer {
+ padding: 12px 12px 0px 12px;
+ overflow: hidden;
+ margin-top: -1px;
+ background-color: #F0F0F0;
+}
+
+.hashover .hashover-reply > .hashover-balloon,
+.hashover .hashover-reply-form .hashover-balloon,
+.hashover .hashover-form-footer {
+ -moz-border-radius: 0px 0px 6px 6px;
+ -webkit-border-radius: 0px 0px 6px 6px;
+ border-radius: 0px 0px 6px 6px;
+}
+
+.hashover .hashover-edit-form,
+.hashover .hashover-border-top {
+ margin-top: 12px;
+ padding-top: 12px;
+}
+
+.hashover .hashover-edit-form .hashover-title {
+ margin-top: 3px;
+}
+
+.hashover #hashover-count {
+ float: left;
+}
+
+.hashover #hashover-sort,
+.hashover .hashover-edit-status,
+.hashover .hashover-comment .hashover-footer .hashover-buttons,
+.hashover .hashover-form-buttons,
+.hashover .hashover-thread-link {
+ float: right;
+}
+
+.hashover select {
+ height: 24px;
+}
+
+.hashover hr {
+ border: 0px;
+ height: 1px;
+ background-color: #AAAAAA;
+ margin-top: 0px;
+ margin-bottom: 5px;
+}
+
+.hashover input::-moz-focus-inner {
+ border: 0px;
+}
+
+.hashover a,
+.hashover a:link,
+.hashover .hashover-fake-link {
+ text-decoration: none;
+ color: #0000CC;
+ outline: none;
+ cursor: pointer;
+}
+
+.hashover a:visited {
+ color: #00268F;
+}
+
+.hashover a:hover,
+.hashover .hashover-fake-link:hover {
+ text-decoration: underline;
+ cursor: pointer;
+ color: #005EFF;
+}
+
+.hashover a.hashover-submit {
+ display: inline-block;
+ color: #222222;
+}
+
+.hashover .hashover-reply .hashover-avatar *,
+.hashover .hashover-comment form .hashover-avatar-image * {
+ width: 32px;
+ height: 32px;
+ line-height: 34px;
+}
+
+.hashover .hashover-reply .hashover-avatar a {
+ font-size: 14px;
+}
+
+.hashover .hashover-avatar-image *,
+.hashover .hashover-avatar *,
+.hashover .hashover-comment > .hashover-balloon,
+.hashover .hashover-reply .hashover-header {
+ background-color: #FCFCFC;
+}
+
+.hashover .hashover-comment {
+ margin-bottom: 24px;
+}
+
+.hashover .hashover-comment > .hashover-header {
+ padding: 0px 0px 12px 0px;
+}
+
+.hashover .hashover-reply,
+.hashover .hashover-reply-form {
+ border: none;
+ margin: 13px 0px 12px 0px;
+}
+
+.hashover .hashover-reply .hashover-header {
+ padding: 12px 12px 0px 12px;
+ margin-left: 48px;
+ border-bottom: none;
+}
+
+.hashover .hashover-reply .hashover-reply,
+.hashover .hashover-reply .hashover-reply-form {
+ padding: 0px;
+ margin: 12px 0px 0px 48px;
+}
+
+.hashover .hashover-comment .hashover-avatar {
+ display: inline-block;
+ height: auto;
+ margin-right: 10px;
+}
+
+.hashover .hashover-avatar-image *,
+.hashover .hashover-avatar *,
+.hashover .hashover-avatar a {
+ display: inline-block;
+ width: 45px;
+ height: 45px;
+ font-size: 18px;
+ text-align: center;
+ line-height: 45px;
+ color: #808080;
+ background-position: center center;
+ background-attachment: scroll;
+ background-repeat: no-repeat;
+
+ -o-background-size: 100% 100%;
+ -webkit-background-size: 100% 100%;
+ -moz-background-size: 100% 100%;
+ background-size: 100% 100%;
+
+ -o-box-sizing: content-box;
+ -webkit-box-sizing: content-box;
+ -moz-box-sizing: content-box;
+ box-sizing: content-box;
+
+ -moz-border-radius: 50%;
+ -webkit-border-radius: 50%;
+ border-radius: 50%;
+}
+
+.hashover .hashover-reply .hashover-avatar,
+.hashover .hashover-comment form .hashover-avatar-image {
+ position: absolute;
+ display: inline-block;
+ top: 0px;
+ left: 0px;
+ padding: 0px;
+ float: left;
+}
+
+.hashover .hashover-reply .hashover-reply .hashover-avatar,
+.hashover .hashover-reply form .hashover-avatar-image {
+ left: 0px;
+}
+
+.hashover .hashover-comment .hashover-content {
+ line-height: 1.5em;
+ margin-bottom: 12px;
+}
+
+.hashover p {
+ margin: 0px 0px 12px 0px;
+}
+
+.hashover p:last-child {
+ margin-bottom: 0px;
+}
+
+.hashover .hashover-reply > .hashover-balloon,
+.hashover .hashover-reply-form .hashover-balloon {
+ margin-left: 48px;
+ border-top: none;
+}
+
+.hashover .hashover-comment .hashover-content img {
+ display: block;
+ max-width: 100%;
+ max-height: 640px;
+ cursor: pointer;
+}
+
+.hashover .hashover-avatar-image:before,
+.hashover .hashover-avatar-image:after,
+.hashover .hashover-comment .hashover-avatar:before,
+.hashover .hashover-comment .hashover-avatar:after {
+ content: " ";
+ position: absolute;
+ display: block;
+ bottom: -13px;
+ left: 50%;
+ margin-left: -10px;
+ width: 0px;
+ height: 0px;
+ border-width: 10px;
+ border-color: transparent;
+ border-style: solid outset solid;
+ pointer-events: none;
+}
+
+.hashover .hashover-avatar-image:before,
+.hashover .hashover-avatar-image:after {
+ margin-left: -15px;
+}
+
+.hashover .hashover-avatar-image:before,
+.hashover .hashover-comment .hashover-avatar:before,
+.hashover .hashover-comment form .hashover-avatar-image:before {
+ border-bottom-color: #AAAAAA;
+}
+
+.hashover .hashover-avatar-image:after,
+.hashover .hashover-comment .hashover-avatar:after,
+.hashover .hashover-comment form .hashover-avatar-image:after {
+ border-bottom-color: #FCFCFC;
+ bottom: -14px;
+}
+
+.hashover .hashover-reply .hashover-avatar:before,
+.hashover .hashover-reply .hashover-avatar:after,
+.hashover .hashover-comment form .hashover-avatar-image:before,
+.hashover .hashover-comment form .hashover-avatar-image:after {
+ border-style: solid solid outset;
+ border-color: transparent;
+ margin-left: auto;
+ margin-bottom: -8px;
+ top: auto;
+ left: auto;
+ bottom: 50%;
+ right: -15px;
+ border-width: 8px;
+}
+
+.hashover .hashover-reply .hashover-avatar:before,
+.hashover .hashover-comment form .hashover-avatar-image:before {
+ border-right-color: #AAAAAA;
+}
+
+.hashover .hashover-reply .hashover-avatar:after,
+.hashover .hashover-comment form .hashover-avatar-image:after {
+ border-right-color: #FCFCFC;
+ right: -16px;
+}
+
+.hashover.hashover-logged-out .hashover-comment form .hashover-avatar-image:after {
+ border-right-color: #FCFCFC;
+}
+
+.hashover .hashover-select-wrapper select {
+ border: none;
+ background-color: #FCFCFC;
+ padding: 0px 14px 0px 0px;
+ cursor: pointer;
+}
+
+.hashover .hashover-select-wrapper,
+.hashover .hashover-embedded-image-wrapper {
+ display: inline-block;
+ position: relative;
+}
+
+.hashover .hashover-select-wrapper {
+ overflow: hidden;
+ margin-top: -3px;
+ line-height: 14px;
+ vertical-align: middle;
+ cursor: pointer;
+
+ -moz-border-radius: 4px;
+ -webkit-border-radius: 4px;
+ border-radius: 4px;
+}
+
+.hashover .hashover-select-wrapper:before {
+ content: "\25BC";
+ position: absolute;
+ right: 0px;
+ top: 0px;
+ display: inline-block;
+ height: 100%;
+ width: 26px;
+ padding: 5px 4px;
+ margin: 0px;
+ font-size: 12px;
+ line-height: 12px;
+ font-family: monospace;
+ text-align: center;
+ pointer-events: none;
+ background-color: #F0F0F0;
+ border-left: 1px solid #AAAAAA;
+}
+
+.hashover .hashover-select-wrapper:hover,
+.hashover .hashover-select-wrapper:hover:before {
+ border-color: #606060;
+}
+
+.hashover .hashover-comment-name {
+ vertical-align: middle;
+}
+
+.hashover .hashover-name-twitter:before {
+ content: "@";
+ color: #00268F;
+ cursor: default;
+}
+
+.hashover .hashover-comment .hashover-title {
+ display: inline-block;
+ vertical-align: middle;
+ clear: right;
+}
+
+.hashover .hashover-comment pre,
+.hashover .hashover-comment code,
+.hashover .hashover-comment blockquote,
+.hashover .hashover-comment ol,
+.hashover .hashover-comment ul {
+ vertical-align: top;
+}
+
+.hashover .hashover-comment pre,
+.hashover .hashover-comment code {
+ display: inline-block;
+ width: 100%;
+ max-height: 400px;
+ white-space: pre;
+ padding: 5px;
+ margin: 0px;
+ font-family: monospace;
+ font-size: 12px;
+ background-color: #EEEEEE;
+ overflow: auto;
+}
+
+.hashover code.hashover-inline {
+ display: inline;
+ padding: 1px 4px;
+}
+
+.hashover .hashover-comment blockquote,
+.hashover .hashover-comment ol,
+.hashover .hashover-comment ul {
+ padding-left: 10px;
+ margin: 0px 24px 0px 24px;
+}
+
+.hashover .hashover-comment blockquote {
+ border-left: 3px solid rgba(0, 0, 0, 0.3);
+}
+
+.hashover .hashover-comment .hashover-date-permalink,
+.hashover .hashover-comment .hashover-date-permalink:visited,
+.hashover .hashover-comment .hashover-date > * {
+ color: #606060;
+}
+
+.hashover .hashover-comment .hashover-date-permalink:hover,
+.hashover .hashover-comment .hashover-date-permalink:active {
+ color: #101010;
+}
+
+.hashover .hashover-comment .hashover-date,
+.hashover .hashover-comment .hashover-date-permalink,
+.hashover .hashover-comment .hashover-replies,
+.hashover .hashover-comment .hashover-likes,
+.hashover .hashover-comment .hashover-dislikes {
+ display: inline-block;
+ vertical-align: top;
+}
+
+.hashover .hashover-comment .hashover-date-permalink,
+.hashover .hashover-comment .hashover-replies,
+.hashover .hashover-comment .hashover-likes,
+.hashover .hashover-comment .hashover-dislikes {
+ float: left;
+}
+
+.hashover .hashover-comment .hashover-replies,
+.hashover .hashover-comment .hashover-likes,
+.hashover .hashover-comment .hashover-dislikes {
+ padding-left: 8px;
+}
+
+.hashover .hashover-formatting,
+.hashover .hashover-comment .hashover-buttons a {
+ display: inline-block;
+ vertical-align: top;
+ min-height: 16px;
+ padding-left: 30px;
+ line-height: 16px;
+ background-image: url('../../images/inputs-and-buttons.png');
+ background-repeat: no-repeat;
+ background-attachment: scroll;
+ opacity: 0.50;
+ filter: alpha(opacity=50);
+}
+
+.hashover .hashover-formatting:hover,
+.hashover .hashover-comment .hashover-buttons a:hover {
+ opacity: 1.0;
+ filter: alpha(opacity=100);
+}
+
+.hashover .hashover-formatting,
+.hashover .hashover-formatting:hover,
+.hashover .hashover-comment .hashover-buttons a,
+.hashover .hashover-comment .hashover-buttons a:hover {
+ color: #111111;
+}
+
+.hashover .hashover-has-email {
+ background-position: left -258px;
+}
+
+.hashover .hashover-no-email {
+ background-position: left -286px;
+}
+
+.hashover .hashover-comment .hashover-like,
+.hashover .hashover-comment .hashover-liked:active,
+.hashover .hashover-comment .hashover-dislike {
+ background-position: left -174px;
+}
+
+.hashover .hashover-comment .hashover-liked,
+.hashover .hashover-comment .hashover-like:active {
+ background-position: left -202px;
+}
+
+.hashover .hashover-comment .hashover-like.hashover-dislikes-enabled {
+ background-position: left -314px;
+}
+
+.hashover .hashover-comment .hashover-liked.hashover-dislikes-enabled,
+.hashover .hashover-comment .hashover-like.hashover-dislikes-enabled:active {
+ background-position: left -370px;
+}
+
+.hashover .hashover-comment .hashover-disliked,
+.hashover .hashover-comment .hashover-dislike:active {
+ background-position: left -230px;
+}
+
+.hashover .hashover-comment .hashover-dislike.hashover-likes-enabled {
+ background-position: left -342px;
+}
+
+.hashover .hashover-comment .hashover-disliked.hashover-likes-enabled,
+.hashover .hashover-comment .hashover-dislike.hashover-likes-enabled:active {
+ background-position: left -398px;
+}
+
+.hashover .hashover-comment .hashover-comment-edit {
+ background-position: 2px -147px;
+}
+
+.hashover .hashover-comment .hashover-like.hashover-dislikes-enabled,
+.hashover .hashover-comment .hashover-liked.hashover-dislikes-enabled,
+.hashover .hashover-comment .hashover-dislike.hashover-likes-enabled,
+.hashover .hashover-comment .hashover-disliked.hashover-likes-enabled {
+ height: 16px;
+ font-size: 0px;
+ color: transparent;
+}
+
+.hashover .hashover-deleted > .hashover-header > .hashover-comment-name {
+ color: #CC0000;
+}
+
+.hashover .hashover-notice > .hashover-balloon > .hashover-content {
+ margin: 0px;
+}
+
+.hashover .hashover-more-link {
+ display: block;
+ padding: 12px;
+ margin-bottom: 12px;
+ background-color: rgba(230, 237, 250, 0.6);
+ border: 1px dashed #AAAAFF;
+ text-align: center;
+ opacity: 1.0;
+ filter: alpha(opacity=100);
+}
+
+.hashover .hashover-more-link,
+.hashover a.hashover-more-link {
+ color: #404040;
+}
+
+.hashover .hashover-more-link:hover {
+ background-color: rgba(230, 237, 250, 1.0);
+}
+
+.hashover .hashover-loading:before {
+ content: " " ! important;
+ display: inline-block;
+ min-width: 16px;
+ min-height: 16px;
+ vertical-align: middle;
+ background-repeat: no-repeat;
+ background-attachment: scroll;
+ background-position: center center;
+ background-image: url('../../images/loading.gif');
+
+ image-rendering: -o-crisp-edges;
+ image-rendering: -webkit-optimize-contrast;
+ image-rendering: -moz-crisp-edges;
+ image-rendering: crisp-edges;
+ image-rendering: pixelated;
+ -ms-interpolation-mode: nearest-neighbor;
+}
+
+.hashover .hashover-embedded-image-wrapper.hashover-loading img {
+ filter: grayscale(1.0);
+ opacity: 0.25;
+ filter: alpha(opacity=25);
+}
+
+.hashover .hashover-embedded-image-wrapper.hashover-loading:before {
+ position: absolute;
+ top: 0px;
+ left: 0px;
+ width: 100%;
+ height: 100%;
+}
+
+.hashover .hashover-more-link.hashover-loading:before,
+.hashover .hashover-thread-link.hashover-loading:before {
+ margin-right: 8px;
+ margin-top: -8px;
+ margin-bottom: -6px;
+}
+
+.hashover .hashover-more-link.hashover-hide-more-link {
+ opacity: 0.0;
+ filter: alpha(opacity=0);
+}
+
+.hashover .hashover-more-link.hashover-hide-more-link,
+.hashover .hashover-message.hashover-message-animated,
+.hashover .hashover-message.hashover-message-animated > *,
+.hashover .hashover-formatting-message.hashover-message-animated,
+.hashover .hashover-formatting-message.hashover-message-animated > * {
+ -o-transition: all 150ms linear 0ms;
+ -webkit-transition: all 150ms linear 0ms;
+ -moz-transition: all 150ms linear 0ms;
+ transition: all 150ms linear 0ms;
+}
+
+.hashover #hashover-end-links {
+ margin-top: -12px;
+ padding: 18px 0px 5px 0px;
+ text-align: center;
+}
+
+/* Handle HDPI */
+.hashover.hashover-mobile .hashover-formatting,
+.hashover.hashover-mobile .hashover-name-input:before,
+.hashover.hashover-mobile .hashover-password-input:before,
+.hashover.hashover-mobile .hashover-email-input:before,
+.hashover.hashover-mobile .hashover-website-input:before,
+.hashover.hashover-mobile .hashover-required-input:after,
+.hashover.hashover-mobile .hashover-comment .hashover-buttons a {
+ background-image: url('../../images/inputs-and-buttons.svg');
+}
+
+@media only screen and (max-width: 640px) {
+ #hashover .hashover-formatting-table {
+ padding: 12px 12px 0px 12px;
+ }
+
+ #hashover .hashover-formatting-table > * {
+ display: table-row;
+ width: 100%;
+ border-bottom: 1px solid #AAAAAA;
+ }
+
+ #hashover .hashover-formatting-table p {
+ margin-bottom: 12px;
+ }
+}
diff --git a/bootstrap/comments/themes/default-borderless/latest.css b/bootstrap/comments/themes/default-borderless/latest.css
new file mode 100644
index 0000000..31cd4cf
--- /dev/null
+++ b/bootstrap/comments/themes/default-borderless/latest.css
@@ -0,0 +1,14 @@
+/*-----------------------------------------------------------------------------
+
+Copyright (C) 2016-2018 Jacob Barkdull
+This file is part of HashOver.
+
+I, Jacob Barkdull, hereby release this work into the public domain.
+This applies worldwide. If this is not legally possible, I grant any
+entity the right to use this work for any purpose, without any
+conditions, unless such conditions are required by law.
+
+-----------------------------------------------------------------------------*/
+
+/* Import the comments theme (optional) */
+@import 'comments.css';
diff --git a/bootstrap/comments/themes/default/comments.css b/bootstrap/comments/themes/default/comments.css
new file mode 100644
index 0000000..234d1ad
--- /dev/null
+++ b/bootstrap/comments/themes/default/comments.css
@@ -0,0 +1,1132 @@
+/*-----------------------------------------------------------------------------
+
+Copyright (C) 2016-2018 Jacob Barkdull
+This file is part of HashOver.
+
+I, Jacob Barkdull, hereby release this work into the public domain.
+This applies worldwide. If this is not legally possible, I grant any
+entity the right to use this work for any purpose, without any
+conditions, unless such conditions are required by law.
+
+-----------------------------------------------------------------------------*/
+
+.hashover,
+.hashover input,
+.hashover textarea,
+.hashover select {
+ text-indent: initial;
+ font-size: 14px;
+ font-family: "Cantarell", "Arial", "Helvetica", "FreeSans", sans-serif;
+ color: #222222;
+}
+
+.hashover > div {
+ display: block;
+}
+
+.hashover :before,
+.hashover :after {
+ position: relative;
+ z-index: 3;
+}
+
+.hashover #hashover-requiredFields,
+.hashover .hashover-hidden,
+.hashover .hashover-notice > .hashover-balloon > .hashover-footer,
+.hashover .hashover-first .hashover-balloon,
+.hashover .hashover-first .hashover-avatar:before,
+.hashover .hashover-first .hashover-avatar:after {
+ display: none ! important;
+}
+
+.hashover,
+#hashover-top-comments,
+#hashover-sort-section,
+.hashover .hashover-inputs {
+ clear: both;
+}
+
+.hashover #hashover-form-section,
+.hashover #hashover-popular-section,
+.hashover #hashover-comments-section {
+ margin-bottom: 36px;
+}
+
+.hashover .hashover-title,
+.hashover #hashover-count,
+.hashover .hashover-comment-name {
+ display: inline-block;
+ font-size: 18px;
+}
+
+.hashover .hashover-reply .hashover-comment-name {
+ font-weight: bold;
+ font-size: 14px;
+}
+
+.hashover form input,
+.hashover form textarea,
+.hashover .hashover-inputs,
+.hashover .hashover-select-wrapper,
+.hashover .hashover-submit,
+.hashover .hashover-avatar-image *,
+.hashover .hashover-avatar *,
+.hashover .hashover-inputs input,
+.hashover .hashover-comment textarea,
+.hashover .hashover-comment,
+.hashover .hashover-reply > .hashover-balloon,
+.hashover .hashover-reply-form,
+.hashover .hashover-reply .hashover-header,
+.hashover .hashover-form-footer {
+ border: 1px solid #AAAAAA;
+}
+
+.hashover .hashover-dashed-title {
+ display: inline-block;
+ width: 100%;
+ margin-bottom: 12px;
+ padding-bottom: 10px;
+ vertical-align: top;
+ border-bottom: 1px dashed rgb(190, 190, 190);
+}
+
+.hashover .hashover-message {
+ position: relative;
+ z-index: 1;
+}
+
+.hashover .hashover-formatting {
+ background-position: 0px -454px;
+}
+
+.hashover .hashover-message,
+.hashover .hashover-message > *,
+.hashover .hashover-formatting-message,
+.hashover .hashover-formatting-message > * {
+ display: block;
+ width: 100%;
+ max-height: 0px;
+}
+
+.hashover .hashover-message > *,
+.hashover .hashover-formatting-message > * {
+ color: transparent;
+}
+
+.hashover .hashover-message > * {
+ padding: 0px 12px;
+ margin-bottom: 12px;
+ text-align: center;
+ border: 1px solid #AAAAAA;
+}
+
+.hashover .hashover-message.hashover-message-open > *,
+.hashover .hashover-formatting-message.hashover-message-open > * {
+ color: initial;
+}
+
+.hashover .hashover-message.hashover-message-open > * {
+ padding: 12px;
+ color: #0000CC;
+ background-color: rgba(225, 225, 255, 0.25);
+ border-color: #00AACC;
+}
+
+.hashover .hashover-php-message-open,
+.hashover .hashover-php-message-open > * {
+ max-height: initial;
+}
+
+.hashover .hashover-message.hashover-message-error > * {
+ color: #CC0000;
+ background-color: rgba(255, 225, 225, 0.25);
+ border-color: #CC0000;
+}
+
+.hashover .hashover-formatting-message > * {
+ margin-top: -1px;
+ border: 1px solid #AAAAAA;
+}
+
+.hashover .hashover-formatting-table {
+ display: table;
+ padding: 12px 0px 12px 0px;
+ background-color: #E5E5E5;
+}
+
+.hashover .hashover-formatting-table > * {
+ display: table-cell;
+ width: 50%;
+ padding: 0px 12px 0px 12px;
+ border-right: 1px solid #AAAAAA;
+ vertical-align: top;
+}
+
+.hashover .hashover-formatting-table > *:last-child {
+ border-right: none;
+}
+
+.hashover .hashover-comment .hashover-message {
+ margin-top: -1px;
+}
+
+.hashover .hashover-comment .hashover-message > * {
+ margin-bottom: 0px;
+}
+
+.hashover #hashover-form,
+.hashover .hashover-comment,
+.hashover .hashover-reply-form,
+.hashover .hashover-message,
+.hashover .hashover-message > *,
+.hashover .hashover-formatting-message,
+.hashover .hashover-formatting-message > *,
+.hashover .hashover-comment form,
+.hashover .hashover-comment form .hashover-balloon {
+ overflow: hidden;
+}
+
+.hashover form,
+.hashover .hashover-comment form {
+ display: block;
+}
+
+.hashover .hashover-inputs {
+ display: table;
+ width: 100%;
+ padding: 12px;
+ border-bottom: none;
+}
+
+.hashover .hashover-inputs > * {
+ display: table-cell;
+ vertical-align: middle;
+ line-height: 16px;
+}
+
+.hashover .hashover-name-input input,
+.hashover .hashover-password-input input,
+.hashover .hashover-email-input input,
+.hashover .hashover-website-input input {
+ background-image: -webkit-linear-gradient(#F0F0F0, #FCFCFC, #FCFCFC, #FCFCFC);
+ background-image: -moz-linear-gradient(#F0F0F0, #FCFCFC, #FCFCFC, #FCFCFC);
+ background-image: linear-gradient(#F0F0F0, #FCFCFC, #FCFCFC, #FCFCFC);
+}
+
+.hashover .hashover-name-input,
+.hashover .hashover-password-input,
+.hashover .hashover-email-input,
+.hashover .hashover-website-input {
+ position: relative;
+}
+
+.hashover .hashover-name-input:before,
+.hashover .hashover-password-input:before,
+.hashover .hashover-email-input:before,
+.hashover .hashover-website-input:before,
+.hashover .hashover-required-input:after {
+ content: " ";
+ display: inline-block;
+ position: absolute;
+ top: 50%;
+ left: 0px;
+ width: 28px;
+ height: 32px;
+ margin-top: -16px;
+ background-image: url('../../images/inputs-and-buttons.png');
+ background-repeat: no-repeat;
+ background-attachment: scroll;
+ border: 1px solid transparent;
+ pointer-events: none;
+}
+
+.hashover .hashover-required-input:after {
+ left: auto;
+ right: 0px;
+ background-position: 0px -419px;
+}
+
+.hashover.hashover-logged-out .hashover-inputs > *,
+.hashover .hashover-comment .hashover-inputs > * {
+ padding: 0px 3px 0px 3px;
+}
+
+.hashover .hashover-comment,
+.hashover .hashover-reply-form,
+.hashover .hashover-comment .hashover-avatar,
+.hashover .hashover-comment .hashover-comment-name,
+.hashover .hashover-avatar-image {
+ position: relative;
+}
+
+.hashover #hashover-form .hashover-avatar-image {
+ width: 45px;
+ padding-right: 10px;
+}
+
+.hashover .hashover-avatar-image *,
+.hashover .hashover-avatar * {
+ vertical-align: middle;
+}
+
+.hashover form input,
+.hashover form textarea,
+.hashover .hashover-inputs input,
+.hashover .hashover-submit {
+ padding: 2px;
+ margin: 0px;
+ background-color: #FCFCFC;
+ outline-offset: none;
+ outline: -webkit-focus-ring-color none;
+}
+
+.hashover form input:focus,
+.hashover form textarea:focus,
+.hashover .hashover-submit:focus,
+.hashover .hashover-inputs input:focus,
+.hashover .hashover-comment label:focus,
+.hashover .hashover-select-wrapper:focus {
+ -o-transition: border 150ms linear 0ms, background-color 150ms linear 0ms, box-shadow 150ms linear 0ms;
+ -webkit-transition: border 150ms linear 0ms, background-color 150ms linear 0ms, box-shadow 150ms linear 0ms;
+ -moz-transition: border 150ms linear 0ms, background-color 150ms linear 0ms, box-shadow 150ms linear 0ms;
+ transition: border 150ms linear 0ms, background-color 150ms linear 0ms, box-shadow 150ms linear 0ms;
+}
+
+.hashover *,
+.hashover *:before,
+.hashover *:after {
+ -o-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+
+.hashover form textarea {
+ position: relative;
+ width: 100%;
+ padding: 12px;
+ vertical-align: top;
+ resize: vertical;
+}
+
+.hashover form textarea:focus {
+ z-index: 2;
+}
+
+.hashover .hashover-comment > .hashover-balloon,
+.hashover .hashover-edit-form,
+.hashover #hashover-end-links,
+.hashover .hashover-border-top {
+ border-top: 1px solid #AAAAAA;
+}
+
+.hashover .hashover-comment > .hashover-balloon {
+ padding: 12px;
+ margin-bottom: -1px;
+ border-bottom: 1px solid #AAAAAA;
+}
+
+.hashover textarea:hover,
+.hashover input:hover,
+.hashover .hashover-submit:hover {
+ background-color: #FCFCFC;
+ border-color: #606060;
+ text-decoration: none;
+}
+
+.hashover input:focus,
+.hashover textarea:focus,
+.hashover .hashover-submit:focus {
+ border-color: #0055FF ! important;
+
+ -o-box-shadow: 0px 0px 2px #BFEFFF ! important;
+ -webkit-box-shadow: 0px 0px 2px #BFEFFF ! important;
+ -moz-box-shadow: 0px 0px 2px #BFEFFF ! important;
+ box-shadow: 0px 0px 2px #BFEFFF ! important;
+}
+
+.hashover .hashover-submit {
+ background-color: #F5F8FC;
+ padding: 6px 10px 6px 10px;
+ margin-left: 5px;
+}
+
+.hashover .hashover-post-button,
+.hashover .hashover-reply-post,
+.hashover .hashover-edit-post {
+ background-color: #8CB1FF;
+ border-color: #0055AA;
+ color: #FCFCFC;
+}
+
+.hashover .hashover-footer *,
+.hashover .hashover-form-footer * {
+ vertical-align: top;
+ line-height: 16px;
+}
+
+.hashover .hashover-post-button:hover,
+.hashover .hashover-reply-post:hover,
+.hashover .hashover-edit-post:hover {
+ background-color: #73A1FF;
+ border-color: #0055AA;
+}
+
+.hashover .hashover-post-button:focus,
+.hashover .hashover-reply-post:focus,
+.hashover .hashover-edit-post:focus {
+ background-color: #6698FF ! important;
+ border-color: #000088 ! important;
+}
+
+.hashover .hashover-edit-delete {
+ float: left;
+ margin-right: 12px;
+ background-color: #FF8C8C;
+ border-color: #AA0000;
+ color: #FCFCFC;
+}
+
+.hashover .hashover-edit-delete:hover {
+ background-color: #FF6666;
+ border-color: #AA0000;
+}
+
+.hashover .hashover-edit-delete:focus {
+ border-color: #880000 ! important;
+ background-color: #FF5959 ! important;
+
+ -o-box-shadow: 0px 0px 2px #FFEFBF ! important;
+ -webkit-box-shadow: 0px 0px 2px #FFEFBF ! important;
+ -moz-box-shadow: 0px 0px 2px #FFEFBF ! important;
+ box-shadow: 0px 0px 2px #FFEFBF ! important;
+}
+
+.hashover .hashover-submit:hover {
+ cursor: pointer;
+}
+
+.hashover .hashover-submit:focus,
+.hashover .hashover-submit[disabled] {
+ border-color: #000000;
+}
+
+.hashover .hashover-submit[disabled] {
+ background-color: #E0E0E0 ! important;
+ color: #222222 ! important;
+
+ -o-box-shadow: none ! important;
+ -webkit-box-shadow: none ! important;
+ -moz-box-shadow: none ! important;
+ box-shadow: none ! important;
+}
+
+.hashover .hashover-inputs input {
+ width: 100%;
+ height: 32px;
+ font-size: 14px;
+ padding-left: 30px;
+ padding-right: 6px;
+ color: #808080;
+}
+
+.hashover .hashover-required-input input {
+ padding-right: 30px;
+}
+
+.hashover .hashover-inputs input:focus {
+ color: #222222;
+}
+
+.hashover .hashover-inputs > :first-child {
+ padding-left: 0px;
+}
+
+.hashover .hashover-inputs > :nth-last-child(1) {
+ padding-right: 0px;
+}
+
+.hashover .hashover-name-input:before {
+ background-position: 0px 1px;
+}
+
+.hashover .hashover-password-input:before {
+ background-position: 0px -27px;
+}
+
+.hashover .hashover-email-input:before {
+ background-position: 0px -55px;
+}
+
+.hashover .hashover-website-input:before {
+ background-position: 0px -83px;
+}
+
+.hashover .hashover-emphasized-input,
+.hashover .hashover-emphasized-input input {
+ border-color: #CC0000 ! important;
+}
+
+.hashover .hashover-emphasized-input:focus,
+.hashover .hashover-emphasized-input input:focus {
+ border-color: #FF0000 ! important;
+
+ -o-box-shadow: 0px 0px 2px #FFBFBF !important;
+ -webkit-box-shadow: 0px 0px 2px #FFBFBF !important;
+ -moz-box-shadow: 0px 0px 2px #FFBFBF !important;
+ box-shadow: 0px 0px 2px #FFBFBF !important;
+}
+
+.hashover form label {
+ cursor: pointer;
+}
+
+.hashover #hashover-form .hashover-comment-label,
+.hashover .hashover-comment-label:nth-child(2) {
+ background-color: #FCFCFC;
+}
+
+.hashover .hashover-inputs label,
+.hashover .hashover-comment-label {
+ padding: 5px;
+ display: inline-block;
+ background-color: rgba(200, 200, 200, 0.25);
+}
+
+.hashover .hashover-comment-label {
+ width: 100%;
+ display: block;
+ border: 1px solid #AAA;
+ margin-bottom: -1px;
+}
+
+.hashover .hashover-form-links {
+ display: inline-block;
+ vertical-align: middle;
+ margin-bottom: 12px;
+}
+
+.hashover .hashover-form-links > * {
+ display: inline-block;
+ margin-top: 7px;
+ margin-right: 6px;
+}
+
+.hashover .hashover-form-links > *:last-child {
+ margin-right: 0px;
+}
+
+.hashover .hashover-form-buttons {
+ margin-bottom: 12px;
+}
+
+.hashover label input[type="checkbox"] {
+ margin: -2px 4px 0px 0px;
+ vertical-align: middle;
+ padding: 0px;
+}
+
+.hashover .hashover-form-footer {
+ padding: 12px 12px 0px 12px;
+ overflow: hidden;
+ margin-top: -1px;
+ background-color: #F0F0F0;
+}
+
+.hashover .hashover-edit-form,
+.hashover .hashover-border-top {
+ margin-top: 12px;
+ padding-top: 12px;
+}
+
+.hashover .hashover-edit-form .hashover-title {
+ margin-top: 3px;
+}
+
+.hashover #hashover-count {
+ float: left;
+}
+
+.hashover #hashover-sort,
+.hashover .hashover-edit-status,
+.hashover .hashover-comment .hashover-footer .hashover-buttons,
+.hashover .hashover-form-buttons,
+.hashover .hashover-thread-link {
+ float: right;
+}
+
+.hashover select {
+ height: 24px;
+}
+
+.hashover hr {
+ border: 0px;
+ height: 1px;
+ background-color: #AAAAAA;
+ margin-top: 0px;
+ margin-bottom: 5px;
+}
+
+.hashover input::-moz-focus-inner {
+ border: 0px;
+}
+
+.hashover a,
+.hashover a:link,
+.hashover .hashover-fake-link {
+ text-decoration: none;
+ color: #0000CC;
+ outline: none;
+ cursor: pointer;
+}
+
+.hashover a:visited {
+ color: #00268F;
+}
+
+.hashover a:hover,
+.hashover .hashover-fake-link:hover {
+ text-decoration: underline;
+ cursor: pointer;
+ color: #005EFF;
+}
+
+.hashover a.hashover-submit {
+ display: inline-block;
+ color: #222222;
+}
+
+.hashover .hashover-reply .hashover-avatar *,
+.hashover .hashover-comment form .hashover-avatar-image * {
+ width: 32px;
+ height: 32px;
+ line-height: 34px;
+}
+
+.hashover .hashover-reply .hashover-avatar a {
+ font-size: 14px;
+}
+
+.hashover #hashover-form,
+.hashover .hashover-comment,
+.hashover .hashover-reply-form {
+ background-color: #F5F5F5;
+}
+
+#hashover-form .hashover-inputs,
+.hashover .hashover-comment > .hashover-header {
+ /* Light blue */
+ background-color: #E5F0FF;
+
+ /* Light green */
+ /*background-color: #E5FFF0;*/
+
+ /* Light red (pink-ish) */
+ /*background-color: #FFE5F0;*/
+}
+
+.hashover .hashover-avatar-image *,
+.hashover .hashover-avatar *,
+.hashover .hashover-reply .hashover-header,
+.hashover .hashover-comment .hashover-balloon,
+.hashover .hashover-reply .hashover-header {
+ background-color: #FCFCFC;
+}
+
+.hashover .hashover-comment,
+.hashover .hashover-reply-form {
+ margin-bottom: 12px;
+}
+
+.hashover .hashover-comment > .hashover-header {
+ padding: 12px;
+}
+
+.hashover .hashover-reply,
+.hashover .hashover-reply-form {
+ border: none;
+ padding: 0px 12px 0px 12px;
+ margin: 13px 0px 12px 0px;
+}
+
+.hashover .hashover-reply .hashover-header {
+ padding: 12px 12px 0px 12px;
+ margin-left: 48px;
+ border-bottom: none;
+}
+
+.hashover .hashover-reply .hashover-reply,
+.hashover .hashover-reply .hashover-reply-form {
+ padding: 0px;
+ margin: 12px 0px 0px 48px;
+}
+
+.hashover .hashover-comment .hashover-avatar {
+ display: inline-block;
+ height: auto;
+ margin-right: 10px;
+}
+
+.hashover .hashover-avatar-image *,
+.hashover .hashover-avatar *,
+.hashover .hashover-avatar a {
+ display: inline-block;
+ width: 45px;
+ height: 45px;
+ font-size: 18px;
+ text-align: center;
+ line-height: 45px;
+ color: #808080;
+ background-position: center center;
+ background-attachment: scroll;
+ background-repeat: no-repeat;
+
+ -o-background-size: 100% 100%;
+ -webkit-background-size: 100% 100%;
+ -moz-background-size: 100% 100%;
+ background-size: 100% 100%;
+
+ -o-box-sizing: content-box;
+ -webkit-box-sizing: content-box;
+ -moz-box-sizing: content-box;
+ box-sizing: content-box;
+}
+
+.hashover .hashover-reply .hashover-avatar,
+.hashover .hashover-comment form .hashover-avatar-image {
+ position: absolute;
+ display: inline-block;
+ top: 0px;
+ left: 12px;
+ padding: 0px;
+ float: left;
+}
+
+.hashover .hashover-reply .hashover-reply .hashover-avatar,
+.hashover .hashover-reply form .hashover-avatar-image {
+ left: 0px;
+}
+
+.hashover .hashover-comment .hashover-content {
+ line-height: 1.5em;
+ margin-bottom: 12px;
+}
+
+.hashover p {
+ margin: 0px 0px 12px 0px;
+}
+
+.hashover p:last-child {
+ margin-bottom: 0px;
+}
+
+.hashover .hashover-reply > .hashover-balloon,
+.hashover .hashover-reply-form .hashover-balloon {
+ margin-left: 48px;
+ margin-bottom: 1px;
+ border-top: none;
+}
+
+.hashover .hashover-comment .hashover-content img {
+ display: block;
+ max-width: 100%;
+ max-height: 640px;
+ cursor: pointer;
+}
+
+.hashover .hashover-avatar-image:before,
+.hashover .hashover-avatar-image:after,
+.hashover .hashover-comment .hashover-avatar:before,
+.hashover .hashover-comment .hashover-avatar:after {
+ content: " ";
+ position: absolute;
+ display: block;
+ bottom: -13px;
+ left: 50%;
+ margin-left: -10px;
+ width: 0px;
+ height: 0px;
+ border-width: 10px;
+ border-color: transparent;
+ border-style: solid outset solid;
+ pointer-events: none;
+}
+
+.hashover .hashover-avatar-image:before,
+.hashover .hashover-avatar-image:after {
+ margin-left: -15px;
+}
+
+.hashover .hashover-avatar-image:before,
+.hashover .hashover-comment .hashover-avatar:before,
+.hashover .hashover-comment form .hashover-avatar-image:before {
+ border-bottom-color: #AAAAAA;
+}
+
+.hashover .hashover-avatar-image:after,
+.hashover .hashover-comment .hashover-avatar:after,
+.hashover .hashover-comment form .hashover-avatar-image:after {
+ border-bottom-color: #FCFCFC;
+ bottom: -14px;
+}
+
+.hashover .hashover-reply .hashover-avatar:before,
+.hashover .hashover-reply .hashover-avatar:after,
+.hashover .hashover-comment form .hashover-avatar-image:before,
+.hashover .hashover-comment form .hashover-avatar-image:after {
+ border-style: solid solid outset;
+ border-color: transparent;
+ margin-left: auto;
+ margin-bottom: -8px;
+ top: auto;
+ left: auto;
+ bottom: 50%;
+ right: -15px;
+ border-width: 8px;
+}
+
+.hashover .hashover-reply .hashover-avatar:before,
+.hashover .hashover-comment form .hashover-avatar-image:before {
+ border-right-color: #AAAAAA;
+}
+
+.hashover .hashover-reply .hashover-avatar:after,
+.hashover .hashover-comment form .hashover-avatar-image:after {
+ border-right-color: #FCFCFC;
+ right: -16px;
+}
+
+.hashover.hashover-logged-out .hashover-comment form .hashover-avatar-image:after {
+ border-right-color: #FCFCFC;
+}
+
+.hashover .hashover-select-wrapper select {
+ border: none;
+ background-color: #FCFCFC;
+ padding: 0px 14px 0px 0px;
+ cursor: pointer;
+}
+
+.hashover .hashover-select-wrapper,
+.hashover .hashover-embedded-image-wrapper {
+ display: inline-block;
+ position: relative;
+}
+
+.hashover .hashover-select-wrapper {
+ overflow: hidden;
+ margin-top: -3px;
+ line-height: 14px;
+ vertical-align: middle;
+ cursor: pointer;
+}
+
+.hashover .hashover-select-wrapper:before {
+ content: "\25BC";
+ position: absolute;
+ right: 0px;
+ top: 0px;
+ display: inline-block;
+ height: 100%;
+ width: 26px;
+ padding: 5px 4px;
+ margin: 0px;
+ font-size: 12px;
+ line-height: 12px;
+ font-family: monospace;
+ text-align: center;
+ pointer-events: none;
+ background-color: #F0F0F0;
+ border-left: 1px solid #AAAAAA;
+}
+
+.hashover .hashover-select-wrapper:hover,
+.hashover .hashover-select-wrapper:hover:before {
+ border-color: #606060;
+}
+
+.hashover .hashover-comment-name {
+ vertical-align: middle;
+}
+
+.hashover .hashover-name-twitter:before {
+ content: "@";
+ color: #00268F;
+ cursor: default;
+}
+
+.hashover .hashover-comment .hashover-title {
+ display: inline-block;
+ vertical-align: middle;
+ clear: right;
+}
+
+.hashover .hashover-comment pre,
+.hashover .hashover-comment code,
+.hashover .hashover-comment blockquote,
+.hashover .hashover-comment ol,
+.hashover .hashover-comment ul {
+ vertical-align: top;
+}
+
+.hashover .hashover-comment pre,
+.hashover .hashover-comment code {
+ display: inline-block;
+ width: 100%;
+ max-height: 400px;
+ white-space: pre;
+ padding: 5px;
+ margin: 0px;
+ font-family: monospace;
+ font-size: 12px;
+ background-color: #EEEEEE;
+ overflow: auto;
+}
+
+.hashover code.hashover-inline {
+ display: inline;
+ padding: 1px 4px;
+}
+
+.hashover .hashover-comment blockquote,
+.hashover .hashover-comment ol,
+.hashover .hashover-comment ul {
+ padding-left: 10px;
+ margin: 0px 24px 0px 24px;
+}
+
+.hashover .hashover-comment blockquote {
+ border-left: 3px solid rgba(0, 0, 0, 0.3);
+}
+
+.hashover .hashover-comment .hashover-date-permalink,
+.hashover .hashover-comment .hashover-date-permalink:visited,
+.hashover .hashover-comment .hashover-date > * {
+ color: #606060;
+}
+
+.hashover .hashover-comment .hashover-date-permalink:hover,
+.hashover .hashover-comment .hashover-date-permalink:active {
+ color: #101010;
+}
+
+.hashover .hashover-comment .hashover-date,
+.hashover .hashover-comment .hashover-date-permalink,
+.hashover .hashover-comment .hashover-replies,
+.hashover .hashover-comment .hashover-likes,
+.hashover .hashover-comment .hashover-dislikes {
+ display: inline-block;
+ vertical-align: top;
+}
+
+.hashover .hashover-comment .hashover-date-permalink,
+.hashover .hashover-comment .hashover-replies,
+.hashover .hashover-comment .hashover-likes,
+.hashover .hashover-comment .hashover-dislikes {
+ float: left;
+}
+
+.hashover .hashover-comment .hashover-replies,
+.hashover .hashover-comment .hashover-likes,
+.hashover .hashover-comment .hashover-dislikes {
+ padding-left: 8px;
+}
+
+.hashover .hashover-formatting,
+.hashover .hashover-comment .hashover-buttons a {
+ display: inline-block;
+ vertical-align: top;
+ min-height: 16px;
+ padding-left: 30px;
+ line-height: 16px;
+ background-image: url('../../images/inputs-and-buttons.png');
+ background-repeat: no-repeat;
+ background-attachment: scroll;
+ opacity: 0.50;
+ filter: alpha(opacity=50);
+}
+
+.hashover .hashover-formatting:hover,
+.hashover .hashover-comment .hashover-buttons a:hover {
+ opacity: 1.0;
+ filter: alpha(opacity=100);
+}
+
+.hashover .hashover-formatting,
+.hashover .hashover-formatting:hover,
+.hashover .hashover-comment .hashover-buttons a,
+.hashover .hashover-comment .hashover-buttons a:hover {
+ color: #111111;
+}
+
+.hashover .hashover-has-email {
+ background-position: left -258px;
+}
+
+.hashover .hashover-no-email {
+ background-position: left -286px;
+}
+
+.hashover .hashover-comment .hashover-like,
+.hashover .hashover-comment .hashover-liked:active,
+.hashover .hashover-comment .hashover-dislike {
+ background-position: left -174px;
+}
+
+.hashover .hashover-comment .hashover-liked,
+.hashover .hashover-comment .hashover-like:active {
+ background-position: left -202px;
+}
+
+.hashover .hashover-comment .hashover-like.hashover-dislikes-enabled {
+ background-position: left -314px;
+}
+
+.hashover .hashover-comment .hashover-liked.hashover-dislikes-enabled,
+.hashover .hashover-comment .hashover-like.hashover-dislikes-enabled:active {
+ background-position: left -370px;
+}
+
+.hashover .hashover-comment .hashover-disliked,
+.hashover .hashover-comment .hashover-dislike:active {
+ background-position: left -230px;
+}
+
+.hashover .hashover-comment .hashover-dislike.hashover-likes-enabled {
+ background-position: left -342px;
+}
+
+.hashover .hashover-comment .hashover-disliked.hashover-likes-enabled,
+.hashover .hashover-comment .hashover-dislike.hashover-likes-enabled:active {
+ background-position: left -398px;
+}
+
+.hashover .hashover-comment .hashover-comment-edit {
+ background-position: 2px -147px;
+}
+
+.hashover .hashover-comment .hashover-like.hashover-dislikes-enabled,
+.hashover .hashover-comment .hashover-liked.hashover-dislikes-enabled,
+.hashover .hashover-comment .hashover-dislike.hashover-likes-enabled,
+.hashover .hashover-comment .hashover-disliked.hashover-likes-enabled {
+ height: 16px;
+ font-size: 0px;
+ color: transparent;
+}
+
+.hashover .hashover-deleted > .hashover-header > .hashover-comment-name {
+ color: #FF0000;
+}
+
+.hashover .hashover-notice > .hashover-balloon > .hashover-content {
+ margin: 0px;
+}
+
+.hashover .hashover-more-link {
+ display: block;
+ padding: 12px;
+ margin-bottom: 12px;
+ background-color: rgba(230, 237, 250, 0.6);
+ border: 1px dashed #AAAAFF;
+ text-align: center;
+ opacity: 1.0;
+ filter: alpha(opacity=100);
+}
+
+.hashover .hashover-more-link,
+.hashover a.hashover-more-link {
+ color: #404040;
+}
+
+.hashover .hashover-more-link:hover {
+ background-color: rgba(230, 237, 250, 1.0);
+}
+
+.hashover .hashover-loading:before {
+ content: " " ! important;
+ display: inline-block;
+ min-width: 16px;
+ min-height: 16px;
+ vertical-align: middle;
+ background-repeat: no-repeat;
+ background-attachment: scroll;
+ background-position: center center;
+ background-image: url('../../images/loading.gif');
+
+ image-rendering: -o-crisp-edges;
+ image-rendering: -webkit-optimize-contrast;
+ image-rendering: -moz-crisp-edges;
+ image-rendering: crisp-edges;
+ image-rendering: pixelated;
+ -ms-interpolation-mode: nearest-neighbor;
+}
+
+.hashover .hashover-embedded-image-wrapper.hashover-loading img {
+ filter: grayscale(1.0);
+ opacity: 0.25;
+ filter: alpha(opacity=25);
+}
+
+.hashover .hashover-embedded-image-wrapper.hashover-loading:before {
+ position: absolute;
+ top: 0px;
+ left: 0px;
+ width: 100%;
+ height: 100%;
+}
+
+.hashover .hashover-more-link.hashover-loading:before,
+.hashover .hashover-thread-link.hashover-loading:before {
+ margin-right: 8px;
+ margin-top: -8px;
+ margin-bottom: -6px;
+}
+
+.hashover .hashover-more-link.hashover-hide-more-link {
+ opacity: 0.0;
+ filter: alpha(opacity=0);
+}
+
+.hashover .hashover-more-link.hashover-hide-more-link,
+.hashover .hashover-message.hashover-message-animated,
+.hashover .hashover-message.hashover-message-animated > *,
+.hashover .hashover-formatting-message.hashover-message-animated,
+.hashover .hashover-formatting-message.hashover-message-animated > * {
+ -o-transition: all 150ms linear 0ms;
+ -webkit-transition: all 150ms linear 0ms;
+ -moz-transition: all 150ms linear 0ms;
+ transition: all 150ms linear 0ms;
+}
+
+.hashover #hashover-end-links {
+ margin-top: -12px;
+ padding: 18px 0px 5px 0px;
+ text-align: center;
+}
+
+/* Handle HDPI */
+.hashover.hashover-mobile .hashover-formatting,
+.hashover.hashover-mobile .hashover-name-input:before,
+.hashover.hashover-mobile .hashover-password-input:before,
+.hashover.hashover-mobile .hashover-email-input:before,
+.hashover.hashover-mobile .hashover-website-input:before,
+.hashover.hashover-mobile .hashover-required-input:after,
+.hashover.hashover-mobile .hashover-comment .hashover-buttons a {
+ background-image: url('../../images/inputs-and-buttons.svg');
+}
+
+@media only screen and (max-width: 640px) {
+ #hashover .hashover-formatting-table {
+ padding: 12px 12px 0px 12px;
+ }
+
+ #hashover .hashover-formatting-table > * {
+ display: table-row;
+ width: 100%;
+ border-bottom: 1px solid #AAAAAA;
+ }
+
+ #hashover .hashover-formatting-table p {
+ margin-bottom: 12px;
+ }
+}
diff --git a/bootstrap/comments/themes/default/comments.html b/bootstrap/comments/themes/default/comments.html
new file mode 100644
index 0000000..f9abc14
--- /dev/null
+++ b/bootstrap/comments/themes/default/comments.html
@@ -0,0 +1,29 @@
+<div class="hashover-header">
+ {hashover:avatar}{hashover:name}
+ {hashover:parent-link}
+</div>
+
+<div class="hashover-balloon">
+ <div id="hashover-content-{hashover:permalink}" class="hashover-content">
+ {hashover:comment}
+ </div>
+
+ <div class="hashover-footer">
+ <span class="hashover-date">
+ {hashover:date}
+ {hashover:like-count}
+ {hashover:dislike-count}
+ </span>
+
+ <span class="hashover-buttons">
+ {hashover:like-link}
+ {hashover:dislike-link}
+ {hashover:edit-link}
+ {hashover:reply-link}
+ </span>
+ </div>
+
+ {placeholder:edit-form}
+</div>
+
+{placeholder:reply-form}
diff --git a/bootstrap/comments/themes/default/general.css b/bootstrap/comments/themes/default/general.css
new file mode 100644
index 0000000..ec23a47
--- /dev/null
+++ b/bootstrap/comments/themes/default/general.css
@@ -0,0 +1,125 @@
+/*-----------------------------------------------------------------------------
+
+Copyright (C) 2016-2018 Jacob Barkdull
+This file is part of HashOver.
+
+I, Jacob Barkdull, hereby release this work into the public domain.
+This applies worldwide. If this is not legally possible, I grant any
+entity the right to use this work for any purpose, without any
+conditions, unless such conditions are required by law.
+
+-----------------------------------------------------------------------------*/
+
+body {
+ margin: 0px;
+ padding: 15px;
+ cursor: default;
+}
+
+body, td, textarea, input, select {
+ font-family: "Cantarell","Arial","Helvetica","FreeSans",sans-serif;
+ color: #000000;
+ font-size: 16px;
+}
+
+*, :after, :before {
+ -moz-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+ -o-box-sizing: border-box;
+ box-sizing: border-box;
+}
+
+body, a, a:link {
+ text-decoration: none;
+ text-transform: none;
+}
+
+a, a:link {
+ color: #3485C7;
+}
+
+input:focus, textarea:focus, .animate-all {
+ -moz-transition: all 150ms linear 0s;
+ -webkit-transition: all 150ms linear 0s;
+ -o-transition: all 150ms linear 0s;
+ transition: all 150ms linear 0s;
+}
+
+a:visited {
+ color: #3485C7;
+}
+
+a:hover {
+ color: #266394;
+ cursor: pointer;
+}
+
+h1 {
+ font-size: 28px;
+ font-weight: normal;
+ line-height: 1.25em;
+}
+
+h1, h2 {
+ margin: 0px;
+ font-weight: normal;
+ overflow: hidden;
+}
+
+.underlined {
+ display: block;
+ padding-bottom: 0.5em;
+ margin-bottom: 0.5em;
+ border-bottom: 1px solid #CCCCCC;
+}
+
+.muted-text {
+ color: #808080;
+}
+
+.striped-rows-odd tr:nth-child(odd) {
+ background-color: #F5F5F5;
+}
+
+.striped-rows-even tr:nth-child(even) {
+ background-color: #F5F5F5;
+}
+
+.column-borders td {
+ border-right: 1px solid #CCCCCC;
+}
+
+.striped-rows-even.column-borders tr:first-child > td,
+.column-borders td:last-child {
+ border: none;
+}
+
+p, .p-spaced {
+ margin: 0px 0px 1em 0px;
+ padding: 0px;
+ overflow: hidden;
+}
+
+p:last-child {
+ margin-bottom: 0px;
+}
+
+table {
+ width: 100%;
+}
+
+.left {
+ float: left;
+}
+
+.right {
+ float: right;
+}
+
+.margin-left-children > * {
+ margin-left: 10px;
+}
+
+.margin-right-children > * {
+ margin-right: 10px;
+}
diff --git a/bootstrap/comments/themes/default/latest.css b/bootstrap/comments/themes/default/latest.css
new file mode 100644
index 0000000..31cd4cf
--- /dev/null
+++ b/bootstrap/comments/themes/default/latest.css
@@ -0,0 +1,14 @@
+/*-----------------------------------------------------------------------------
+
+Copyright (C) 2016-2018 Jacob Barkdull
+This file is part of HashOver.
+
+I, Jacob Barkdull, hereby release this work into the public domain.
+This applies worldwide. If this is not legally possible, I grant any
+entity the right to use this work for any purpose, without any
+conditions, unless such conditions are required by law.
+
+-----------------------------------------------------------------------------*/
+
+/* Import the comments theme (optional) */
+@import 'comments.css';
diff --git a/bootstrap/comments/themes/default/latest.html b/bootstrap/comments/themes/default/latest.html
new file mode 100644
index 0000000..a6b498c
--- /dev/null
+++ b/bootstrap/comments/themes/default/latest.html
@@ -0,0 +1,14 @@
+<div class="hashover-header">
+ {hashover:avatar}{hashover:name}
+</div>
+
+<div class="hashover-balloon">
+ <div id="hashover-content-{hashover:permalink}" class="hashover-content">
+ {hashover:comment}
+ </div>
+
+ <div class="hashover-footer hashover-border-top">
+ <p><span class="hashover-date">{hashover:thread-link}</span></p>
+ <p><span class="hashover-date">{hashover:date}</span></p>
+ </div>
+</div>
diff --git a/bootstrap/comments/themes/default/special.css b/bootstrap/comments/themes/default/special.css
new file mode 100644
index 0000000..2b3cc1d
--- /dev/null
+++ b/bootstrap/comments/themes/default/special.css
@@ -0,0 +1,160 @@
+/*-----------------------------------------------------------------------------
+
+Copyright (C) 2016-2018 Jacob Barkdull
+This file is part of HashOver.
+
+I, Jacob Barkdull, hereby release this work into the public domain.
+This applies worldwide. If this is not legally possible, I grant any
+entity the right to use this work for any purpose, without any
+conditions, unless such conditions are required by law.
+
+-----------------------------------------------------------------------------*/
+
+body * {
+ line-height: 1.25em;
+}
+
+a, a:link {
+ display: inline-block;
+ border-bottom: 0.1em solid transparent;
+ margin-bottom: -0.1em;
+}
+
+a:hover {
+ border-bottom-color: #266394;
+}
+
+input, textarea {
+ padding: 3px 5px 3px 5px;
+ border: 1px solid #909090;
+ background-color: #FCFCFC;
+}
+
+input, textarea, .select-wrapper {
+ margin: 0px 0px 5px 0px;
+ vertical-align: middle;
+}
+
+input:hover, textarea:hover {
+ border-color: #000000;
+}
+
+input[type="text"]:focus,
+input[type="password"]:focus,
+input[type="number"]:focus,
+input[type="button"]:focus,
+input[type="submit"]:focus,
+textarea:focus {
+ -o-box-shadow: 0px 0px 2px rgba(0, 191, 255, 0.5);
+ -moz-box-shadow: 0px 0px 2px rgba(0, 191, 255, 0.5);
+ -webkit-box-shadow: 0px 0px 2px rgba(0, 191, 255, 0.5);
+ box-shadow: 0px 0px 2px rgba(0, 191, 255, 0.5);
+}
+
+input[type="text"]:focus {
+ border-color: #004799;
+}
+
+input[type="text"]:focus,
+input[type="password"]:focus,
+input[type="number"]:focus,
+input[type="button"]:focus,
+textarea:focus {
+ border-color: #3B96E0;
+}
+
+input[type="button"] {
+ padding: 5px 10px 5px 10px;
+ background-color: #F5F5F5;
+}
+
+input[type="button"]:hover {
+ background-color: #F0F0F0;
+}
+
+input[type="submit"] {
+ padding: 5px 15px 5px 15px;
+ color: #FFFFFF;
+ border: 1px solid #326BAC;
+ background-color: #41A7FA;
+}
+
+input[type="submit"]:hover {
+ text-decoration: none;
+ border: 1px solid #004799;
+ background-color: #3B96E0;
+}
+
+input[type="submit"],
+input[type="button"] {
+ margin: 0px;
+ cursor: pointer;
+}
+
+input[type="checkbox"] {
+ margin-top: -3px;
+}
+
+.select-wrapper {
+ display: inline-block;
+ position: relative;
+ overflow: hidden;
+ border: 1px solid #AAAAAA;
+ cursor: pointer;
+}
+
+.select-wrapper:before {
+ content: "\25BC";
+ position: absolute;
+ right: 0px;
+ top: 0px;
+ display: inline-block;
+ height: 100%;
+ width: 31px;
+ padding: 6px 0px 6px 0px;
+ margin: 0px;
+ line-height: 12px;
+ text-align: center;
+ pointer-events: none;
+ background-color: #F0F0F0;
+ border-left: 1px solid #AAAAAA;
+ font-family: monospace;
+ font-size: 14px;
+}
+
+.select-wrapper:hover,
+.select-wrapper:hover:before {
+ border-color: #000000;
+}
+
+.select-wrapper select {
+ border: none;
+ background-color: #FCFCFC;
+ padding: 1px 20px 1px 1px;
+ cursor: pointer;
+}
+
+#message {
+ position: relative;
+ font-weight: bold;
+ background-color: #FFFFFF;
+ margin-top: -1.25em;
+ z-index: 1;
+
+ -moz-transition: color 500ms linear 0ms, background 500ms linear 500ms;
+ -webkit-transition: color 500ms linear 0ms, background 500ms linear 500ms;
+ transition: color 500ms linear 0ms, background 500ms linear 500ms;
+}
+
+#message.success {
+ color: #00AA00;
+}
+
+#message.error {
+ color: #CC0000;
+}
+
+#message.hide {
+ background-color: transparent;
+ color: transparent;
+}
diff --git a/bootstrap/database/Connection.php b/bootstrap/database/Connection.php
new file mode 100644
index 0000000..deec2c6
--- /dev/null
+++ b/bootstrap/database/Connection.php
@@ -0,0 +1,18 @@
+<?php
+
+class Connection
+{
+ public static function make($config)
+ {
+ try {
+ return new PDO(
+ $config['connection'].';dbname='.$config['name'],
+ $config['username'],
+ $config['password'],
+ $config['options']
+ );
+ } catch (PDOException $e) {
+ error_log($e->getMessage());
+ }
+ }
+}
diff --git a/bootstrap/database/QueryBuilder.php b/bootstrap/database/QueryBuilder.php
new file mode 100644
index 0000000..a814ed3
--- /dev/null
+++ b/bootstrap/database/QueryBuilder.php
@@ -0,0 +1,31 @@
+<?php
+
+class QueryBuilder
+{
+ protected $pdo;
+
+ public function __construct($pdo)
+ {
+ $this->pdo = $pdo;
+ }
+
+ public function selectAll($table, $intoClass)
+ {
+ $statement = $this->pdo->prepare("select * from {$table}");
+ $statement->execute();
+ return $statement->fetchAll(PDO::FETCH_CLASS, $intoClass);
+ }
+
+ public function insert($table, $data)
+ {
+ $sql = sprintf(
+ 'insert into %s (%s) values (%s)',
+ $table,
+ implode(', ', array_keys($data)),
+ ':' . implode(', :', array_keys($data))
+ );
+
+ $statment = $this->pdo->prepare($sql);
+ $statment->execute($data);
+ }
+}