aboutsummaryrefslogtreecommitdiff
path: root/bootstrap/comments/backend
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/comments/backend
downloadedwinmattiacci.com-2659205908bd5cab508f4ff817123673e078ab74.tar.gz
edwinmattiacci.com-2659205908bd5cab508f4ff817123673e078ab74.tar.bz2
edwinmattiacci.com-2659205908bd5cab508f4ff817123673e078ab74.zip
Initialize Repo: First Commit
Diffstat (limited to 'bootstrap/comments/backend')
-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
60 files changed, 12815 insertions, 0 deletions
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);