summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJonas Kohl2024-10-17 10:56:01 +0200
committerJonas Kohl2024-10-17 10:56:01 +0200
commitfe0f414dc0211a4014581dc03fcfd514ed7ed02d (patch)
treecd86fc00cd9b7a97eabb9668e0a39e2b4b3e5357
parente0e89b9fdbf301e0ead944636023947a67aca57d (diff)
Transition templating to Twig
-rw-r--r--src/application/actions/_default/get.php10
-rw-r--r--src/application/actions/auth/get.php9
-rw-r--r--src/application/actions/deletepost/post.php21
-rw-r--r--src/application/actions/deletetopic/post.php7
-rw-r--r--src/application/actions/newtopic/get.php9
-rw-r--r--src/application/actions/pwreset/get.php16
-rw-r--r--src/application/actions/register/get.php9
-rw-r--r--src/application/actions/search/get.php19
-rw-r--r--src/application/actions/viewtopic/get.php37
-rw-r--r--src/application/actions/viewuser/get.php15
-rw-r--r--src/application/messages/de.msg5
-rw-r--r--src/application/mystic/forum/utils/RequestUtils.php13
-rw-r--r--src/application/templates/bootstrap-3/base.twig250
-rw-r--r--src/application/templates/bootstrap-3/components/alert_error.twig8
-rw-r--r--src/application/templates/bootstrap-3/components/alert_info.twig8
-rw-r--r--src/application/templates/bootstrap-3/components/alert_success.twig8
-rw-r--r--src/application/templates/bootstrap-3/components/post.twig168
-rw-r--r--src/application/templates/bootstrap-3/components/topic_log.twig62
-rw-r--r--src/application/templates/bootstrap-3/delete_post.twig36
-rw-r--r--src/application/templates/bootstrap-3/delete_topic.twig32
-rw-r--r--src/application/templates/bootstrap-3/error_page.twig12
-rw-r--r--src/application/templates/bootstrap-3/info_page.twig12
-rw-r--r--src/application/templates/bootstrap-3/login.twig50
-rw-r--r--src/application/templates/bootstrap-3/new_password.twig42
-rw-r--r--src/application/templates/bootstrap-3/new_topic.twig58
-rw-r--r--src/application/templates/bootstrap-3/password_reset.twig42
-rw-r--r--src/application/templates/bootstrap-3/register.twig89
-rw-r--r--src/application/templates/bootstrap-3/search.twig63
-rw-r--r--src/application/templates/bootstrap-3/view_topic.twig353
-rw-r--r--src/application/templates/bootstrap-3/view_topics.twig25
-rw-r--r--src/application/templates/bootstrap-3/view_user.twig201
-rw-r--r--src/application/views/alert_error.php10
-rw-r--r--src/application/views/alert_info.php9
-rw-r--r--src/application/views/alert_success.php9
-rw-r--r--src/application/views/form_addpost.php53
-rw-r--r--src/application/views/form_delete_post_confirm.php26
-rw-r--r--src/application/views/form_delete_topic_confirm.php24
-rw-r--r--src/application/views/form_login.php51
-rw-r--r--src/application/views/form_new_password.php38
-rw-r--r--src/application/views/form_newtopic.php60
-rw-r--r--src/application/views/form_password_reset.php41
-rw-r--r--src/application/views/form_register.php88
-rw-r--r--src/application/views/form_search.php30
-rw-r--r--src/application/views/nav_guest.php23
-rw-r--r--src/application/views/nav_logged_in.php12
-rw-r--r--src/application/views/template_end.php165
-rw-r--r--src/application/views/template_navigation.php9
-rw-r--r--src/application/views/template_navigation_end.php4
-rw-r--r--src/application/views/template_navigation_start.php15
-rw-r--r--src/application/views/template_start.php30
-rw-r--r--src/application/views/view_logintoreply.php7
-rw-r--r--src/application/views/view_post.php150
-rw-r--r--src/application/views/view_search_results.php35
-rw-r--r--src/application/views/view_topic_end.php0
-rw-r--r--src/application/views/view_topic_locked.php4
-rw-r--r--src/application/views/view_topic_start.php254
-rw-r--r--src/application/views/view_topiclog.php67
-rw-r--r--src/application/views/view_topics.php24
-rw-r--r--src/application/views/view_user.php209
-rw-r--r--src/composer.json3
-rw-r--r--src/composer.lock236
-rw-r--r--src/index.php104
62 files changed, 1887 insertions, 1592 deletions
diff --git a/src/application/actions/_default/get.php b/src/application/actions/_default/get.php
index cd0c21c..616589f 100644
--- a/src/application/actions/_default/get.php
+++ b/src/application/actions/_default/get.php
@@ -1,11 +1,7 @@
<?php
use mystic\forum\orm\Topic;
-use mystic\forum\utils\RequestUtils;
-_view("template_start");
-_view("template_navigation_start");
-_view("template_navigation", ["user" => RequestUtils::getAuthorizedUser($db)]);
-_view("template_navigation_end");
-_view("view_topics", ["topics" => $db->fetchCustom(Topic::class, "ORDER BY creation_date DESC")]);
-_view("template_end", [...getThemeAndLangInfo()]);
+render("view_topics.twig", [
+ "topics" => $db->fetchCustom(Topic::class, "ORDER BY creation_date DESC"),
+]);
diff --git a/src/application/actions/auth/get.php b/src/application/actions/auth/get.php
index 2ff38ff..dfeb837 100644
--- a/src/application/actions/auth/get.php
+++ b/src/application/actions/auth/get.php
@@ -1,10 +1,3 @@
<?php
-use mystic\forum\utils\RequestUtils;
-
-_view("template_start", ["_title" => __("Log in")]);
-_view("template_navigation_start");
-_view("template_navigation", ["user" => RequestUtils::getAuthorizedUser($db)]);
-_view("template_navigation_end");
-_view("form_login");
-_view("template_end", [...getThemeAndLangInfo()]);
+render("login.twig");
diff --git a/src/application/actions/deletepost/post.php b/src/application/actions/deletepost/post.php
index b711021..4cd9872 100644
--- a/src/application/actions/deletepost/post.php
+++ b/src/application/actions/deletepost/post.php
@@ -24,11 +24,11 @@ if (!$db->fetch($item) || $item->deleted) {
exit;
}
-$topicAuthor = new User();
-$topicAuthor->id = $item->authorId;
+$postAuthor = new User();
+$postAuthor->id = $item->authorId;
-if (!$db->fetch($topicAuthor))
- $topicAuthor = null;
+if (!$db->fetch($postAuthor))
+ $postAuthor = null;
$topic = new Topic();
$topic->id = $item->topicId;
@@ -36,10 +36,10 @@ $topic->id = $item->topicId;
if (!$db->fetch($topic))
$topic = null;
-$canEdit = ($currentUser->id === $topicAuthor?->id && $topicAuthor?->hasPermission(UserPermissions::DELETE_OWN_POST))
+$canDelete = ($currentUser->id === $postAuthor?->id && $postAuthor?->hasPermission(UserPermissions::DELETE_OWN_POST))
|| ($currentUser->hasPermission(UserPermissions::DELETE_OTHER_POST));
-if (!$canEdit) {
+if (!$canDelete) {
http_response_code(403);
msg_error("You don't have permission to delete this post");
exit;
@@ -75,16 +75,11 @@ if ($confirm !== null) {
header("Location: ?_action=viewtopic&topic=" . urlencode($item->topicId));
} else {
- _view("template_start", ["_title" => __("Delete post")]);
- _view("template_navigation_start");
- _view("template_navigation", ["user" => RequestUtils::getAuthorizedUser($db)]);
- _view("template_navigation_end");
- _view("form_delete_post_confirm", [
+ render("delete_post.twig", [
"post" => $item,
- "postAuthor" => $topicAuthor,
+ "postAuthor" => $postAuthor,
"topicAuthor" => null,
"attachments" => $attachments,
"topic" => $topic,
]);
- _view("template_end", [...getThemeAndLangInfo()]);
}
diff --git a/src/application/actions/deletetopic/post.php b/src/application/actions/deletetopic/post.php
index e67cadf..8683c0f 100644
--- a/src/application/actions/deletetopic/post.php
+++ b/src/application/actions/deletetopic/post.php
@@ -55,13 +55,8 @@ if ($confirm !== null) {
header("Location: .");
} else {
- _view("template_start", ["_title" => "Delete topic"]);
- _view("template_navigation_start");
- _view("template_navigation", ["user" => RequestUtils::getAuthorizedUser($db)]);
- _view("template_navigation_end");
- _view("form_delete_topic_confirm", [
+ render("delete_topic.twig", [
"topic" => $topic,
"topicAuthor" => $topicAuthor,
]);
- _view("template_end", [...getThemeAndLangInfo()]);
}
diff --git a/src/application/actions/newtopic/get.php b/src/application/actions/newtopic/get.php
index 366caac..621cd18 100644
--- a/src/application/actions/newtopic/get.php
+++ b/src/application/actions/newtopic/get.php
@@ -1,10 +1,3 @@
<?php
-use mystic\forum\utils\RequestUtils;
-
-_view("template_start", ["_title" => __("New topic")]);
-_view("template_navigation_start");
-_view("template_navigation", ["user" => RequestUtils::getAuthorizedUser($db)]);
-_view("template_navigation_end");
-_view("form_newtopic");
-_view("template_end", [...getThemeAndLangInfo()]);
+render("new_topic.twig");
diff --git a/src/application/actions/pwreset/get.php b/src/application/actions/pwreset/get.php
index c66b28d..35b93f0 100644
--- a/src/application/actions/pwreset/get.php
+++ b/src/application/actions/pwreset/get.php
@@ -11,19 +11,7 @@ if ($token !== null && $signature !== null) {
exit;
}
- _view("template_start", [ "_title" => __("Reset password") ]);
- _view("template_navigation_start");
- _view("template_navigation_end");
- _view("form_new_password", [
- "token" => $token,
- "signature" => $signature,
- ]);
- _view("template_end", [...getThemeAndLangInfo()]);
+ render("new_password.twig");
} else {
- _view("template_start", [ "_title" => __("Reset password") ]);
- _view("template_navigation_start");
- _view("template_navigation", ["user" => null]);
- _view("template_navigation_end");
- _view("form_password_reset");
- _view("template_end", [...getThemeAndLangInfo()]);
+ render("password_reset.twig");
}
diff --git a/src/application/actions/register/get.php b/src/application/actions/register/get.php
index 914ea4e..d1df1b6 100644
--- a/src/application/actions/register/get.php
+++ b/src/application/actions/register/get.php
@@ -1,10 +1,3 @@
<?php
-use mystic\forum\utils\RequestUtils;
-
-_view("template_start", ["_title" => __("Register")]);
-_view("template_navigation_start");
-_view("template_navigation", ["user" => RequestUtils::getAuthorizedUser($db)]);
-_view("template_navigation_end");
-_view("form_register");
-_view("template_end", [...getThemeAndLangInfo()]);
+render("register.twig");
diff --git a/src/application/actions/search/get.php b/src/application/actions/search/get.php
index d3de970..bc1bdba 100644
--- a/src/application/actions/search/get.php
+++ b/src/application/actions/search/get.php
@@ -41,25 +41,14 @@ if ($query !== null) {
}
$end_time = microtime(true);
$search_duration = $end_time - $start_time;
-
- _view("template_start", ["_title" => __("Search results for “%query%”", [ "query" => $query ])]);
- _view("template_navigation_start");
- _view("template_navigation", ["user" => RequestUtils::getAuthorizedUser($db)]);
- _view("template_navigation_end");
- _view("form_search", [ "query" => $query ]);
- _view("view_search_results", [
+
+ render("search.twig", [
"posts" => &$posts,
"topics" => &$topicLookup,
"users" => &$userLookup,
"attachments" => &$attachmentLookup,
"search_duration" => $search_duration,
]);
- _view("template_end", [...getThemeAndLangInfo()]);
} else {
- _view("template_start", ["_title" => __("Search")]);
- _view("template_navigation_start");
- _view("template_navigation", ["user" => RequestUtils::getAuthorizedUser($db)]);
- _view("template_navigation_end");
- _view("form_search");
- _view("template_end", [...getThemeAndLangInfo()]);
-} \ No newline at end of file
+ render("search.twig");
+}
diff --git a/src/application/actions/viewtopic/get.php b/src/application/actions/viewtopic/get.php
index 45dc824..636d791 100644
--- a/src/application/actions/viewtopic/get.php
+++ b/src/application/actions/viewtopic/get.php
@@ -6,7 +6,6 @@ use mystic\forum\orm\Attachment;
use mystic\forum\orm\Post;
use mystic\forum\orm\TopicLogMessage;
use mystic\forum\orm\User;
-use mystic\forum\utils\RequestUtils;
$posts = $db->fetchCustom(Post::class, 'WHERE topic_id = $1 ORDER BY post_date', [ $topicId ]);
/** @var TopicLogMessage[] $logMessages */
@@ -25,13 +24,7 @@ if ($topic->createdBy !== null) {
$allItems = [...$posts, ...$logMessages];
usort($allItems, fn(Post|TopicLogMessage $a, Post|TopicLogMessage $b): int => $a->postDate <=> $b->postDate);
-_view("template_start", ["_title" => $topic->title]);
-_view("template_navigation_start");
-_view("template_navigation", ["user" => RequestUtils::getAuthorizedUser($db)]);
-_view("template_navigation_end");
-_view("view_topic_start", ["topic" => $topic, "topicAuthor" => $topicAuthor]);
-
-foreach ($allItems as $item) {
+$allItems = array_map(function(Post|TopicLogMessage $item) use (&$db, &$topicAuthor, &$topic): array {
/** @var ?User $postAuthor */
$postAuthor = null;
if ($item->authorId !== null && !isset($userCache[$item->authorId])) {
@@ -46,31 +39,27 @@ foreach ($allItems as $item) {
if ($item instanceof Post) {
$attachments = $db->fetchCustom(Attachment::class, 'WHERE post_id = $1', [ $item->id ]);
- _view("view_post", [
+ return [
+ "type" => "post",
"post" => $item,
"postAuthor" => $postAuthor,
"topicAuthor" => $topicAuthor,
"attachments" => $attachments,
"topic" => $topic,
- ]);
+ ];
} else {
- _view("view_topiclog", [
+ return [
+ "type" => "logMessage",
"logMessage" => $item,
"postAuthor" => $postAuthor,
"topicAuthor" => $topicAuthor,
"topic" => $topic,
- ]);
+ ];
}
-}
-
-_view("view_topic_end");
-
-if ($topic->isLocked) {
- _view("view_topic_locked");
-} elseif ($currentUser) {
- _view("form_addpost");
-} else {
- _view("view_logintoreply");
-}
+}, $allItems);
-_view("template_end", [...getThemeAndLangInfo()]);
+render("view_topic.twig", [
+ "topic" => $topic,
+ "topicAuthor" => $topicAuthor,
+ "allItems" => &$allItems,
+]);
diff --git a/src/application/actions/viewuser/get.php b/src/application/actions/viewuser/get.php
index c98df16..e6dac43 100644
--- a/src/application/actions/viewuser/get.php
+++ b/src/application/actions/viewuser/get.php
@@ -17,18 +17,15 @@ foreach ($posts as $item) {
$attachs = $db->fetchCustom(Attachment::class, 'WHERE post_id = $1', [ $item->id ]);
$attachments[$item->id] = $attachs;
}
-_view("template_start", ["_title" => $user->displayName]);
-_view("template_navigation_start");
-_view("template_navigation", [
- "user" => $currentUser,
- "isViewingOwnProfile" => $isOwnProfile,
-]);
-_view("template_navigation_end");
-_view("view_user", [
+
+$dateJoined = DateTime::createFromImmutable($user->created);
+$dateJoined->setTime(0, 0, 0, 0);
+
+render("view_user.twig", [
"user" => $user,
"posts" => $posts,
"topics" => $topics,
"attachments" => $attachments,
"lastNameChangeTooRecent" => $lastNameChangeTooRecent,
+ "dateJoined" => $dateJoined,
]);
-_view("template_end", [...getThemeAndLangInfo()]);
diff --git a/src/application/messages/de.msg b/src/application/messages/de.msg
index d77f2eb..b6fb3d6 100644
--- a/src/application/messages/de.msg
+++ b/src/application/messages/de.msg
@@ -525,3 +525,8 @@ metadata({
: "Spoiler"
= "Spoiler"
+: "View profile"
+= "Profil ansehen"
+
+: "Log out"
+= "Abmelden"
diff --git a/src/application/mystic/forum/utils/RequestUtils.php b/src/application/mystic/forum/utils/RequestUtils.php
index 5c6a485..11253aa 100644
--- a/src/application/mystic/forum/utils/RequestUtils.php
+++ b/src/application/mystic/forum/utils/RequestUtils.php
@@ -40,9 +40,10 @@ final class RequestUtils {
return $fieldValue;
}
- public static function storeForm(): void {
+ public static function storeForm(string $formId): void {
$_SESSION["lastForm"] = $_POST ?? [];
$_SESSION["lastForm_uri"] = $_SERVER["REQUEST_URI"];
+ $_SESSION["lastForm_id"] = $formId;
}
public static function setFormErrorDestination(?string $dest): ?string {
@@ -55,7 +56,7 @@ final class RequestUtils {
$next ??= self::$formErrorDestination ?? $_SERVER["REQUEST_URI"];
$_SESSION["formError/$formId"] = $message;
// store last form submission
- self::storeForm();
+ self::storeForm($formId);
header("Location: $next");
exit;
}
@@ -66,14 +67,20 @@ final class RequestUtils {
return $err;
}
- public static function getLastForm(string &$lastFormUri): ?array {
+ public static function getLastForm_legacy(string &$lastFormUri): ?array {
$lastFormUri = $_SESSION["lastForm_uri"] ?? "";
return $_SESSION["lastForm"] ?? null;
}
+ public static function getLastForm(string &$lastFormId): ?array {
+ $lastFormId = $_SESSION["lastForm_id"] ?? "";
+ return $_SESSION["lastForm"] ?? null;
+ }
+
public static function clearLastForm(): void {
unset($_SESSION["lastForm"]);
unset($_SESSION["lastForm_uri"]);
+ unset($_SESSION["lastForm_id"]);
}
public static function getAuthorizedUser(Database &$db): ?User {
diff --git a/src/application/templates/bootstrap-3/base.twig b/src/application/templates/bootstrap-3/base.twig
new file mode 100644
index 0000000..5e003b9
--- /dev/null
+++ b/src/application/templates/bootstrap-3/base.twig
@@ -0,0 +1,250 @@
+{%- if title -%}
+ {%- set title = title ~ " | " -%}
+{%- endif -%}
+{%- set title = title ~ (g.env.MYSTIC_FORUM_TITLE|default("Forum")) -%}
+{%- set nextParam = "" -%}
+{%- if g.globals.action in ["login", "register"] -%}
+ {%- set nextParam = g.get.next|default("") -%}
+{%- else -%}
+ {%- set nextParam = g.server.REQUEST_URI -%}
+{%- endif -%}
+<!DOCTYPE html>
+<!--[if lt IE 7]> <html lang="de" class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
+<!--[if IE 7]> <html lang="de" class="no-js lt-ie9 lt-ie8"> <![endif]-->
+<!--[if IE 8]> <html lang="de" class="no-js lt-ie9"> <![endif]-->
+<!--[if gt IE 8]><!--> <html lang="de" class="no-js"> <!--<![endif]-->
+<head>
+ <meta charset="utf-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <meta name="viewport" content="width=device-width,initial-scale=1">
+ <meta name="generator" content="mysticBB {{ constant("MYSTICBB_VERSION") }}">
+ <title>{{ title }}</title>
+ <link rel="stylesheet" href="?_action=ctheme">
+ <script src="/ui/jquery-1.12.4.min.js"></script>
+ <script src="/ui/dist/js/bootstrap.min.js"></script>
+ <script src="/ui/modernizr-2.6.2.min.js"></script>
+ <script src="?_action=ji18n"></script>
+ <!--[if lt IE 9]>
+ <script src="/ui/html5shiv.min.js"></script>
+ <script src="/ui/respond.min.js"></script>
+ <![endif]-->
+ {% block head_after %}{% endblock %}
+</head>
+<body>
+
+{% block nav %}
+<nav class="navbar navbar-default navbar-static-top">
+ <div class="container">
+ <div class="navbar-header">
+ <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#nav-collapse" aria-expanded="false">
+ <span class="sr-only">Toggle navigation</span>
+ <span class="icon-bar"></span>
+ <span class="icon-bar"></span>
+ <span class="icon-bar"></span>
+ </button>
+ <a class="navbar-brand" href=".">
+ <span class="myb-icon mybblogo" aria-hidden="false"></span>
+ {{ g.env.MYSTIC_FORUM_TITLE|default("Forum") }}
+ </a>
+ </div>
+ <div class="collapse navbar-collapse" id="nav-collapse">
+ {% block navbar %}
+ {% if currentUser %}
+ <ul class="nav navbar-nav navbar-right">
+ <li><p class="navbar-text">{{ __("Welcome, %user%!", {
+ user: currentUser.id == constant("mystic\\forum\\orm\\User::SUPERUSER_ID") ? ('<strong class="text-danger">' ~ (currentUser.displayName|e("html")) ~ '</strong>')|raw : ('<strong>' ~ (currentUser.displayName|e("html")) ~ '</strong>')|raw
+ }) }}
+ </p></li>
+ <li{{ g.globals.action == "search" ? ' class="active"'|raw : '' }}><a href="?_action=search"><span class="fa fa-search" aria-hidden="true"></span><span class="sr-only">{{ __("Search") }}</span></a></li>
+ <li{{ (g.globals.action == "viewuser" and g.get.user == currentUser.id) ? ' class="active"'|raw : '' }}><a href="?_action=viewuser&amp;user={{ currentUser.id|url_encode }}"><span class="fa fa-user" aria-hidden="true"></span><span class="sr-only">{{ __("View profile") }}</span></a></li>
+ <li><a href="?_action=logout&amp;next={{ g.server.REQUEST_URI|url_encode }}"><span class="fa fa-sign-out" aria-hidden="true"></span><span class="sr-only">{{ __("Log out") }}</span></a></li>
+ </ul>
+ {% else %}
+ <ul class="nav navbar-nav navbar-right">
+ <li{{ g.globals.action == "search" ? ' class="active"'|raw : '' }}><a href="?_action=search"><span class="fa fa-search" aria-hidden="true"></span><span class="sr-only">{{ __("Search") }}</span></a></li>
+ <li{{ g.globals.action == "auth" ? ' class="active"'|raw : '' }}><a href="?_action=auth&amp;next={{ nextParam|url_encode }}">{{ __("Log in") }}</a></li>
+ {% if constant("REGISTRATION_ENABLED") %}
+ <li{{ g.globals.action == "register" ? ' class="active"'|raw : '' }}><a href="?_action=register&amp;next={{ nextParam|url_encode }}">{{ __("Register") }}</a></li>
+ {% endif %}
+ </ul>
+ {% endif %}
+ {% endblock %}
+ </div>
+ </div>
+</nav>
+{% endblock %}
+
+{% block main %}
+<div class="container">
+ {% block content %}{% endblock %}
+</div>
+{% endblock %}
+
+{% block footer %}
+<footer class="footer">
+<div class="container">
+<div class="panel panel-default">
+<div class="panel-body">
+ <table style="border-collapse: collapse; width: 100%; background: none">
+ <tbody>
+ <tr>
+ <td style="padding: 0; vertical-align: middle; text-align: left; width: 100%" class="text-normal">
+ &copy; {{ "now"|date("Y") }} {{ g.env.MYSTIC_FORUM_COPYRIGHT|default(g.env.MYSTIC_FORUM_TITLE)|default("Forum") }}.
+ Powered by <a href="https://git.jkohl.link/mystic-forum.git/tag/?h=v{{ constant("MYSTICBB_VERSION")|url_encode }}">mysticBB v{{ constant("MYSTICBB_VERSION") }}</a>.
+ </td>
+ <td style="padding: 0; vertical-align: middle; text-align: right; white-space: nowrap" class="text-normal">
+ <form action="?_action=settheme" class="form-inline seamless-inline" method="post">
+ <input type="hidden" name="next" value="{{ g.server.REQUEST_URI }}">
+ <div class="form-group">
+ <label for="theme-select">{{ __("Theme:") }}</label>
+ <select class="form-control input-sm auto-submit" id="theme-select" name="theme">
+ {% for themeKey, themeInfo in availableThemes %}
+ <option value="{{ themeKey }}"{{ themeKey == currentTheme ? " selected" : "" }}>{{ themeInfo.name }}</option>
+ {% endfor %}
+ </select>
+ </div>
+ </form>
+ &nbsp;&nbsp;&nbsp;
+ <form action="?_action=setlang" class="form-inline seamless-inline" method="post">
+ <input type="hidden" name="next" value="{{ g.server.REQUEST_URI }}">
+ <div class="form-group">
+ <label for="lang-select">{{ __("Language:") }}</label>
+ <select class="form-control input-sm auto-submit" id="lang-select" name="lang">
+ {% for langKey, langName in availableLangs %}
+ <option value="{{ langKey }}"{{ langKey == currentLang ? " selected" : "" }}>{{ langName }}</option>
+ {% endfor %}
+ </select>
+ </div>
+ </form>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+</div>
+</div>
+</div>
+</footer>
+{% endblock %}
+
+{% block scripts %}
+<script type="text/javascript">
+ $(function() {
+ function insertAroundSelection(textarea, before, after) {
+ var start = textarea.selectionStart;
+ var end = textarea.selectionEnd;
+ var text = textarea.value;
+ var pre = text.substring(0, start);
+ var inner = text.substring(start, end);
+ var post = text.substring(end);
+ start += before.length;
+ end += before.length;
+ text = pre + before + inner + after + post;
+ textarea.value = text;
+ textarea.focus();
+ textarea.selectionStart = start;
+ textarea.selectionEnd = end;
+ }
+
+ function getTextarea(btn) {
+ return $($(btn).attr("data-area"))[0];
+ }
+
+ var commands = {
+ bold: function(textarea) {
+ insertAroundSelection(textarea, "[b]", "[/b]");
+ },
+ italic: function(textarea) {
+ insertAroundSelection(textarea, "[i]", "[/i]");
+ },
+ underline: function(textarea) {
+ insertAroundSelection(textarea, "[u]", "[/u]");
+ },
+ strikethrough: function(textarea) {
+ insertAroundSelection(textarea, "[s]", "[/s]");
+ },
+ sup: function(textarea) {
+ insertAroundSelection(textarea, "[^]", "[/^]");
+ },
+ sub: function(textarea) {
+ insertAroundSelection(textarea, "[_]", "[/_]");
+ },
+ quote: function(textarea) {
+ insertAroundSelection(textarea, "> ", "");
+ },
+ spoiler: function(textarea) {
+ insertAroundSelection(textarea, "[spoiler]", "[/spoiler]");
+ }
+ }
+
+ $("button[data-editor-command]").attr("data-toggle", "tooltip").attr("data-placement", "bottom").click(function() {
+ var command = $(this).attr("data-editor-command");
+ var textarea = getTextarea(this);
+ commands[command](textarea);
+ }).tooltip();
+ });
+</script>
+
+<script>
+$(function() {
+ var _messages = {{ ({
+ selectFiles: [
+ ___("Select file", "Select files", 1),
+ ___("Select file", "Select files", 2),
+ ],
+ filesSelected: [
+ ___("%n% file selected", "No files selected", 0),
+ ___("%n% file selected", "%n% files selected", 1),
+ ___("%n% file selected", "%n% files selected", 2),
+ ],
+ })|json_encode|raw }}
+
+ $(".auto-submit").on("change", function() {
+ $(this)[0].form.submit();
+ });
+
+ $("._time").each(function(i, e) {
+ var date = new Date($(e).text());
+ $(e).text(date.toLocaleString());
+ });
+ $("._date").each(function(i, e) {
+ var date = new Date($(e).text());
+ $(e).text(date.toLocaleDateString());
+ });
+ $("._time-only").each(function(i, e) {
+ var date = new Date($(e).text());
+ $(e).text(date.toLocaleTimeString());
+ });
+
+ $("input[type=file]").each(function(i, e) {
+ var isMultiple = !!$(e).prop("multiple");
+ var isSmall = !!$(e).hasClass("small");
+ var $input = $('<input type="text" readonly class="form-control" />').attr("placeholder", _messages.filesSelected[0]).css("text-overflow", "ellipsis");
+ var $btn = $('<button class="btn btn-default" type="button"></button>');
+ if (isSmall) {
+ $input.addClass("input-sm");
+ $btn.addClass("btn-sm");
+ }
+ $(e).after($('<div class="input-group file-input-group"></div>').append(
+ $input,
+ $('<span class="input-group-btn"></span>').append(
+ $btn.text(_messages.selectFiles[isMultiple ? 1 : 0]).click(function() {
+ $(e).click();
+ })
+ )
+ )).addClass("sr-only");
+ $(e).on("change", function() {
+ var files = $(e)[0].files;
+ if (files.length < 1)
+ $input.val("");
+ else if (files.length === 1)
+ $input.val(files[0].name);
+ else
+ $input.val(_messages.filesSelected[2].replace("%n%", files.length));
+ });
+ })
+});
+</script>
+{% endblock %}
+
+</body>
+</html>
diff --git a/src/application/templates/bootstrap-3/components/alert_error.twig b/src/application/templates/bootstrap-3/components/alert_error.twig
new file mode 100644
index 0000000..f3f759b
--- /dev/null
+++ b/src/application/templates/bootstrap-3/components/alert_error.twig
@@ -0,0 +1,8 @@
+<div class="alert alert-danger" role="alert">
+ <span class="fa fa-exclamation-circle" aria-hidden="true"></span>
+ {% if message starts with "?!HTML::" %}
+ {{ message|slice(8)|raw }}
+ {% else %}
+ {{ message }}
+ {% endif %}
+</div>
diff --git a/src/application/templates/bootstrap-3/components/alert_info.twig b/src/application/templates/bootstrap-3/components/alert_info.twig
new file mode 100644
index 0000000..099d091
--- /dev/null
+++ b/src/application/templates/bootstrap-3/components/alert_info.twig
@@ -0,0 +1,8 @@
+<div class="alert alert-info" role="alert">
+ <span class="fa fa-info-circle" aria-hidden="true"></span>
+ {% if message starts with "?!HTML::" %}
+ {{ message|slice(8)|raw }}
+ {% else %}
+ {{ message }}
+ {% endif %}
+</div>
diff --git a/src/application/templates/bootstrap-3/components/alert_success.twig b/src/application/templates/bootstrap-3/components/alert_success.twig
new file mode 100644
index 0000000..1c8f403
--- /dev/null
+++ b/src/application/templates/bootstrap-3/components/alert_success.twig
@@ -0,0 +1,8 @@
+<div class="alert alert-success" role="alert">
+ <span class="fa fa-check-circle" aria-hidden="true"></span>
+ {% if message starts with "?!HTML::" %}
+ {{ message|slice(8)|raw }}
+ {% else %}
+ {{ message }}
+ {% endif %}
+</div>
diff --git a/src/application/templates/bootstrap-3/components/post.twig b/src/application/templates/bootstrap-3/components/post.twig
new file mode 100644
index 0000000..ac969c0
--- /dev/null
+++ b/src/application/templates/bootstrap-3/components/post.twig
@@ -0,0 +1,168 @@
+{%- set fileAttachments = attachments|filter(a => not (a.mimeType starts with "image/" or a.mimeType starts with "video/")) -%}
+{%- set imageAttachments = attachments|filter(a => a.mimeType starts with "image/" or a.mimeType starts with "video/") -%}
+
+{%- set canReply =
+ not post.deleted
+ and not topic.isLocked
+ and currentUser is not null
+ and currentUser.hasPermission(permission("CREATE_OWN_POST"))
+-%}
+
+{%- set canEdit =
+ not post.deleted
+ and not topic.isLocked
+ and currentUser is not null
+ and (
+ (
+ postAuthor is not null
+ and postAuthor.hasPermission(permission("EDIT_OWN_POST"))
+ )
+ or currentUser.hasPermission(permission("EDIT_OTHER_POST"))
+ ) -%}
+
+{%- set canDelete =
+ not post.deleted
+ and currentUser is not null
+ and (
+ (
+ postAuthor is not null
+ and postAuthor.id == currentUser.id
+ and postAuthor.hasPermission(permission("DELETE_OWN_POST"))
+ )
+ or currentUser.hasPermission(permission("DELETE_OTHER_POST"))
+ ) -%}
+
+{%- set canViewAttachments = currentUser is not null -%}
+
+{%- set your_are_the_author =
+ currentUser is not null
+ and postAuthor is not null
+ and currentUser.id == postAuthor.id
+-%}
+
+{%- set is_op =
+ topicAuthor is not null
+ and postAuthor is not null
+ and postAuthor.id == topicAuthor.id
+-%}
+
+{% if post.deleted %}
+ <div class="media" id="post-{{ post.id }}">
+ <div class="media-left hidden-sm hidden-xs">
+ <div class="media-object" style="width:64px"></div>
+ </div>
+ <div class="media-body">
+ <div class="well icon-well text-warning">
+ <span class="fa fa-exclamation-triangle text-warning" aria-hidden="true"></span>
+ <em>{{ __("This post has been deleted") }}</em>
+ </div>
+ </div>
+ </div>
+{% else %}
+ <div class="media" id="post-{{ post.id }}" data-text="{{ post.content }}" style="overflow: visible;">
+ {% if not hide_pfp %}
+ <div class="media-left hidden-sm hidden-xs">
+ {% if postAuthor %}
+ {% if hide_actions %}
+ <img class="media-object" alt="{{ __("Profile picture") }}" src="?_action=profilepicture&amp;user={{ postAuthor.id|url_encode }}" width="64" height="64">
+ {% else %}
+ <a href="?_action=viewuser&amp;user={{ postAuthor.id|url_encode }}">
+ <img class="media-object" alt="{{ __("Profile picture") }}" src="?_action=profilepicture&amp;user={{ postAuthor.id|url_encode }}" width="64" height="64">
+ </a>
+ {% endif %}
+ {% else %}
+ <div class="media-object" style="width:64px;height:64px"></div>
+ {% endif %}
+ </div>
+ {% endif %}
+ <div class="media-body" style="overflow: visible;">
+ <div class="panel panel-default">
+ <div class="panel-heading">
+ <div class="panel-title h3">
+ {% if not hide_actions %}
+ <div class="pull-right">
+ <a href="#post-{{ post.id }}" class="btn btn-default"><span class="fa fa-link" aria-hidden="true"></span><span class="sr-only">{{ __("Permalink") }}</span></a>
+ {% if canReply %}
+ <button data-post-id="{{ post.id }}" class="btn btn-default js-only _reply-post"><span class="fa fa-comment" aria-hidden="true"></span><span class="sr-only">{{ __("Reply to post") }}</span></button>
+ {% endif %}
+ {% if canEdit %}
+ <button data-post-id="{{ post.id }}" class="btn btn-default js-only _edit-post"><span class="fa fa-pencil" aria-hidden="true"></span><span class="sr-only">{{ __("Edit post") }}</span></button>
+ {% endif %}
+ {% if canDelete %}
+ <form action="?_action=deletepost" method="post" class="seamless-inline">
+ <input type="hidden" name="post" value="{{ post.id }}">
+ <button type="submit" class="btn btn-danger"><span class="fa fa-trash" aria-hidden="true"></span><span class="sr-only">{{ __("Delete post") }}</span></button>
+ </form>
+ {% endif %}
+ </div>
+ {% endif %}
+ {% if postAuthor %}
+ {% if hide_actions %}
+ {{ postAuthor.displayName }}
+ {% else %}
+ <a href="?_action=viewuser&amp;user={{ postAuthor.id|url_encode }}">{{ postAuthor.displayName }}</a>
+ {% if is_op %}
+ <span title="{{ __("Created this topic") }}" class="text-info fa fa-user-circle"></span>
+ {% endif %}
+ {% endif %}
+ {% if your_are_the_author %}
+ <span class="text-normal label label-primary">{{ __("You") }}</span>
+ {% endif %}
+ {% else %}
+ <em class="text-muted">{{ __("(deleted)") }}</em>
+ {% endif %}
+ </div>
+ <span class="_time">{{ post.postDate.format("c") }}</span>
+ {% if post.edited %}
+ <em class="text-muted">{{ __("(edited)") }}</em>
+ {% endif %}
+ </div>
+ <div class="panel-body">
+ <div class="post-content">{{ renderPost(post.content) }}</div>
+ {% if imageAttachments|length > 0 %}
+ <div class="post-images clearfix">
+ {% for attachment in imageAttachments %}
+ {% if hide_actions %}
+ <span class="image-attachment" title="{{ attachment.name }}">
+ <img class="image-attachment-image" src="?_action=thumb&amp;attachment={{ attachment.id|url_encode }}" alt="" width="100">
+ </span>
+ {% else %}
+ <a class="
+ image-attachment
+ attachment
+ {{ attachment.mimeType starts with "video/" ? "video-attachment" }}
+ " href="?_action=attachment&amp;attachment={{ attachment.id|url_encode }}" title="{{ attachment.name }}" data-attachment-id="{{ attachment.id }}">
+ <img class="image-attachment-image" src="?_action=thumb&amp;attachment={{ attachment.id|url_encode }}" alt="" width="100">
+ {% if not canViewAttachments %}
+ <span class="attachment-lock fa fa-ban" aria-hidden="true"></span>
+ {% elseif attachment.mimeType starts with "video/" %}
+ <span class="video-player-icon fa fa-play-circle" aria-hidden="true"></span>
+ {% endif %}
+ </a>
+ {% endif %}
+ {% endfor %}
+ </div>
+ {% endif %}
+ </div>
+ {% if fileAttachments|length > 0 %}
+ <div class="panel-footer">
+ <div class="btn-group">
+ {% for attachment in fileAttachments %}
+ {% if hide_actions %}
+ <button class="btn btn-default">{{ attachment.name }}</button>
+ {% else %}
+ <a class="btn btn-default attachment" href="?_action=attachment&amp;attachment={{ attachment.id|url_encode }}">
+ {% if not canViewAttachments %}
+ <span class="fa fa-ban" aria-hidden="true"></span>
+ {% endif %}
+ {{ attachment.name }}
+ </a>
+ {% endif %}
+ {% endfor %}
+ </div>
+ </div>
+ {% endif %}
+ </div>
+ </div>
+ </div>
+{% endif %}
diff --git a/src/application/templates/bootstrap-3/components/topic_log.twig b/src/application/templates/bootstrap-3/components/topic_log.twig
new file mode 100644
index 0000000..9364cd1
--- /dev/null
+++ b/src/application/templates/bootstrap-3/components/topic_log.twig
@@ -0,0 +1,62 @@
+{%- set user = "" -%}
+{%- if postAuthor is null -%}
+ {%- set user = __("deleted")|e("html") -%}
+{%- else -%}
+ {%- set user =
+ '<a href="?_action=viewuser&amp;user='
+ ~ postAuthor.id|url_encode|e("html")
+ ~ '">'
+ ~ postAuthor.displayName|e("html")
+ ~ '</a>'
+ -%}
+{%- endif -%}
+
+<div class="media" id="post-{{ logMessage.id }}">
+ <div class="media-left hidden-sm hidden-xs">
+ {% if postAuthor %}
+ {% if hideActions %}
+ <img class="media-object" alt="{{ __("Profile picture") }}" src="?_action=profilepicture&amp;user={{ postAuthor.id|url_encode }}" width="64" height="64">
+ {% else %}
+ <a href="?_action=viewuser&amp;user={{ postAuthor.id|url_encode }}">
+ <img class="media-object" alt="{{ __("Profile picture") }}" src="?_action=profilepicture&amp;user={{ postAuthor.id|url_encode }}" width="64" height="64">
+ </a>
+ {% endif %}
+ {% else %}
+ <div class="media-object" style="width:64px;height:64px"></div>
+ {% endif %}
+ </div>
+ <div class="media-body">
+ {% if logMessage.type == constant("mystic\\forum\\orm\\TopicLogMessage::LOCKED") %}
+ <div class="well icon-well text-info">
+ <span class="fa fa-lock text-info" aria-hidden="true"></span>
+ <em>{{ __("%user% locked this topic", {
+ "user": user,
+ }) }}</em>
+ <br>
+ <small class="_time">{{ logMessage.postDate.format("c") }}</small>
+ </div>
+ {% elseif logMessage.type == constant("mystic\\forum\\orm\\TopicLogMessage::UNLOCKED") %}
+ <div class="well icon-well text-success">
+ <span class="fa fa-unlock text-success" aria-hidden="true"></span>
+ <em>{{ __("%user% unlocked this topic", {
+ "user": user,
+ }) }}</em>
+ <br>
+ <small class="_time">{{ logMessage.postDate.format("c") }}</small>
+ </div>
+ {% elseif logMessage.type == constant("mystic\\forum\\orm\\TopicLogMessage::TITLE_CHANGED") %}
+ <div class="well icon-well text-info">
+ <span class="fa fa-pencil text-info" aria-hidden="true"></span>
+ <em>{{ __("%user% changed the title of this topic from %old_title% to %new_title%", {
+ "user": user,
+ "old_title": '<strong>' ~ logMessage.params.old_value|default(__("unknown"))|e("html") ~ '</strong>',
+ "new_title": '<strong>' ~ logMessage.params.new_value|default(__("unknown"))|e("html") ~ '</strong>',
+ }) }}</em>
+ <br>
+ <small class="_time">{{ logMessage.postDate.format("c") }}</small>
+ </div>
+ {% else %}
+ {{ __("unknown") }}
+ {% endif %}
+ </div>
+</div>
diff --git a/src/application/templates/bootstrap-3/delete_post.twig b/src/application/templates/bootstrap-3/delete_post.twig
new file mode 100644
index 0000000..90eaf09
--- /dev/null
+++ b/src/application/templates/bootstrap-3/delete_post.twig
@@ -0,0 +1,36 @@
+{% set title = __("Delete post") %}
+
+{% extends "base.twig" %}
+
+{% block content %}
+
+<div class="panel panel-danger">
+ <div class="panel-heading">
+ <h3 class="panel-title">{{ __("Do you want to delete this post?") }}</h3>
+ </div>
+ <div class="panel-body">
+ {{ __("Are you sure you want to delete the following post:") }}<br>
+ {% include "components/post.twig" with {
+ post: ctx.post,
+ postAuthor: ctx.postAuthor,
+ attachments: ctx.attachments,
+ hide_actions: true,
+ } %}
+ </div>
+ <div class="panel-footer">
+ <div class="text-right">
+ <form action=".#post-{{ ctx.post.id }}" method="get" class="seamless-inline">
+ <input type="hidden" name="_action" value="viewtopic">
+ <input type="hidden" name="topic" value="{{ ctx.post.topicId }}">
+ <button class="btn btn-default">{{ __("Keep post") }}</button>
+ </form>
+ <form action="?_action=deletepost" method="post" class="seamless-inline">
+ <input type="hidden" name="post" value="{{ ctx.post.id }}">
+ <input type="hidden" name="confirm" value="{{ ("confirm" ~ ctx.post.id)|hash("sha256", true)|base64_encode }}">
+ <button class="btn btn-danger">{{ __("Delete post") }}</button>
+ </form>
+ </div>
+ </div>
+</div>
+
+{% endblock %}
diff --git a/src/application/templates/bootstrap-3/delete_topic.twig b/src/application/templates/bootstrap-3/delete_topic.twig
new file mode 100644
index 0000000..3107c89
--- /dev/null
+++ b/src/application/templates/bootstrap-3/delete_topic.twig
@@ -0,0 +1,32 @@
+{% set title = __("Delete topic") %}
+
+{% extends "base.twig" %}
+
+{% block content %}
+
+<div class="panel panel-danger">
+ <div class="panel-heading">
+ <h3 class="panel-title">{{ __("Do you want to delete this topic?") }}</h3>
+ </div>
+ <div class="panel-body">
+ {{ __("Are you sure you want to delete the topic <em>%topic%</em> including <strong>all posts and attachments</strong>?", {
+ "topic": ctx.topic.title|e("html"),
+ }) }}
+ </div>
+ <div class="panel-footer">
+ <div class="text-right">
+ <form action="." method="get" class="seamless-inline">
+ <input type="hidden" name="_action" value="viewtopic">
+ <input type="hidden" name="topic" value="{{ ctx.topic.id }}">
+ <button class="btn btn-default">{{ __("Keep topic") }}</button>
+ </form>
+ <form action="?_action=deletetopic" method="post" class="seamless-inline">
+ <input type="hidden" name="topic" value="{{ ctx.topic.id }}">
+ <input type="hidden" name="confirm" value="{{ ("confirm" ~ ctx.topic.id)|hash("sha256", true)|base64_encode }}">
+ <button class="btn btn-danger">{{ __("Delete topic &amp; posts") }}</button>
+ </form>
+ </div>
+ </div>
+</div>
+
+{% endblock %}
diff --git a/src/application/templates/bootstrap-3/error_page.twig b/src/application/templates/bootstrap-3/error_page.twig
new file mode 100644
index 0000000..dde4057
--- /dev/null
+++ b/src/application/templates/bootstrap-3/error_page.twig
@@ -0,0 +1,12 @@
+{% set title = __("Error") %}
+{% extends "base.twig" %}
+
+{% block navbar %}
+ {% if not ctx.skipLoginCheck %}
+ {{ parent() }}
+ {% endif %}
+{% endblock %}
+
+{% block content %}
+ {% include "components/alert_error.twig" with { message: ctx.message } %}
+{% endblock %}
diff --git a/src/application/templates/bootstrap-3/info_page.twig b/src/application/templates/bootstrap-3/info_page.twig
new file mode 100644
index 0000000..e243b36
--- /dev/null
+++ b/src/application/templates/bootstrap-3/info_page.twig
@@ -0,0 +1,12 @@
+{% set title = __("Information") %}
+{% extends "base.twig" %}
+
+{% block navbar %}
+ {% if not ctx.skipLoginCheck %}
+ {{ parent() }}
+ {% endif %}
+{% endblock %}
+
+{% block content %}
+ {% include "components/alert_info.twig" with { message: ctx.message } %}
+{% endblock %}
diff --git a/src/application/templates/bootstrap-3/login.twig b/src/application/templates/bootstrap-3/login.twig
new file mode 100644
index 0000000..63b9867
--- /dev/null
+++ b/src/application/templates/bootstrap-3/login.twig
@@ -0,0 +1,50 @@
+{% set title = __("Log in") %}
+{% set formId = "login" %}
+{% set formError = getAndClearFormError(formId) %}
+
+{% extends "base.twig" %}
+
+{% block content %}
+
+<div class="page-header margin-top-0">
+ <h1>{{ __("Log in") }}</h1>
+</div>
+
+<div class="col-md-4"></div>
+
+<div class="well col-md-4">
+ {% if formError %}
+ {% include "components/alert_error.twig" with { message: formError } %}
+ {% endif %}
+ <form action="{{ g.server.REQUEST_URI }}" method="post">
+ <input type="hidden" name="form_id" value="{{ formId }}">
+
+ <div class="form-group">
+ <label for="i_username">{{ __("Username:") }}</label>
+ <input class="form-control" type="text" id="i_username" name="username" value="{{ lastFormField(formId, "username") }}" required autofocus>
+ </div>
+
+ <div class="form-group">
+ <label for="i_password">{{ __("Password:") }}</label>
+ <input class="form-control" type="password" id="i_password" name="password" required>
+ </div>
+
+ <div class="form-group">
+ <button class="btn btn-primary" type="submit">{{ __("Log in") }}</button>
+ <a class="btn btn-link" href="?_action=pwreset">{{ __("I forgot my password") }}</a>
+ </div>
+
+ {% if constant("REGISTRATION_ENABLED") %}
+ <div class="form-group">
+ {{ __("Don't have an account? %link%Register now%/link%", {
+ "link": '<a href="?_action=register">',
+ "/link": '</a>',
+ }) }}
+ </div>
+ {% endif %}
+ </form>
+</div>
+
+<div class="col-md-4"></div>
+
+{% endblock %}
diff --git a/src/application/templates/bootstrap-3/new_password.twig b/src/application/templates/bootstrap-3/new_password.twig
new file mode 100644
index 0000000..55a4b79
--- /dev/null
+++ b/src/application/templates/bootstrap-3/new_password.twig
@@ -0,0 +1,42 @@
+{% set title = __("Reset password") %}
+{% set formId = "pwnew" %}
+{% set formError = getAndClearFormError(formId) %}
+
+{% extends "base.twig" %}
+
+{% block content %}
+
+<div class="page-header margin-top-0">
+ <h1>{{ __("Reset password") }}</h1>
+</div>
+
+<div class="col-md-4"></div>
+
+<div class="well col-md-4">
+ {% if formError %}
+ {% include "components/alert_error.twig" with { message: formError } %}
+ {% endif %}
+ <form action="{{ g.server.REQUEST_URI }}" method="post">
+ <input type="hidden" name="form_id" value="{{ formId }}">
+ <input type="hidden" name="token" value="{{ g.get.token }}">
+ <input type="hidden" name="sig" value="{{ g.get.sig }}">
+
+ <div class="form-group">
+ <label for="i_new_password">{{ __("New password:") }}</label>
+ <input class="form-control" type="password" id="i_new_password" name="new_password" required autofocus>
+ </div>
+
+ <div class="form-group">
+ <label for="i_retype_password">{{ __("Retype password:") }}</label>
+ <input class="form-control" type="password" id="i_retype_password" name="retype_password" required>
+ </div>
+
+ <div class="form-group">
+ <button class="btn btn-primary" type="submit">{{ __("Set new password") }}</button>
+ </div>
+ </form>
+</div>
+
+<div class="col-md-4"></div>
+
+{% endblock %}
diff --git a/src/application/templates/bootstrap-3/new_topic.twig b/src/application/templates/bootstrap-3/new_topic.twig
new file mode 100644
index 0000000..74be156
--- /dev/null
+++ b/src/application/templates/bootstrap-3/new_topic.twig
@@ -0,0 +1,58 @@
+{% set title = __("New topic") %}
+{% set formId = "newtopic" %}
+{% set formError = getAndClearFormError(formId) %}
+
+{% extends "base.twig" %}
+
+{% block content %}
+
+<div class="page-header margin-top-0">
+ <h1>{{ __("New topic") }}</h1>
+</div>
+
+{% if formError %}
+ {% include "components/alert_error.twig" with { message: formError } %}
+{% endif %}
+<form action="{{ g.server.REQUEST_URI }}" method="post" enctype="multipart/form-data">
+ <input type="hidden" name="form_id" value="{{ formId }}">
+ <div class="form-group">
+ <label for="i_message">{{ __("Topic title:") }}</label>
+ <input type="text" class="form-control" id="i_title" name="title" value="{{ lastFormField(formId, "title") }}" required autofocus>
+ </div>
+ <div class="form-group">
+ <label for="i_message">{{ __("Message:") }}</label>
+ <div class="panel panel-default">
+ <div class="panel-heading" style="padding:4px">
+ <div class="btn-toolbar" role="toolbar">
+ <div class="btn-group" role="group">
+ <button data-area="#i_message" title="{{ __("Bold") }}" data-editor-command="bold" type="button" class="btn btn-default btn-xs"><i class="fa fa-fw fa-bold"></i></button>
+ <button data-area="#i_message" title="{{ __("Italic") }}" data-editor-command="italic" type="button" class="btn btn-default btn-xs"><i class="fa fa-fw fa-italic"></i></button>
+ <button data-area="#i_message" title="{{ __("Underlined") }}" data-editor-command="underline" type="button" class="btn btn-default btn-xs"><i class="fa fa-fw fa-underline"></i></button>
+ <button data-area="#i_message" title="{{ __("Strikethrough") }}" data-editor-command="strikethrough" type="button" class="btn btn-default btn-xs"><i class="fa fa-fw fa-strikethrough"></i></button>
+ </div>
+ <div class="btn-group" role="group">
+ <button data-area="#i_message" title="{{ __("Superscript") }}" data-editor-command="sup" type="button" class="btn btn-default btn-xs"><i class="fa fa-fw fa-superscript"></i></button>
+ <button data-area="#i_message" title="{{ __("Subscript") }}" data-editor-command="sub" type="button" class="btn btn-default btn-xs"><i class="fa fa-fw fa-subscript"></i></button>
+ </div>
+ <div class="btn-group" role="group">
+ <button data-area="#i_message" title="{{ __("Quote") }}" data-editor-command="quote" type="button" class="btn btn-default btn-xs"><i class="fa fa-fw fa-quote-left"></i></button>
+ <button data-area="#i_message" title="{{ __("Spoiler") }}" data-editor-command="spoiler" type="button" class="btn btn-default btn-xs"><i class="fa fa-fw fa-eye"></i></button>
+ </div>
+ </div>
+ </div>
+ <div class="panel-body" style="padding:0">
+ <textarea class="form-control" id="i_message" name="message" required rows="12" cols="60" style="resize:vertical;max-height:499px;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;margin:-1px;width:calc(100% + 2px)">{{ lastFormField(formId, "message") }}</textarea>
+ </div>
+ </div>
+ </div>
+ <div class="form-group">
+ <label for="i_files">{{ __("Attachments: <small>(max. %max_attachment_count% files, max. %max_attachment_size% MiB each)</small>", {
+ "max_attachment_count": constant("MAX_ATTACHMENT_COUNT"),
+ "max_attachment_size": constant("MAX_ATTACHMENT_SIZE") // (2**20),
+ }) }}</label>
+ <input type="file" name="files[]" id="i_files" multiple accept="*/*">
+ </div>
+ <button type="submit" class="btn btn-success">{{ __("Create topic") }} <span class="fa fa-send" aria-hidden="true"></span></button>
+</form>
+
+{% endblock %}
diff --git a/src/application/templates/bootstrap-3/password_reset.twig b/src/application/templates/bootstrap-3/password_reset.twig
new file mode 100644
index 0000000..f633106
--- /dev/null
+++ b/src/application/templates/bootstrap-3/password_reset.twig
@@ -0,0 +1,42 @@
+{% set title = __("Reset password") %}
+{% set formId = "pwreset" %}
+{% set formError = getAndClearFormError(formId) %}
+
+{% extends "base.twig" %}
+
+{% block content %}
+
+<div class="page-header margin-top-0">
+ <h1>{{ __("Reset password") }}</h1>
+</div>
+
+<div class="col-md-4"></div>
+
+<div class="well col-md-4">
+ {% if formError %}
+ {% include "components/alert_error.twig" with { message: formError } %}
+ {% endif %}
+ <form action="{{ g.server.REQUEST_URI }}" method="post">
+ <input type="hidden" name="form_id" value="{{ formId }}">
+
+ <div class="form-group">
+ <label for="i_username">{{ __("Email address:") }}</label>
+ <input class="form-control" type="email" id="i_email" name="email" value="{{ lastFormField(formId, "email") }}" required autofocus>
+ </div>
+
+ <div class="form-group">
+ <button class="btn btn-primary" type="submit">{{ __("Reset password") }}</button>
+ </div>
+
+ <div class="form-group">
+ {{ __("I know my password and I want to %link%log in%/link%!", {
+ "link": '<a href="?_action=auth">',
+ "/link": '</a>',
+ }) }}
+ </div>
+ </form>
+</div>
+
+<div class="col-md-4"></div>
+
+{% endblock %}
diff --git a/src/application/templates/bootstrap-3/register.twig b/src/application/templates/bootstrap-3/register.twig
new file mode 100644
index 0000000..1e4cd92
--- /dev/null
+++ b/src/application/templates/bootstrap-3/register.twig
@@ -0,0 +1,89 @@
+{% set title = __("Register") %}
+{% set formId = "register" %}
+{% set formError = getAndClearFormError(formId) %}
+
+{% extends "base.twig" %}
+
+{% block content %}
+
+<div class="page-header margin-top-0">
+ <h1>{{ __("Register") }}</h1>
+</div>
+
+<div class="col-md-4"></div>
+
+<div class="well col-md-4">
+ {% if formError %}
+ {% include "components/alert_error.twig" with { message: formError } %}
+ {% endif %}
+ <form action="{{ g.server.REQUEST_URI }}" method="post">
+ <input type="hidden" name="form_id" value="{{ formId }}">
+
+ <div class="form-group" id="group0">
+ <label for="i_username">{{ __("Username:") }}</label>
+ <input class="form-control" id="i_username" type="text" name="username" value="" required>
+ </div>
+
+ <div class="form-group" id="group1">
+ <label for="i_username">{{ __("Username:") }}</label>
+ <input class="form-control" id="i_username" type="text" name="df82a9bc21" value="{{ lastFormField(formId, "df82a9bc21") }}" required autofocus>
+ </div>
+
+ <div class="form-group" id="group2">
+ <label for="i_display_name">{{ __("Display name:") }}</label>
+ <input class="form-control" id="i_display_name" type="text" name="display_name" value="{{ lastFormField(formId, "display_name") }}" required>
+ </div>
+
+ <div class="form-group" id="group3">
+ <label for="i_password">{{ __("Choose password:") }}</label>
+ <input class="form-control" id="i_password" type="password" name="password" required>
+ </div>
+
+ <div class="form-group" id="group4">
+ <label for="i_password_retype">{{ __("Repeat password:") }}</label>
+ <input class="form-control" id="i_password_retype" type="password" name="password_retype" required>
+ </div>
+
+ <div class="form-group" id="group5">
+ <label for="i_email">{{ __("Email address:") }}</label>
+ <input class="form-control" id="i_email" type="email" name="email" value="{{ lastFormField(formId, "email") }}" required>
+ </div>
+
+ <div class="form-group" id="group6">
+ <label for="i_email">{{ __("CAPTCHA:") }}</label>
+ <div class="text-center margin-bottom">
+ <img src="?_action=captcha&amp;t={{ "now"|date("Uv") }}" alt="CAPTCHA" width="192" height="48" id="captcha-img">
+ </div>
+ <div class="input-group">
+ <input type="text" name="captcha" id="i_captcha" class="form-control" required>
+ <div class="input-group-btn">
+ <button class="btn btn-default" type="button" id="btn-refresh-captcha"><span class="fa fa-refresh" aria-hidden="true"></span><span class="sr-only">{{ __("New CAPTCHA") }}</span></button>
+ </div>
+ </div>
+ </div>
+
+ <div class="form-group">
+ <button class="btn btn-primary" type="submit">{{ __("Register now") }}</button>
+ </div>
+
+ <div class="form-group">
+ {{ __("Already have an account? %link%Sign in now%/link%", {
+ "link": '<a href="?_action=auth">',
+ "/link": '</a>',
+ }) }}
+ </div>
+ </form>
+</div>
+
+<div class="col-md-4"></div>
+
+<script>
+$(function() {
+ $("#btn-refresh-captcha").click(function() {
+ $("#captcha-img").attr("src", "?_action=captcha&t=" + new Date().getTime().toString());
+ });
+ $("#i_username").prop("disabled", true).prop("required", false);
+});
+</script>
+
+{% endblock %}
diff --git a/src/application/templates/bootstrap-3/search.twig b/src/application/templates/bootstrap-3/search.twig
new file mode 100644
index 0000000..c8f6315
--- /dev/null
+++ b/src/application/templates/bootstrap-3/search.twig
@@ -0,0 +1,63 @@
+{% set title = __("Search") %}
+{% set formId = "search" %}
+{% set formError = getAndClearFormError(formId) %}
+
+{% extends "base.twig" %}
+
+{% block content %}
+
+<div class="page-header margin-top-0">
+ <h1>{{ __("Search") }}</h1>
+</div>
+
+{% if formError %}
+ {% include "components/alert_error.twig" with { message: formError } %}
+{% endif %}
+
+<form action="." method="get">
+ <input type="hidden" name="form_id" value="{{ formId }}">
+ <input type="hidden" name="_action" value="search">
+ <div class="form-group">
+ <div class="input-group">
+ <input class="form-control" type="search" id="i_query" name="query" value="{{ lastFormField(formId, "query")|default(g.get.query)|default("") }}" required autofocus placeholder="{{ __("Enter your search query...") }}">
+ <div class="input-group-btn">
+ <button class="btn btn-primary" type="submit">{{ __("Search") }}</button>
+ </div>
+ </div>
+ </div>
+</form>
+
+{% if g.get.query is defined and g.get.query is not null and g.get.query != "" %}
+ {% if ctx.posts|length > 0 %}
+ <p>{{ __("%result_count% result(s) in %search_duration% second(s)", {
+ "result_count": ctx.posts|length,
+ "search_duration": ctx.search_duration|number_format(2, __(".", context: "Number formatting"), __(",", context: "Number formatting")),
+ }) }}</p>
+ <div class="list-group margin-top">
+ {% for post in ctx.posts|filter(p => not p.deleted) %}
+ {% set hasAttachments = ctx.attachments[post.id]|length > 0 %}
+ {% set postAuthor = ctx.users[post.authorId] %}
+ <a href="?_action=viewtopic&amp;topic={{ post.topicId|url_encode }}#post-{{ post.id|url_encode }}" class="list-group-item">
+ {% if hasAttachments %}
+ <span class="badge"><span class="fa fa-paperclip"></span></span>
+ {% endif %}
+ {{ renderPostSummary(post.content) }}<br>
+ <span class="text-muted">{{ __("posted by %author% on %post_date% in %topic%", {
+ "author": '<em>' ~ (postAuthor ? postAuthor.displayName : __("unknown"))|e("html") ~ '</em>',
+ "post_date": '<span class="_time">' ~ post.postDate.format("c")|e("html") ~ '</span>',
+ "topic": '<em>'
+ ~ (topics[post.topicId].isLocked ? '<span class="fa fa-lock text-muted" aria-hidden="true"></span> ' : '')
+ ~ (topics[post.topicId] ? topics[post.topicId].title : null)|default("unknown")|e("html") ~ '</em>',
+ }) }}</span>
+ </a>
+ {% endfor %}
+ </div>
+ {% else %}
+ <div class="well icon-well text-info margin-top margin-bottom">
+ <span class="fa fa-info-circle text-info" aria-hidden="true"></span>
+ <em>{{ __("No results for this search") }}</em>
+ </div>
+ {% endif %}
+{% endif %}
+
+{% endblock %}
diff --git a/src/application/templates/bootstrap-3/view_topic.twig b/src/application/templates/bootstrap-3/view_topic.twig
new file mode 100644
index 0000000..008c336
--- /dev/null
+++ b/src/application/templates/bootstrap-3/view_topic.twig
@@ -0,0 +1,353 @@
+{% set canReply =
+ not ctx.topic.isLocked
+ and currentUser is not null
+ and currentUser.hasPermission(permission("CREATE_OWN_POST")) %}
+
+{% set canEdit =
+ currentUser is not null and (
+ (
+ ctx.topicAuthor is not null
+ and currentUser.id == ctx.topicAuthor.id
+ and ctx.topicAuthor.hasPermission(permission("EDIT_OWN_TOPIC"))
+ )
+ or currentUser.hasPermission(permission("EDIT_OTHER_TOPIC"))
+ ) %}
+
+{% set canDelete =
+ currentUser is not null and (
+ (
+ ctx.topicAuthor is not null
+ and currentUser.id == ctx.topicAuthor.id
+ and ctx.topicAuthor.hasPermission(permission("DELETE_OWN_TOPIC"))
+ )
+ or currentUser.hasPermission(permission("DELETE_OTHER_TOPIC"))
+ ) %}
+
+{% set title = ctx.topic.title %}
+{% extends "base.twig" %}
+
+{% block content %}
+
+{% if canEdit %}
+ <div class="modal fade" tabindex="-1" role="dialog" id="diag-edit-post">
+ <form class="modal-dialog" role="document" action="?_action=updatepost" method="post">
+ <input type="hidden" id="i_edit_post" name="post">
+ <div class="modal-content">
+ <div class="modal-header">
+ <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
+ <h4 class="modal-title">{{ __("Edit post") }}</h4>
+ </div>
+ <div class="modal-body">
+ <label class="sr-only" for="i_edit_message">{{ __("Message:") }}</label>
+ <div class="panel panel-default">
+ <div class="panel-heading" style="padding:4px">
+ <div class="btn-toolbar" role="toolbar">
+ <div class="btn-group" role="group">
+ <button data-area="#i_edit_message" title="{{ __("Bold") }}" data-editor-command="bold" type="button" class="btn btn-default btn-xs"><i class="fa fa-fw fa-bold"></i></button>
+ <button data-area="#i_edit_message" title="{{ __("Italic") }}" data-editor-command="italic" type="button" class="btn btn-default btn-xs"><i class="fa fa-fw fa-italic"></i></button>
+ <button data-area="#i_edit_message" title="{{ __("Underlined") }}" data-editor-command="underline" type="button" class="btn btn-default btn-xs"><i class="fa fa-fw fa-underline"></i></button>
+ <button data-area="#i_edit_message" title="{{ __("Strikethrough") }}" data-editor-command="strikethrough" type="button" class="btn btn-default btn-xs"><i class="fa fa-fw fa-strikethrough"></i></button>
+ </div>
+ <div class="btn-group" role="group">
+ <button data-area="#i_edit_message" title="{{ __("Superscript") }}" data-editor-command="sup" type="button" class="btn btn-default btn-xs"><i class="fa fa-fw fa-superscript"></i></button>
+ <button data-area="#i_edit_message" title="{{ __("Subscript") }}" data-editor-command="sub" type="button" class="btn btn-default btn-xs"><i class="fa fa-fw fa-subscript"></i></button>
+ </div>
+ <div class="btn-group" role="group">
+ <button data-area="#i_edit_message" title="{{ __("Quote") }}" data-editor-command="quote" type="button" class="btn btn-default btn-xs"><i class="fa fa-fw fa-quote-left"></i></button>
+ <button data-area="#i_edit_message" title="{{ __("Spoiler") }}" data-editor-command="spoiler" type="button" class="btn btn-default btn-xs"><i class="fa fa-fw fa-eye"></i></button>
+ </div>
+ </div>
+ </div>
+ <div class="panel-body" style="padding:0">
+ <textarea class="form-control" id="i_edit_message" name="message" required rows="12" cols="60" style="resize:vertical;max-height:500px;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;margin:-1px;width:calc(100% + 2px)"></textarea>
+ </div>
+ </div>
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-default" data-dismiss="modal"><span class="fa fa-remove" aria-hidden="true"></span> {{ __("Cancel") }}</button>
+ <button type="submit" class="btn btn-success"><span class="fa fa-save" aria-hidden="true"></span> {{ __("Save changes") }}</button>
+ </div>
+ </div>
+ </form>
+ </div>
+{% endif %}
+{% if currentUser is null %}
+ <div class="modal fade" tabindex="-1" role="dialog" id="diag-cant-view-attachment">
+ <div class="modal-dialog modal-danger" role="document">
+ <div class="modal-content panel-danger">
+ <div class="modal-header panel-heading">
+ <h4 class="modal-title"><span class="fa fa-exclamation-circle" aria-hidden="true"></span> {{ __("Permission denied") }}</h4>
+ </div>
+ <div class="modal-body">
+ {{ __("You must be logged in to view attachments") }}
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-default" data-dismiss="modal"><span class="fa fa-close" aria-hidden="true"></span> {{ __("Close") }}</button>
+ <a href="?_action=auth&amp;next={{ g.server.REQUEST_URI|url_encode }}" class="btn btn-success"><span class="fa fa-user" aria-hidden="true"></span> {{ __("Log in") }}</a>
+ </div>
+ </div>
+ </div>
+ </div>
+ <script>
+ $(function() {
+ $(".attachment").click(function(e) {
+ e.preventDefault();
+ $("#diag-cant-view-attachment").modal();
+ });
+ });
+ </script>
+{% else %}
+ <div class="modal fade" tabindex="-1" role="dialog" id="diag-image-attachment">
+ <div class="modal-dialog" role="document">
+ <div class="modal-content">
+ <div class="modal-header">
+ <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
+ <h4 class="modal-title">{{ __("Attachment") }}</h4>
+ </div>
+ <div class="modal-body">
+ <img class="image-attachment-view attachment-view" id="image-attachment-view" alt="">
+ </div>
+ <div class="modal-footer">
+ <a href="" download id="image-attachment-dl-btn" class="btn btn-default"><span class="fa fa-download" aria-hidden="true"></span> {{ __("Download") }}</a>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="modal fade" tabindex="-1" role="dialog" id="diag-video-attachment">
+ <div class="modal-dialog" role="document">
+ <div class="modal-content">
+ <div class="modal-header">
+ <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
+ <h4 class="modal-title">{{ __("Attachment") }}</h4>
+ </div>
+ <div class="modal-body">
+ <video class="video-attachment-view attachment-view" id="video-attachment-view" controls></video>
+ </div>
+ <div class="modal-footer">
+ <a href="" download id="video-attachment-dl-btn" class="btn btn-default"><span class="fa fa-download" aria-hidden="true"></span> {{ __("Download") }}</a>
+ </div>
+ </div>
+ </div>
+ </div>
+ <script>
+ $(function() {
+ $(".image-attachment:not(.video-attachment)").click(function(e) {
+ e.preventDefault();
+ var attUrl = "?_action=attachment&attachment=" + encodeURIComponent($(this).attr("data-attachment-id"));
+ $("#image-attachment-view").attr("src", attUrl);
+ $("#image-attachment-dl-btn").attr("href", attUrl);
+ $("#diag-image-attachment").modal();
+ });
+ $(".image-attachment.video-attachment").click(function(e) {
+ e.preventDefault();
+ var attUrl = "?_action=attachment&attachment=" + encodeURIComponent($(this).attr("data-attachment-id"));
+ $("#video-attachment-view").attr("src", attUrl);
+ $("#video-attachment-dl-btn").attr("href", attUrl);
+ $("#diag-video-attachment").modal();
+ });
+ $("#diag-video-attachment").on("hide.bs.modal", function() {
+ $("#video-attachment-view")[0].pause();
+ });
+ });
+ </script>
+{% endif %}
+
+{% set formError = getAndClearFormError("updateTopic") %}
+{% if formError %}
+ {% include "components/alert_error.twig" with { message: formError } %}
+{% endif %}
+{% set formError = getAndClearFormError("lockTopic") %}
+{% if formError %}
+ {% include "components/alert_error.twig" with { message: formError } %}
+{% endif %}
+{% set formError = null %}
+
+<div class="page-header margin-top-0 clearfix">
+ <div id="displayHeading">
+ <div role="heading" class="h1 seamless-inline">
+ {% if ctx.topic.isLocked %}
+ <span class="fa fa-lock text-muted" aria-hidden="true"></span>
+ {% endif %}
+ {{ ctx.topic.title }}
+ <div class="pull-right text-normal">
+ {% if canEdit and not ctx.topic.isLocked %}
+ <button id="btn-edit-title" class="btn btn-default js-only"><span class="fa fa-pencil" aria-hidden="true"></span> {{ __("Edit title") }}</button>
+ {% endif %}
+ {% if canReply %}
+ <button id="btn-reply" class="btn btn-default js-only"><span class="fa fa-comment" aria-hidden="true"></span> {{ __("Reply") }}</button>
+ {% endif %}
+ {% if canEdit %}
+ {% if ctx.topic.isLocked %}
+ <form action="?_action=locktopic" method="post" class="seamless-inline">
+ <input type="hidden" name="topic" value="{{ ctx.topic.id }}">
+ <input type="hidden" name="locked" value="false">
+ <button type="submit" class="btn btn-success"><span class="fa fa-unlock" aria-hidden="true"></span> {{ __("Unlock topic") }}</button>
+ </form>
+ {% else %}
+ <form action="?_action=locktopic" method="post" class="seamless-inline">
+ <input type="hidden" name="topic" value="{{ ctx.topic.id }}">
+ <input type="hidden" name="locked" value="true">
+ <button type="submit" class="btn btn-warning"><span class="fa fa-lock" aria-hidden="true"></span> {{ __("Lock topic") }}</button>
+ </form>
+ {% endif %}
+ {% endif %}
+ {% if canDelete %}
+ <form action="?_action=deletetopic" method="post" class="seamless-inline">
+ <input type="hidden" name="topic" value="{{ ctx.topic.id }}">
+ <button type="submit" class="btn btn-danger"><span class="fa fa-trash" aria-hidden="true"></span> {{ __("Delete topic") }}</button>
+ </form>
+ {% endif %}
+ </div>
+ </div><br>
+ {{ __("Started by %user% on %date%", {
+ "user": (ctx.topicAuthor is not null) ? ('<a href="?_action=viewuser&amp;user=' ~ ctx.topicAuthor.id|url_encode|e("html") ~ '">' ~ ctx.topicAuthor.displayName|e("html") ~ '</a>') : __("(deleted)"),
+ "date": '<span class="_time">' ~ ctx.topic.creationDate.format("c")|e("html") ~ '</span>',
+ }) }}
+ </div>
+ {% if canEdit %}
+ <form action="?_action=updatetopic" method="post" id="editHeading" style="display: none;" class="form-inline seamless-inline">
+ <input type="hidden" name="topic" value="{{ ctx.topic.id }}">
+ <div class="row">
+ <div class="col-md-8">
+ <input type="text" class="form-control" name="title" id="i_edit_title" data-original-value="{{ ctx.topic.title }}" value="{{ ctx.topic.title }}" style="box-sizing: border-box; width: 100%; font-size: 36px; height: 56px">
+ </div>
+ <div class="col-md-4 text-right">
+ <button type="button" id="topicTitleEditCancel" class="btn btn-default"><span class="fa fa-close" aria-hidden="true"></span> {{ __("Cancel") }}</button>
+ <button type="submit" class="btn btn-success"><span class="fa fa-save" aria-hidden="true"></span> {{ __("Save changes") }}</button>
+ </div>
+ </div>
+ </form>
+ {% endif %}
+</div>
+<script>
+{% if canEdit %}
+$(function() {
+ $("#btn-edit-title").click(function() {
+ $("#displayHeading").hide();
+ $("#editHeading").show();
+ $("#i_edit_title").val($("#i_edit_title").attr("data-original-value")).focus();
+ });
+ $("#topicTitleEditCancel").click(function() {
+ $("#displayHeading").show();
+ $("#editHeading").hide();
+ });
+ $("._edit-post").click(function() {
+ var $post = $("#post-" + $(this).attr("data-post-id"));
+ var $postContent = $post.find(".post-content");
+ $("#i_edit_message").css("height", "").val($post.attr("data-text"));
+ $("#i_edit_post").val($(this).attr("data-post-id"));
+ $("#diag-edit-post").modal();
+ });
+ $("#diag-edit-post").on("shown.bs.modal", function() {
+ $("#i_edit_message").focus();
+ });
+});
+{% endif %}
+{% if canReply %}
+$(function() {
+ function focusReplyBox() {
+ var msgInput = $("#i_message");
+ msgInput[0].scrollIntoView();
+ msgInput.focus();
+ }
+ $("#btn-reply").click(function() {
+ focusReplyBox();
+ });
+ $("._reply-post").click(function() {
+ var text = $("#post-" + $(this).attr("data-post-id")).attr("data-text");
+ var val = $("#i_message").val();
+ var lines = text.split("\n");
+ for (var i = 0; i < lines.length; ++i)
+ val += "\n> " + lines[i];
+ val += "\n\n";
+ $("#i_message").val(val.replace(/^\n+/, ""));
+ focusReplyBox();
+ });
+});
+{% endif %}
+</script>
+
+{% for item in ctx.allItems %}
+ {% if item.type == "post" %}
+ {% include "components/post.twig" with {
+ post: item.post,
+ postAuthor: item.postAuthor,
+ topic: item.topic,
+ attachments: item.attachments,
+ hide_actions: false,
+ hide_pfp: false,
+ } %}
+ {% elseif item.type == "logMessage" %}
+ {% include "components/topic_log.twig" with {
+ type: item.type,
+ logMessage: item.logMessage,
+ postAuthor: item.postAuthor,
+ topicAuthor: item.topicAuthor,
+ topic: item.topic,
+ hide_actions: false,
+ hide_pfp: false,
+ } %}
+ {% endif %}
+{% endfor %}
+
+{% if ctx.topic.isLocked %}
+ <div class="well icon-well text-warning margin-top-4x">
+ <span class="fa fa-lock text-warning" aria-hidden="true"></span>
+ <em>{{ __("This topic has been locked") }}</em>
+ </div>
+{% elseif currentUser is not null %}
+ {% set formId = "addpost" %}
+ <h3 id="form">{{ __("Reply to this topic") }}</h3>
+ {% set formError = getAndClearFormError(formId) %}
+ {% if formError %}
+ {% include "components/alert_error.twig" with { message: formError } %}
+ {% endif %}
+ <form action="{{ g.server.REQUEST_URI }}#form" method="post" enctype="multipart/form-data">
+ <input type="hidden" name="form_id" value="{{ formId }}">
+ <div class="form-group">
+ <label for="i_message">{{ __("Message:") }}</label>
+ <div class="panel panel-default">
+ <div class="panel-heading" style="padding:4px">
+ <div class="btn-toolbar" role="toolbar">
+ <div class="btn-group" role="group">
+ <button data-area="#i_message" title="{{ __("Bold") }}" data-editor-command="bold" type="button" class="btn btn-default btn-xs"><i class="fa fa-fw fa-bold"></i></button>
+ <button data-area="#i_message" title="{{ __("Italic") }}" data-editor-command="italic" type="button" class="btn btn-default btn-xs"><i class="fa fa-fw fa-italic"></i></button>
+ <button data-area="#i_message" title="{{ __("Underlined") }}" data-editor-command="underline" type="button" class="btn btn-default btn-xs"><i class="fa fa-fw fa-underline"></i></button>
+ <button data-area="#i_message" title="{{ __("Strikethrough") }}" data-editor-command="strikethrough" type="button" class="btn btn-default btn-xs"><i class="fa fa-fw fa-strikethrough"></i></button>
+ </div>
+ <div class="btn-group" role="group">
+ <button data-area="#i_message" title="{{ __("Superscript") }}" data-editor-command="sup" type="button" class="btn btn-default btn-xs"><i class="fa fa-fw fa-superscript"></i></button>
+ <button data-area="#i_message" title="{{ __("Subscript") }}" data-editor-command="sub" type="button" class="btn btn-default btn-xs"><i class="fa fa-fw fa-subscript"></i></button>
+ </div>
+ <div class="btn-group" role="group">
+ <button data-area="#i_message" title="{{ __("Quote") }}" data-editor-command="quote" type="button" class="btn btn-default btn-xs"><i class="fa fa-fw fa-quote-left"></i></button>
+ <button data-area="#i_message" title="{{ __("Spoiler") }}" data-editor-command="spoiler" type="button" class="btn btn-default btn-xs"><i class="fa fa-fw fa-eye"></i></button>
+ </div>
+ </div>
+ </div>
+ <div class="panel-body" style="padding:0">
+ <textarea class="form-control" id="i_message" name="message" required rows="12" cols="60" style="resize:vertical;max-height:499px;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;margin:-1px;width:calc(100% + 2px)"></textarea>
+ </div>
+ </div>
+ </div>
+ <div class="form-group">
+ <label for="i_files">{{ __("Attachments: <small>(max. %max_attachment_count% files, max. %max_attachment_size% MiB each)</small>", {
+ "max_attachment_count": constant("MAX_ATTACHMENT_COUNT"),
+ "max_attachment_size": constant("MAX_ATTACHMENT_SIZE") // (2**20),
+ }) }}</label>
+ <input type="file" name="files[]" id="i_files" multiple accept="*/*">
+ </div>
+ <button type="submit" class="btn btn-success">{{ __("Post reply") }} <span class="fa fa-send" aria-hidden="true"></span></button>
+ </form>
+{% else %}
+ <div class="well margin-top-4x text-center">
+ <div class="h3 margin-top-0">{{ __("Log in to reply to this topic") }}</div>
+ <a href="?_action=auth&amp;next={{ g.server.REQUEST_URI|url_encode }}" class="btn btn-success">
+ <span class="fa fa-user" aria-hidden="true"></span>
+ {{ __("Log in") }}
+ </a>
+ </div>
+{% endif %}
+
+
+{% endblock %}
diff --git a/src/application/templates/bootstrap-3/view_topics.twig b/src/application/templates/bootstrap-3/view_topics.twig
new file mode 100644
index 0000000..899ebe7
--- /dev/null
+++ b/src/application/templates/bootstrap-3/view_topics.twig
@@ -0,0 +1,25 @@
+{% extends "base.twig" %}
+
+{% block content %}
+
+{% if currentUser.hasPermission(permission("CREATE_OWN_TOPIC")) %}
+ <p class="text-right">
+ <a href="?_action=newtopic" class="btn btn-success"><span class="fa fa-plus" aria-hidden="true"></span> {{ __("New topic") }}</a>
+ </p>
+{% endif %}
+
+<div class="list-group">
+ {% for topic in ctx.topics %}
+ <a class="list-group-item" href="?_action=viewtopic&amp;topic={{ topic.id|url_encode }}">
+ <h4 class="list-group-item-heading">
+ {% if topic.isLocked %}
+ <span class="fa fa-lock text-muted" aria-hidden="true"></span>
+ {% endif %}
+ {{ topic.title }}
+ </h4>
+ <p class="list-group-item-text _time">{{ topic.creationDate.format("c") }}</p>
+ </a>
+ {% endfor %}
+</div>
+
+{% endblock %}
diff --git a/src/application/templates/bootstrap-3/view_user.twig b/src/application/templates/bootstrap-3/view_user.twig
new file mode 100644
index 0000000..b4c936c
--- /dev/null
+++ b/src/application/templates/bootstrap-3/view_user.twig
@@ -0,0 +1,201 @@
+{% set canEdit =
+ currentUser is not null
+ and (
+ (
+ ctx.user.id == currentUser.id
+ and currentUser.hasPermission(permission("EDIT_OWN_USER"))
+ )
+ or currentUser.hasPermission(permission("EDIT_OTHER_USER"))
+ ) %}
+
+{% set isOwnProfile =
+ currentUser is not null
+ and currentUser.id == ctx.user.id %}
+
+{% set sUserPossessive = isOwnProfile ? "Your Posts" : "%display_name%'s posts" %}
+
+{% set emailPending = isOwnProfile and ctx.user.pendingEmail is not null %}
+
+{% set title = ctx.user.displayName %}
+
+{% extends "base.twig" %}
+
+{% block content %}
+
+<div class="clearfix page-header margin-top-0">
+ <img class="pull-left margin-right" src="?_action=profilepicture&amp;user={{ ctx.user.id|url_encode }}" alt="{{ __("Profile picture") }}" width="64" height="64">
+ <span class="h1">{{ ctx.user.displayName }}</span>
+ {% if isOwnProfile %}
+ <span class="label label-primary">{{ __("You") }}</span>
+ {% endif %}<br>
+ @{{ ctx.user.name }} &bull; <span class="text-muted">{{ __("Member since %join_date%", {
+ "join_date": '<span class="_date">' ~ (ctx.dateJoined.format("c")|e("html")) ~ '</span>',
+ }) }}</span>
+</div>
+
+{% if canEdit %}
+<div class="row">
+<div class="col-md-9">
+{% endif %}
+
+<h3>{{ __(sUserPossessive, {
+ "display_name": ctx.user.displayName|e("html"),
+}) }}</h3>
+
+{% if ctx.posts|length > 0 %}
+
+ <div class="post-container">
+ <div class="post-container-posts">
+ <div class="list-group margin-top">
+ {% for post in ctx.posts %}
+ <a href="?_action=viewtopic&amp;topic={{ post.topicId|url_encode }}#post-{{ post.id|url_encode }}" class="list-group-item">
+ {% if hasAttachments %}
+ <span class="badge"><span class="fa fa-paperclip"></span></span>
+ {% endif %}
+ {{ renderPostSummary(post.content) }}<br>
+ <span class="text-muted">{{ __("posted on %post_date% in %topic%", {
+ "post_date": '<span class="_time">' ~ post.postDate.format("c")|e("html") ~ '</span>',
+ "topic": '<em>' ~
+ (ctx.topics[post.topicId] is not null and ctx.topics[post.topicId].isLocked ? '<span class="fa fa-lock text-muted" aria-hidden="true"></span> ' : '') ~
+ (ctx.topics[post.topicId] is not null ? ctx.topics[post.topicId].title : __("unknown"))|e("html") ~ '</em>',
+ }) }}</span>
+ </a>
+ {% endfor %}
+ </div>
+ </div>
+ <div class="post-container-controls">
+ <button class="btn btn-default">{{ __("Show all posts") }}</button>
+ </div>
+ </div>
+{% else %}
+ <div class="well icon-well text-info margin-top margin-bottom">
+ <span class="fa fa-info-circle text-info" aria-hidden="true"></span>
+ <em>{{ __("This user has not posted anything yet") }}</em>
+ </div>
+{% endif %}
+
+{% if canEdit %}
+</div> <!-- .col-md-9 -->
+
+<div class="col-md-3">
+ <h3>{{ __("Edit profile") }}</h3>
+ {% set formId = "update_profile" %}
+ {% set formError = getAndClearFormError(formId) %}
+ {% if formError %}
+ {% include "components/alert_error.twig" with { message: formError } %}
+ {% endif %}
+ <form action="{{ g.server.REQUEST_URI }}" method="post" enctype="multipart/form-data">
+ <input type="hidden" name="form_id" value="{{ formId }}">
+ <div class="form-group">
+ <label for="i_display_name">{{ __("Display name:") }}</label>
+ <input required class="form-control" type="text" name="display_name" id="i_display_name" value="{{ ctx.user.displayName }}">
+ </div>
+ <div class="form-group">
+ <label for="i_name">{{ __("Username:") }}</label>
+ {% if ctx.lastNameChangeTooRecent %}
+ <input class="form-control" type="text" id="i_name" value="{{ ctx.user.name }}" disabled>
+ <small class="text-danger"><strong>{{ __("You can only change your username every 30 days!") }}</strong></small>
+ {% else %}
+ <input required class="form-control" type="text" name="name" id="i_name" value="{{ ctx.user.name }}">
+ {% endif %}
+ </div>
+ <div class="form-group">
+ <label for="i_email">{{ __("Email address:") }}</label>
+ {% if emailPending %}
+ <input class="form-control" type="email" id="i_email" value="{{ ctx.user.email }}" disabled>
+ {% else %}
+ <input required class="form-control" type="email" id="i_email" name="email" value="{{ ctx.user.email }}">
+ {% endif %}
+ </div>
+ <div class="form-group">
+ <label>{{ __("Profile picture:") }}</label>
+ <div class="radio margin-top-0 {{ ctx.user.profilePicture is empty ? " disabled text-muted" }}">
+ <label>
+ <input type="radio" name="pfp_action" id="pfp_action_1" value="keep"{{ ctx.user.profilePicture is not empty ? ' checked' : ' disabled' }}>
+ {{ __("Keep current profile picture") }}
+ </label>
+ </div>
+ <div class="radio">
+ <label>
+ <input type="radio" name="pfp_action" id="pfp_action_2" value="remove"{{ ctx.user.profilePicture is empty ? ' checked' : '' }}>
+ {% if ctx.user.profilePicture is empty %}
+ {{ __("No profile picture") }}
+ {% else %}
+ {{ __("Remove profile picture") }}
+ {% endif %}
+ </label>
+ </div>
+ <div class="radio">
+ <label>
+ <input type="radio" name="pfp_action" value="replace" id="pfp_action_3">
+ {{ __("Upload new profile picture") }}
+ </label>
+ </div>
+ <input type="file" name="pfp" id="i_pfp" accept="image/png,image/jpeg" class="margin-left-3x small">
+ </div>
+ <div class="form-group">
+ <button type="submit" class="btn btn-success">{{ __("Save changes") }}</button>
+ </div>
+ </form>
+ {% if isOwnProfile %}
+ <h3>{{ __("Change password") }}</h3>
+ {% set formId = "update_password" %}
+ {% set formError = getAndClearFormError(formId) %}
+ {% if formError %}
+ {% include "components/alert_error.twig" with { message: formError } %}
+ {% endif %}
+ <form action="{{ g.server.REQUEST_URI }}" method="post">
+ <input type="hidden" name="form_id" value="{{ formId }}">
+ <div class="form-group">
+ <label for="i_current_password">{{ __("Current password:") }}</label>
+ <input autocomplete="current-password" required class="form-control" type="password" name="current_password" id="i_current_password" required>
+ </div>
+ <div class="form-group">
+ <label for="i_new_password">{{ __("New password:") }}</label>
+ <input autocomplete="new-password" required class="form-control" type="password" name="new_password" id="i_new_password" required>
+ </div>
+ <div class="form-group">
+ <label for="i_retype_password">{{ __("Retype password:") }}</label>
+ <input autocomplete="new-password" required class="form-control" type="password" name="retype_password" id="i_retype_password" required>
+ </div>
+ <div class="form-group">
+ <button type="submit" class="btn btn-success">{{ __("Change password") }}</button>
+ </div>
+ </form>
+ {% endif %}
+</div> <!-- .col-md-3 -->
+</div> <!-- .row -->
+{% endif %}
+
+<script>
+$(function() {
+ $(".post-container").each(function(i, e) {
+ if ($(e).height() > 900) { // more than 800 so it doesn't collapse just a few pixels
+ $(e).addClass("collapsed");
+ }
+ $(e).find(".post-container-controls button").click(function() {
+ $(e).removeClass("collapsed");
+ });
+ });
+});
+{% if canEdit %}
+$(function() {
+ function _hide() {
+ $("#i_pfp").hide().prop("disabled", true).prop("required", false);
+ $("#i_pfp + .file-input-group").hide().find("button").prop("disabled", true);
+ }
+ _hide();
+ setTimeout(_hide, 10);
+ $("[name='pfp_action']").on("change input check click", function() {
+ if ($("#pfp_action_3").is(":checked")) {
+ $("#i_pfp").show().prop("disabled", false).prop("required", true);
+ $("#i_pfp + .file-input-group").show().find("button").prop("disabled", false);
+ } else {
+ _hide();
+ }
+ })
+});
+{% endif %}
+</script>
+
+{% endblock %}
diff --git a/src/application/views/alert_error.php b/src/application/views/alert_error.php
deleted file mode 100644
index e3b48ba..0000000
--- a/src/application/views/alert_error.php
+++ /dev/null
@@ -1,10 +0,0 @@
-<?php
-if (str_starts_with($message, "?!HTML::"))
- $message = substr($message, 8);
-else
- $message = htmlentities($message);
-?>
-<div class="alert alert-danger" role="alert">
-<span class="fa fa-exclamation-circle" aria-hidden="true"></span>
-<?= $message; ?>
-</div>
diff --git a/src/application/views/alert_info.php b/src/application/views/alert_info.php
deleted file mode 100644
index 7bf2e7b..0000000
--- a/src/application/views/alert_info.php
+++ /dev/null
@@ -1,9 +0,0 @@
-<?php
-if (str_starts_with($message, "?!HTML::"))
- $message = substr($message, 8);
-else
- $message = htmlentities($message);
-?>
-<div class="alert alert-info" role="alert">
-<?= $message; ?>
-</div>
diff --git a/src/application/views/alert_success.php b/src/application/views/alert_success.php
deleted file mode 100644
index de3e023..0000000
--- a/src/application/views/alert_success.php
+++ /dev/null
@@ -1,9 +0,0 @@
-<?php
-if (str_starts_with($message, "?!HTML::"))
- $message = substr($message, 8);
-else
- $message = htmlentities($message);
-?>
-<div class="alert alert-success" role="alert">
-<?= $message; ?>
-</div>
diff --git a/src/application/views/form_addpost.php b/src/application/views/form_addpost.php
deleted file mode 100644
index b2fb2f2..0000000
--- a/src/application/views/form_addpost.php
+++ /dev/null
@@ -1,53 +0,0 @@
-<?php
-
-use mystic\forum\utils\RequestUtils;
-
-$lastFormUri = "";
-$lastForm = RequestUtils::getLastForm($lastFormUri) ?? [];
-if ($lastFormUri !== $_SERVER["REQUEST_URI"]) $lastForm = [];
-RequestUtils::clearLastForm();
-
-?>
-<h3 id="form"><?= __("Reply to this topic") ?></h3>
-<?php
-if (($_formError = RequestUtils::getAndClearFormError("addpost")) !== null) {
- _view("alert_error", ["message" => $_formError]);
-}
-?>
-<form action="<?= htmlentities($_SERVER["REQUEST_URI"]) ?>#form" method="post" enctype="multipart/form-data">
-<input type="hidden" name="form_id" value="addpost">
-<div class="form-group">
- <label for="i_message"><?= __("Message:") ?></label>
- <div class="panel panel-default">
- <div class="panel-heading" style="padding:4px">
- <div class="btn-toolbar" role="toolbar">
- <div class="btn-group" role="group">
- <button data-area="#i_message" title="<?= __("Bold") ?>" data-editor-command="bold" type="button" class="btn btn-default btn-xs"><i class="fa fa-fw fa-bold"></i></button>
- <button data-area="#i_message" title="<?= __("Italic") ?>" data-editor-command="italic" type="button" class="btn btn-default btn-xs"><i class="fa fa-fw fa-italic"></i></button>
- <button data-area="#i_message" title="<?= __("Underlined") ?>" data-editor-command="underline" type="button" class="btn btn-default btn-xs"><i class="fa fa-fw fa-underline"></i></button>
- <button data-area="#i_message" title="<?= __("Strikethrough") ?>" data-editor-command="strikethrough" type="button" class="btn btn-default btn-xs"><i class="fa fa-fw fa-strikethrough"></i></button>
- </div>
- <div class="btn-group" role="group">
- <button data-area="#i_message" title="<?= __("Superscript") ?>" data-editor-command="sup" type="button" class="btn btn-default btn-xs"><i class="fa fa-fw fa-superscript"></i></button>
- <button data-area="#i_message" title="<?= __("Subscript") ?>" data-editor-command="sub" type="button" class="btn btn-default btn-xs"><i class="fa fa-fw fa-subscript"></i></button>
- </div>
- <div class="btn-group" role="group">
- <button data-area="#i_message" title="<?= __("Quote") ?>" data-editor-command="quote" type="button" class="btn btn-default btn-xs"><i class="fa fa-fw fa-quote-left"></i></button>
- <button data-area="#i_message" title="<?= __("Spoiler") ?>" data-editor-command="spoiler" type="button" class="btn btn-default btn-xs"><i class="fa fa-fw fa-eye"></i></button>
- </div>
- </div>
- </div>
- <div class="panel-body" style="padding:0">
- <textarea class="form-control" id="i_message" name="message" required rows="12" cols="60" style="resize:vertical;max-height:499px;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;margin:-1px;width:calc(100% + 2px)"></textarea>
- </div>
- </div>
-</div>
-<div class="form-group">
- <label for="i_files"><?= __("Attachments: <small>(max. %max_attachment_count% files, max. %max_attachment_size% MiB each)</small>", [
- "max_attachment_count" => strval(MAX_ATTACHMENT_COUNT),
- "max_attachment_size" => strval(MAX_ATTACHMENT_SIZE >> 20)
- ]) ?></label>
- <input type="file" name="files[]" id="i_files" multiple accept="*/*">
-</div>
-<button type="submit" class="btn btn-success"><?= __("Post reply") ?> <span class="fa fa-send" aria-hidden="true"></span></button>
-</form>
diff --git a/src/application/views/form_delete_post_confirm.php b/src/application/views/form_delete_post_confirm.php
deleted file mode 100644
index e127235..0000000
--- a/src/application/views/form_delete_post_confirm.php
+++ /dev/null
@@ -1,26 +0,0 @@
-<div class="panel panel-danger">
- <div class="panel-heading">
- <h3 class="panel-title"><?= __("Do you want to delete this post?") ?></h3>
- </div>
- <div class="panel-body">
- <?= __("Are you sure you want to delete the following post:") ?><br>
- <?php _view("view_post", [
- ...$___PARAMS,
- "hide_actions" => true
- ]) ?>
- </div>
- <div class="panel-footer">
- <div class="text-right">
- <form action=".#post-<?= htmlentities(urlencode($post->id)) ?>" method="get" class="seamless-inline">
- <input type="hidden" name="_action" value="viewtopic">
- <input type="hidden" name="topic" value="<?= htmlentities($post->topicId) ?>">
- <button class="btn btn-default"><?= __("Keep post") ?></button>
- </form>
- <form action="?_action=deletepost" method="post" class="seamless-inline">
- <input type="hidden" name="post" value="<?= htmlentities($post->id) ?>">
- <input type="hidden" name="confirm" value="<?= htmlentities(base64_encode(hash("sha256", "confirm" . $post->id, true))); ?>">
- <button class="btn btn-danger"><?= __("Delete post") ?></button>
- </form>
- </div>
- </div>
-</div>
diff --git a/src/application/views/form_delete_topic_confirm.php b/src/application/views/form_delete_topic_confirm.php
deleted file mode 100644
index 4f9deec..0000000
--- a/src/application/views/form_delete_topic_confirm.php
+++ /dev/null
@@ -1,24 +0,0 @@
-<div class="panel panel-danger">
- <div class="panel-heading">
- <h3 class="panel-title"><?= __("Do you want to delete this topic?") ?></h3>
- </div>
- <div class="panel-body">
- <?= __("Are you sure you want to delete the topic <em>%topic%</em> including <strong>all posts and attachments</strong>?", [
- "topic" => htmlentities($topic->title),
- ]) ?>
- </div>
- <div class="panel-footer">
- <div class="text-right">
- <form action="." method="get" class="seamless-inline">
- <input type="hidden" name="_action" value="viewtopic">
- <input type="hidden" name="topic" value="<?= htmlentities($topic->id) ?>">
- <button class="btn btn-default"><?= __("Keep topic") ?></button>
- </form>
- <form action="?_action=deletetopic" method="post" class="seamless-inline">
- <input type="hidden" name="topic" value="<?= htmlentities($topic->id) ?>">
- <input type="hidden" name="confirm" value="<?= htmlentities(base64_encode(hash("sha256", "confirm" . $topic->id, true))); ?>">
- <button class="btn btn-danger"><?= __("Delete topic &amp; posts") ?></button>
- </form>
- </div>
- </div>
-</div>
diff --git a/src/application/views/form_login.php b/src/application/views/form_login.php
deleted file mode 100644
index 5fa79ad..0000000
--- a/src/application/views/form_login.php
+++ /dev/null
@@ -1,51 +0,0 @@
-<?php
-
-use mystic\forum\Messaging;
-use mystic\forum\utils\RequestUtils;
-
-$lastFormUri = "";
-$lastForm = RequestUtils::getLastForm($lastFormUri) ?? [];
-if ($lastFormUri !== $_SERVER["REQUEST_URI"]) $lastForm = [];
-RequestUtils::clearLastForm();
-
-?>
-<div class="page-header margin-top-0">
- <h1><?= __("Log in") ?></h1>
-</div>
-<div class="col-md-4"></div>
-<div class="well col-md-4">
-<?php
-if (($_formError = RequestUtils::getAndClearFormError("login")) !== null) {
- _view("alert_error", ["message" => $_formError]);
-}
-?>
-<form action="<?= htmlentities($_SERVER["REQUEST_URI"]) ?>" method="post">
-<input type="hidden" name="form_id" value="login">
-<input type="hidden" name="token" value="<?= htmlentities($token) ?>">
-<input type="hidden" name="sig" value="<?= htmlentities($signature) ?>">
-<div class="form-group">
- <label for="i_username"><?= __("Username:") ?></label>
- <input class="form-control" type="text" id="i_username" name="username" value="<?= htmlentities($lastForm["username"] ?? "") ?>" required autofocus>
-</div>
-
-<div class="form-group">
- <label for="i_password"><?= __("Password:") ?></label>
- <input class="form-control" type="password" id="i_password" name="password" required>
-</div>
-
-<div class="form-group">
- <button class="btn btn-primary" type="submit"><?= __("Log in") ?></button>
- <a class="btn btn-link" href="?_action=pwreset"><?= __("I forgot my password") ?></a>
-</div>
-
-<?php if (REGISTRATION_ENABLED): ?>
-<div class="form-group">
- <?= __("Don't have an account? %link%Register now%/link%", [
- "link" => '<a href="?_action=register">',
- "/link" => '</a>',
- ]) ?>
-</div>
-<?php endif; ?>
-</form>
-</div>
-<div class="col-md-4"></div>
diff --git a/src/application/views/form_new_password.php b/src/application/views/form_new_password.php
deleted file mode 100644
index 7431bd5..0000000
--- a/src/application/views/form_new_password.php
+++ /dev/null
@@ -1,38 +0,0 @@
-<?php
-
-use mystic\forum\utils\RequestUtils;
-
-$lastFormUri = "";
-$lastForm = RequestUtils::getLastForm($lastFormUri) ?? [];
-if ($lastFormUri !== $_SERVER["REQUEST_URI"]) $lastForm = [];
-RequestUtils::clearLastForm();
-
-?>
-<div class="page-header margin-top-0">
- <h1><?= __("Reset password") ?></h1>
-</div>
-<div class="col-md-4"></div>
-<div class="well col-md-4">
-<?php
-if (($_formError = RequestUtils::getAndClearFormError("pwnew")) !== null) {
- _view("alert_error", ["message" => $_formError]);
-}
-?>
-<form action="<?= htmlentities($_SERVER["REQUEST_URI"]) ?>" method="post">
-<input type="hidden" name="form_id" value="pwnew">
-<div class="form-group">
- <label for="i_new_password"><?= __("New password:") ?></label>
- <input class="form-control" type="password" id="i_new_password" name="new_password" required autofocus>
-</div>
-
-<div class="form-group">
- <label for="i_retype_password"><?= __("Retype password:") ?></label>
- <input class="form-control" type="password" id="i_retype_password" name="retype_password" required>
-</div>
-
-<div class="form-group">
- <button class="btn btn-primary" type="submit"><?= __("Set new password") ?></button>
-</div>
-</form>
-</div>
-<div class="col-md-4"></div>
diff --git a/src/application/views/form_newtopic.php b/src/application/views/form_newtopic.php
deleted file mode 100644
index e7050c7..0000000
--- a/src/application/views/form_newtopic.php
+++ /dev/null
@@ -1,60 +0,0 @@
-<?php
-
-use mystic\forum\Messaging;
-use mystic\forum\utils\RequestUtils;
-
-$lastFormUri = "";
-$lastForm = RequestUtils::getLastForm($lastFormUri) ?? [];
-if ($lastFormUri !== $_SERVER["REQUEST_URI"]) $lastForm = [];
-RequestUtils::clearLastForm();
-
-?>
-<div class="page-header margin-top-0">
- <h1><?= __("New topic") ?></h1>
-</div>
-<?php
-if (($_formError = RequestUtils::getAndClearFormError("newtopic")) !== null) {
- _view("alert_error", ["message" => $_formError]);
-}
-?>
-<form action="<?= htmlentities($_SERVER["REQUEST_URI"]) ?>#form" method="post" enctype="multipart/form-data">
-<input type="hidden" name="form_id" value="newtopic">
-<div class="form-group">
- <label for="i_message"><?= __("Topic title:") ?></label>
- <input type="text" class="form-control" id="i_title" name="title" value="<?= htmlentities($lastForm["title"] ?? "") ?>" required autofocus>
-</div>
-<div class="form-group">
- <label for="i_message"><?= __("Message:") ?></label>
- <div class="panel panel-default">
- <div class="panel-heading" style="padding:4px">
- <div class="btn-toolbar" role="toolbar">
- <div class="btn-group" role="group">
- <button data-area="#i_message" title="<?= __("Bold") ?>" data-editor-command="bold" type="button" class="btn btn-default btn-xs"><i class="fa fa-fw fa-bold"></i></button>
- <button data-area="#i_message" title="<?= __("Italic") ?>" data-editor-command="italic" type="button" class="btn btn-default btn-xs"><i class="fa fa-fw fa-italic"></i></button>
- <button data-area="#i_message" title="<?= __("Underlined") ?>" data-editor-command="underline" type="button" class="btn btn-default btn-xs"><i class="fa fa-fw fa-underline"></i></button>
- <button data-area="#i_message" title="<?= __("Strikethrough") ?>" data-editor-command="strikethrough" type="button" class="btn btn-default btn-xs"><i class="fa fa-fw fa-strikethrough"></i></button>
- </div>
- <div class="btn-group" role="group">
- <button data-area="#i_message" title="<?= __("Superscript") ?>" data-editor-command="sup" type="button" class="btn btn-default btn-xs"><i class="fa fa-fw fa-superscript"></i></button>
- <button data-area="#i_message" title="<?= __("Subscript") ?>" data-editor-command="sub" type="button" class="btn btn-default btn-xs"><i class="fa fa-fw fa-subscript"></i></button>
- </div>
- <div class="btn-group" role="group">
- <button data-area="#i_message" title="<?= __("Quote") ?>" data-editor-command="quote" type="button" class="btn btn-default btn-xs"><i class="fa fa-fw fa-quote-left"></i></button>
- <button data-area="#i_message" title="<?= __("Spoiler") ?>" data-editor-command="spoiler" type="button" class="btn btn-default btn-xs"><i class="fa fa-fw fa-eye"></i></button>
- </div>
- </div>
- </div>
- <div class="panel-body" style="padding:0">
- <textarea class="form-control" id="i_message" name="message" required rows="12" cols="60" style="resize:vertical;max-height:499px;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;margin:-1px;width:calc(100% + 2px)"><?= htmlentities($lastForm["message"] ?? "") ?></textarea>
- </div>
- </div>
-</div>
-<div class="form-group">
- <label for="i_files"><?= __("Attachments: <small>(max. %max_attachment_count% files, max. %max_attachment_size% MiB each)</small>", [
- "max_attachment_count" => strval(MAX_ATTACHMENT_COUNT),
- "max_attachment_size" => strval(MAX_ATTACHMENT_SIZE >> 20)
- ]) ?></label>
- <input type="file" name="files[]" id="i_files" multiple accept="*/*">
-</div>
-<button type="submit" class="btn btn-success"><?= __("Create topic") ?> <span class="fa fa-send" aria-hidden="true"></span></button>
-</form>
diff --git a/src/application/views/form_password_reset.php b/src/application/views/form_password_reset.php
deleted file mode 100644
index 57d8ed2..0000000
--- a/src/application/views/form_password_reset.php
+++ /dev/null
@@ -1,41 +0,0 @@
-<?php
-
-use mystic\forum\Messaging;
-use mystic\forum\utils\RequestUtils;
-
-$lastFormUri = "";
-$lastForm = RequestUtils::getLastForm($lastFormUri) ?? [];
-if ($lastFormUri !== $_SERVER["REQUEST_URI"]) $lastForm = [];
-RequestUtils::clearLastForm();
-
-?>
-<div class="page-header margin-top-0">
- <h1><?= __("Reset password") ?></h1>
-</div>
-<div class="col-md-4"></div>
-<div class="well col-md-4">
-<?php
-if (($_formError = RequestUtils::getAndClearFormError("pwreset")) !== null) {
- _view("alert_error", ["message" => $_formError]);
-}
-?>
-<form action="<?= htmlentities($_SERVER["REQUEST_URI"]) ?>" method="post">
-<input type="hidden" name="form_id" value="pwreset">
-<div class="form-group">
- <label for="i_username"><?= __("Email address:") ?></label>
- <input class="form-control" type="email" id="i_email" name="email" value="<?= htmlentities($lastForm["email"] ?? "") ?>" required autofocus>
-</div>
-
-<div class="form-group">
- <button class="btn btn-primary" type="submit"><?= __("Reset password") ?></button>
-</div>
-
-<div class="form-group">
- <?= __("I know my password and I want to %link%log in%/link%!", [
- "link" => '<a href="?_action=auth">',
- "/link" => '</a>',
- ]) ?>
-</div>
-</form>
-</div>
-<div class="col-md-4"></div>
diff --git a/src/application/views/form_register.php b/src/application/views/form_register.php
deleted file mode 100644
index 54da2c4..0000000
--- a/src/application/views/form_register.php
+++ /dev/null
@@ -1,88 +0,0 @@
-<?php
-
-use mystic\forum\Messaging;
-use mystic\forum\utils\RequestUtils;
-
-$lastFormUri = "";
-$lastForm = RequestUtils::getLastForm($lastFormUri) ?? [];
-if ($lastFormUri !== $_SERVER["REQUEST_URI"]) $lastForm = [];
-RequestUtils::clearLastForm();
-
-?>
-<div class="page-header margin-top-0">
- <h1><?= __("Register") ?></h1>
-</div>
-<div class="col-md-4"></div>
-<div class="well col-md-4">
-<?php
-if (($_formError = RequestUtils::getAndClearFormError("register")) !== null) {
- _view("alert_error", ["message" => $_formError]);
-}
-?>
-<form action="<?= htmlentities($_SERVER["REQUEST_URI"]) ?>" method="post">
-<input type="hidden" name="form_id" value="register">
-<div class="form-group" id="group0">
- <label for="i_username"><?= __("Username:") ?></label>
- <input class="form-control" id="i_username" type="text" name="username" value="" required>
-</div>
-
-<div class="form-group" id="group1">
- <label for="i_username"><?= __("Username:") ?></label>
- <input class="form-control" id="i_username" type="text" name="df82a9bc21" value="<?= htmlentities($lastForm["df82a9bc21"] ?? "") ?>" required autofocus>
-</div>
-
-<div class="form-group" id="group2">
- <label for="i_display_name"><?= __("Display name:") ?></label>
- <input class="form-control" id="i_display_name" type="text" name="display_name" value="<?= htmlentities($lastForm["display_name"] ?? "") ?>" required>
-</div>
-
-<div class="form-group" id="group3">
- <label for="i_password"><?= __("Choose password:") ?></label>
- <input class="form-control" id="i_password" type="password" name="password" required>
-</div>
-
-<div class="form-group" id="group4">
- <label for="i_password_retype"><?= __("Repeat password:") ?></label>
- <input class="form-control" id="i_password_retype" type="password" name="password_retype" required>
-</div>
-
-<div class="form-group" id="group5">
- <label for="i_email"><?= __("Email address:") ?></label>
- <input class="form-control" id="i_email" type="email" name="email" value="<?= htmlentities($lastForm["email"] ?? "") ?>" required>
-</div>
-
-<div class="form-group" id="group6">
- <label for="i_email"><?= __("CAPTCHA:") ?></label>
- <div class="text-center margin-bottom">
- <img src="?_action=captcha&t=<?= htmlentities(strval(microtime(true) * 1000)) ?>" alt="CAPTCHA" width="192" height="48" id="captcha-img">
- </div>
- <div class="input-group">
- <input type="text" name="captcha" id="i_captcha" class="form-control" required>
- <div class="input-group-btn">
- <button class="btn btn-default" type="button" id="btn-refresh-captcha"><span class="fa fa-refresh" aria-hidden="true"></span><span class="sr-only"><?= __("New CAPTCHA") ?></span></button>
- </div>
- </div>
-</div>
-
-<div class="form-group">
- <button class="btn btn-primary" type="submit"><?= __("Register now") ?></button>
-</div>
-
-<div class="form-group">
- <?= __("Already have an account? %link%Sign in now%/link%", [
- "link" => '<a href="?_action=auth">',
- "/link" => '</a>',
- ]) ?>
-</div>
-</form>
-</div>
-<div class="col-md-4"></div>
-
-<script>
-$(function() {
- $("#btn-refresh-captcha").click(function() {
- $("#captcha-img").attr("src", "?_action=captcha&t=" + new Date().getTime().toString());
- });
- $("#i_username").prop("disabled", true).prop("required", false);
-});
-</script>
diff --git a/src/application/views/form_search.php b/src/application/views/form_search.php
deleted file mode 100644
index 394a0e7..0000000
--- a/src/application/views/form_search.php
+++ /dev/null
@@ -1,30 +0,0 @@
-<?php
-
-use mystic\forum\utils\RequestUtils;
-
-$lastFormUri = "";
-$lastForm = RequestUtils::getLastForm($lastFormUri) ?? [];
-if ($lastFormUri !== $_SERVER["REQUEST_URI"]) $lastForm = [];
-RequestUtils::clearLastForm();
-
-?>
-<div class="page-header margin-top-0">
- <h1><?= __("Search") ?></h1>
-</div>
-<?php
-if (($_formError = RequestUtils::getAndClearFormError("search")) !== null) {
- _view("alert_error", ["message" => $_formError]);
-}
-?>
-<form action="." method="get">
- <input type="hidden" name="form_id" value="search">
- <input type="hidden" name="_action" value="search">
- <div class="form-group">
- <div class="input-group">
- <input class="form-control" type="search" id="i_query" name="query" value="<?= htmlentities($lastForm["query"] ?? $query ?? "") ?>" required autofocus placeholder="<?= htmlentities(__("Enter your search query...")) ?>">
- <div class="input-group-btn">
- <button class="btn btn-primary" type="submit"><?= __("Search") ?></button>
- </div>
- </div>
- </div>
-</form>
diff --git a/src/application/views/nav_guest.php b/src/application/views/nav_guest.php
deleted file mode 100644
index 95c236e..0000000
--- a/src/application/views/nav_guest.php
+++ /dev/null
@@ -1,23 +0,0 @@
-<?php
-
-$nextParam = "";
-
-switch ($GLOBALS["action"]) {
- case "auth":
- case "register":
- $nextParam = $_GET["next"] ?? "";
- break;
- default:
- $nextParam = $_SERVER["REQUEST_URI"];
-}
-
-if ($GLOBALS["action"] === "login")
-
-?>
-<ul class="nav navbar-nav navbar-right">
-<li<?= $GLOBALS["action"] === "search" ? ' class="active"' : '' ?>><a href="?_action=search"><span class="fa fa-search" aria-hidden="true"></span><span class="sr-only"><?= __("Search") ?></span></a></li>
-<li<?= $GLOBALS["action"] === "auth" ? ' class="active"' : '' ?>><a href="?_action=auth&amp;next=<?= htmlentities(urlencode($nextParam)) ?>"><?= __("Log in") ?></a></li>
-<?php if (REGISTRATION_ENABLED): ?>
-<li<?= $GLOBALS["action"] === "register" ? ' class="active"' : '' ?>><a href="?_action=register&amp;next=<?= htmlentities(urlencode($nextParam)) ?>"><?= __("Register") ?></a></li>
-<?php endif; ?>
-</ul> \ No newline at end of file
diff --git a/src/application/views/nav_logged_in.php b/src/application/views/nav_logged_in.php
deleted file mode 100644
index 8dbb325..0000000
--- a/src/application/views/nav_logged_in.php
+++ /dev/null
@@ -1,12 +0,0 @@
-<?php
-use mystic\forum\orm\User;
-?>
-<ul class="nav navbar-nav navbar-right">
-<li><p class="navbar-text"><?= __("Welcome, %user%!", [
- "user" => ($user->id === User::SUPERUSER_ID) ? ('<strong class="text-danger">' . htmlentities($user->displayName) . '</strong>') : ('<strong>' . htmlentities($user->displayName) . '</strong>')
-]) ?>
-</p></li>
-<li<?= $GLOBALS["action"] === "search" ? ' class="active"' : '' ?>><a href="?_action=search"><span class="fa fa-search" aria-hidden="true"></span><span class="sr-only"><?= __("Search") ?></span></a></li>
-<li<?= ($isViewingOwnProfile ?? false) ? ' class="active"' : '' ?>><a href="?_action=viewuser&amp;user=<?= htmlentities(urlencode($user->id)) ?>"><span class="fa fa-user" aria-hidden="true"></span><span class="sr-only">View profile</span></a></li>
-<li><a href="?_action=logout&amp;next=<?= htmlentities(urlencode($_SERVER["REQUEST_URI"])) ?>"><span class="fa fa-sign-out" aria-hidden="true"></span><span class="sr-only">Log out</span></a></li>
-</ul>
diff --git a/src/application/views/template_end.php b/src/application/views/template_end.php
deleted file mode 100644
index b162def..0000000
--- a/src/application/views/template_end.php
+++ /dev/null
@@ -1,165 +0,0 @@
-</div>
-<footer class="footer">
-<div class="container">
-<div class="panel panel-default">
-<div class="panel-body">
- <table style="border-collapse: collapse; width: 100%; background: none">
- <tbody>
- <tr>
- <td style="padding: 0; vertical-align: middle; text-align: left; width: 100%" class="text-normal">
- &copy; <?= date("Y") ?> <?= htmlentities(env("MYSTIC_FORUM_COPYRIGHT") ?? env("MYSTIC_FORUM_TITLE") ?? "Forum") ?>.
- Powered by <a href="https://git.jkohl.link/mystic-forum.git/tag/?h=v<?= htmlentities(urlencode(MYSTICBB_VERSION)) ?>">mysticBB v<?= htmlentities(MYSTICBB_VERSION) ?></a>.
- </td>
- <td style="padding: 0; vertical-align: middle; text-align: right; white-space: nowrap" class="text-normal">
- <form action="?_action=settheme" class="form-inline seamless-inline" method="post">
- <input type="hidden" name="next" value="<?= htmlentities($_SERVER["REQUEST_URI"]) ?>">
- <div class="form-group">
- <label for="theme-select"><?= __("Theme:") ?></label>
- <select class="form-control input-sm auto-submit" id="theme-select" name="theme">
- <?php foreach ($availableThemes as $themeKey => $themeInfo): ?>
- <option value="<?= htmlentities($themeKey) ?>" <?= $themeKey === $currentTheme ? " selected" : "" ?>><?= htmlentities($themeInfo->name) ?></option>
- <?php endforeach; ?>
- </select>
- </div>
- </form>
- &nbsp;&nbsp;&nbsp;
- <form action="?_action=setlang" class="form-inline seamless-inline" method="post">
- <input type="hidden" name="next" value="<?= htmlentities($_SERVER["REQUEST_URI"]) ?>">
- <div class="form-group">
- <label for="lang-select"><?= __("Language:") ?></label>
- <select class="form-control input-sm auto-submit" id="lang-select" name="lang">
- <?php foreach ($availableLangs as $langKey => $langName): ?>
- <option value="<?= htmlentities($langKey) ?>" <?= $langKey === $currentLang ? " selected" : "" ?>><?= htmlentities($langName) ?></option>
- <?php endforeach; ?>
- </select>
- </div>
- </form>
- </td>
- </tr>
- </tbody>
- </table>
-</div>
-</div>
-</div>
-</footer>
-
-<script type="text/javascript">
- $(function() {
- function insertAroundSelection(textarea, before, after) {
- var start = textarea.selectionStart;
- var end = textarea.selectionEnd;
- var text = textarea.value;
- var pre = text.substring(0, start);
- var inner = text.substring(start, end);
- var post = text.substring(end);
- start += before.length;
- end += before.length;
- text = pre + before + inner + after + post;
- textarea.value = text;
- textarea.focus();
- textarea.selectionStart = start;
- textarea.selectionEnd = end;
- }
-
- function getTextarea(btn) {
- return $($(btn).attr("data-area"))[0];
- }
-
- var commands = {
- bold: function(textarea) {
- insertAroundSelection(textarea, "[b]", "[/b]");
- },
- italic: function(textarea) {
- insertAroundSelection(textarea, "[i]", "[/i]");
- },
- underline: function(textarea) {
- insertAroundSelection(textarea, "[u]", "[/u]");
- },
- strikethrough: function(textarea) {
- insertAroundSelection(textarea, "[s]", "[/s]");
- },
- sup: function(textarea) {
- insertAroundSelection(textarea, "[^]", "[/^]");
- },
- sub: function(textarea) {
- insertAroundSelection(textarea, "[_]", "[/_]");
- },
- quote: function(textarea) {
- insertAroundSelection(textarea, "> ", "");
- },
- spoiler: function(textarea) {
- insertAroundSelection(textarea, "[spoiler]", "[/spoiler]");
- }
- }
-
- $("button[data-editor-command]").attr("data-toggle", "tooltip").attr("data-placement", "bottom").click(function() {
- var command = $(this).attr("data-editor-command");
- var textarea = getTextarea(this);
- commands[command](textarea);
- }).tooltip();
- });
-</script>
-
-<script>
-$(function() {
- var _messages = <?= json_encode([
- "selectFiles" => [
- ___("Select file", "Select files", 1),
- ___("Select file", "Select files", 2),
- ],
- "filesSelected" => [
- ___("%n% file selected", "No files selected", 0),
- ___("%n% file selected", "%n% files selected", 1),
- ___("%n% file selected", "%n% files selected", 2),
- ],
- ]) ?>
-
- $(".auto-submit").on("change", function() {
- $(this)[0].form.submit();
- });
-
- $("._time").each(function(i, e) {
- var date = new Date($(e).text());
- $(e).text(date.toLocaleString());
- });
- $("._date").each(function(i, e) {
- var date = new Date($(e).text());
- $(e).text(date.toLocaleDateString());
- });
- $("._time-only").each(function(i, e) {
- var date = new Date($(e).text());
- $(e).text(date.toLocaleTimeString());
- });
-
- $("input[type=file]").each(function(i, e) {
- var isMultiple = !!$(e).prop("multiple");
- var isSmall = !!$(e).hasClass("small");
- var $input = $('<input type="text" readonly class="form-control" />').attr("placeholder", _messages.filesSelected[0]).css("text-overflow", "ellipsis");
- var $btn = $('<button class="btn btn-default" type="button"></button>');
- if (isSmall) {
- $input.addClass("input-sm");
- $btn.addClass("btn-sm");
- }
- $(e).after($('<div class="input-group file-input-group"></div>').append(
- $input,
- $('<span class="input-group-btn"></span>').append(
- $btn.text(_messages.selectFiles[isMultiple ? 1 : 0]).click(function() {
- $(e).click();
- })
- )
- )).addClass("sr-only");
- $(e).on("change", function() {
- var files = $(e)[0].files;
- if (files.length < 1)
- $input.val("");
- else if (files.length === 1)
- $input.val(files[0].name);
- else
- $input.val(_messages.filesSelected[2].replace("%n%", files.length));
- });
- })
-});
-</script>
-
-</body>
-</html>
diff --git a/src/application/views/template_navigation.php b/src/application/views/template_navigation.php
deleted file mode 100644
index ff0752b..0000000
--- a/src/application/views/template_navigation.php
+++ /dev/null
@@ -1,9 +0,0 @@
-<?php
-if ($user) {
- _view("nav_logged_in", [
- "user" => $user,
- "isViewingOwnProfile" => $isViewingOwnProfile ?? false,
- ]);
-} else {
- _view("nav_guest");
-}
diff --git a/src/application/views/template_navigation_end.php b/src/application/views/template_navigation_end.php
deleted file mode 100644
index a43919c..0000000
--- a/src/application/views/template_navigation_end.php
+++ /dev/null
@@ -1,4 +0,0 @@
-</div>
-</div>
-</nav>
-<div class="container">
diff --git a/src/application/views/template_navigation_start.php b/src/application/views/template_navigation_start.php
deleted file mode 100644
index bd2b3a5..0000000
--- a/src/application/views/template_navigation_start.php
+++ /dev/null
@@ -1,15 +0,0 @@
-<nav class="navbar navbar-default navbar-static-top">
- <div class="container">
- <div class="navbar-header">
- <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#nav-collapse" aria-expanded="false">
- <span class="sr-only">Toggle navigation</span>
- <span class="icon-bar"></span>
- <span class="icon-bar"></span>
- <span class="icon-bar"></span>
- </button>
- <a class="navbar-brand" href=".">
- <span class="myb-icon mybblogo" aria-hidden="false"></span>
- <?= htmlentities(env("MYSTIC_FORUM_TITLE") ?? "Forum") ?>
- </a>
- </div>
- <div class="collapse navbar-collapse" id="nav-collapse">
diff --git a/src/application/views/template_start.php b/src/application/views/template_start.php
deleted file mode 100644
index cf3daf6..0000000
--- a/src/application/views/template_start.php
+++ /dev/null
@@ -1,30 +0,0 @@
-<?php
-
-$pageTitle = "";
-if (isset($_title) && $_title !== null)
- $pageTitle = "$_title | ";
-$pageTitle .= env("MYSTIC_FORUM_TITLE") ?? "Forum";
-
-?>
-<!DOCTYPE html>
-<!--[if lt IE 7]> <html lang="de" class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
-<!--[if IE 7]> <html lang="de" class="no-js lt-ie9 lt-ie8"> <![endif]-->
-<!--[if IE 8]> <html lang="de" class="no-js lt-ie9"> <![endif]-->
-<!--[if gt IE 8]><!--> <html lang="de" class="no-js"> <!--<![endif]-->
-<head>
- <meta charset="utf-8">
- <meta http-equiv="X-UA-Compatible" content="IE=edge">
- <meta name="viewport" content="width=device-width,initial-scale=1">
- <meta name="generator" content="mysticBB <?= htmlentities(MYSTICBB_VERSION) ?>">
- <title><?= htmlentities($pageTitle) ?></title>
- <link rel="stylesheet" href="?_action=ctheme">
- <script src="/ui/jquery-1.12.4.min.js"></script>
- <script src="/ui/dist/js/bootstrap.min.js"></script>
- <script src="/ui/modernizr-2.6.2.min.js"></script>
- <script src="?_action=ji18n"></script>
- <!--[if lt IE 9]>
- <script src="/ui/html5shiv.min.js"></script>
- <script src="/ui/respond.min.js"></script>
- <![endif]-->
-</head>
-<body>
diff --git a/src/application/views/view_logintoreply.php b/src/application/views/view_logintoreply.php
deleted file mode 100644
index 1b5983a..0000000
--- a/src/application/views/view_logintoreply.php
+++ /dev/null
@@ -1,7 +0,0 @@
-<div class="well margin-top-4x text-center">
- <div class="h3 margin-top-0"><?= __("Log in to reply to this topic") ?></div>
- <a href="?_action=auth&amp;next=<?= htmlentities(urlencode($_SERVER["REQUEST_URI"])) ?>" class="btn btn-success">
- <span class="fa fa-user" aria-hidden="true"></span>
- <?= __("Log in") ?>
- </a>
-</div> \ No newline at end of file
diff --git a/src/application/views/view_post.php b/src/application/views/view_post.php
deleted file mode 100644
index e2fcc36..0000000
--- a/src/application/views/view_post.php
+++ /dev/null
@@ -1,150 +0,0 @@
-<?php
-
-use mystic\forum\orm\UserPermissions;
-use mystic\forum\orm\Attachment;
-
-/** @var mystic\forum\orm\Post $post */
-/** @var mystic\forum\orm\User $postAuthor */
-/** @var mystic\forum\orm\Topic $topic */
-
-$isImage = fn(string $m) => str_starts_with($m, "image/") || str_starts_with($m, "video/");
-
-$fileAttachments = array_filter($attachments, fn(Attachment $a) => !$isImage($a->mimeType));
-$imageAttachments = array_filter($attachments, fn(Attachment $a) => $isImage($a->mimeType));
-
-$canReply = !$topic->isLocked && ($GLOBALS["currentUser"]?->hasPermission(UserPermissions::CREATE_OWN_POST) ?? false);
-
-$canEdit = ($GLOBALS["currentUser"]?->id === $postAuthor?->id && $postAuthor?->hasPermission(UserPermissions::EDIT_OWN_POST))
- || ($GLOBALS["currentUser"]?->hasPermission(UserPermissions::EDIT_OTHER_POST));
-
-$canEdit = $canEdit && !$topic->isLocked;
-
-$canDelete = ($GLOBALS["currentUser"]?->id === $postAuthor?->id && $postAuthor?->hasPermission(UserPermissions::DELETE_OWN_POST))
- || ($GLOBALS["currentUser"]?->hasPermission(UserPermissions::DELETE_OTHER_POST));
-
-$canViewAttachments = $GLOBALS["currentUser"] !== null;
-
-$hide_actions ??= false;
-$hide_pfp ??= false;
-$your_are_the_author = $GLOBALS["currentUser"]?->id === $postAuthor?->id;
-$is_op = $postAuthor?->id === $topicAuthor?->id && $postAuthor?->id !== null;
-
-?>
-
-<?php if ($post->deleted): ?>
-<div class="media" id="post-<?= htmlentities($post->id) ?>">
-<div class="media-left hidden-sm hidden-xs">
- <div class="media-object" style="width:64px"></div>
-</div>
-<div class="media-body">
- <div class="well icon-well text-warning">
- <span class="fa fa-exclamation-triangle text-warning" aria-hidden="true"></span>
- <em><?= __("This post has been deleted") ?></em>
- </div>
-</div>
-</div>
-<?php else: ?>
-<div class="media" id="post-<?= htmlentities($post->id) ?>" data-text="<?= htmlentities($post->content) ?>" style="overflow: visible;">
-<?php if (!$hide_pfp): ?>
- <div class="media-left hidden-sm hidden-xs">
- <?php if ($postAuthor): ?>
- <?php if ($hide_actions): ?>
- <img class="media-object" alt="<?= __("Profile picture") ?>" src="?_action=profilepicture&amp;user=<?= htmlentities(urlencode($postAuthor->id)) ?>" width="64" height="64">
- <?php else: ?>
- <a href="?_action=viewuser&amp;user=<?= htmlentities(urlencode($postAuthor->id)) ?>">
- <img class="media-object" alt="<?= __("Profile picture") ?>" src="?_action=profilepicture&amp;user=<?= htmlentities(urlencode($postAuthor->id)) ?>" width="64" height="64">
- </a>
- <?php endif; ?>
- <?php else: ?>
- <div class="media-object" style="width:64px;height:64px"></div>
- <?php endif; ?>
- </div>
-<?php endif; ?>
- <div class="media-body" style="overflow: visible;">
- <div class="panel panel-default">
- <div class="panel-heading">
- <div class="panel-title h3">
- <?php if (!$hide_actions): ?>
- <div class="pull-right">
- <a href="#post-<?= htmlentities(urlencode($post->id)) ?>" class="btn btn-default"><span class="fa fa-link" aria-hidden="true"></span><span class="sr-only"><?= __("Permalink") ?></span></a>
- <?php if ($canReply): ?>
- <button data-post-id="<?= htmlentities($post->id) ?>" class="btn btn-default js-only _reply-post"><span class="fa fa-comment" aria-hidden="true"></span><span class="sr-only"><?= __("Reply to post") ?></span></button>
- <?php endif; ?>
- <?php if ($canEdit): ?>
- <button data-post-id="<?= htmlentities($post->id) ?>" class="btn btn-default js-only _edit-post"><span class="fa fa-pencil" aria-hidden="true"></span><span class="sr-only"><?= __("Edit post") ?></span></button>
- <?php endif; ?>
- <?php if ($canDelete): ?>
- <form action="?_action=deletepost" method="post" class="seamless-inline">
- <input type="hidden" name="post" value="<?= htmlentities($post->id) ?>">
- <button type="submit" class="btn btn-danger"><span class="fa fa-trash" aria-hidden="true"></span><span class="sr-only"><?= __("Delete post") ?></span></button>
- </form>
- <?php endif; ?>
- </div>
- <?php endif; ?>
- <?php if ($postAuthor): ?>
- <?php if ($hide_actions): ?>
- <?= htmlentities($postAuthor->displayName) ?>
- <?php else: ?>
- <a href="?_action=viewuser&amp;user=<?= htmlentities(urlencode($postAuthor->id)) ?>"><?= htmlentities($postAuthor->displayName) ?></a>
- <?php if ($is_op): ?>
- <span title="<?= __("Created this topic") ?>" class="text-info fa fa-user-circle"></span>
- <?php endif; ?>
- <?php endif; ?>
- <?php if ($your_are_the_author): ?>
- <span class="text-normal label label-primary"><?= __("You") ?></span>
- <?php endif; ?>
- <?php else: ?>
- <em class="text-muted"><?= __("(deleted)") ?></em>
- <?php endif; ?>
- </div>
- <span class="_time"><?= $post->postDate->format("c"); ?></span>
- <?php if ($post->edited): ?>
- <em class="text-muted"><?= __("(edited)") ?></em>
- <?php endif; ?>
- </div>
- <div class="panel-body">
- <div class="post-content"><?= renderPost($post->content) ?></div>
- <?php if (count($imageAttachments) > 0): ?>
- <div class="post-images clearfix">
- <?php /** @var Attachment $attachment */ foreach ($imageAttachments as $attachment): ?>
- <?php if ($hide_actions): ?>
- <span class="image-attachment" title="<?= htmlentities($attachment->name) ?>">
- <img class="image-attachment-image" src="?_action=thumb&amp;attachment=<?= htmlentities(urlencode($attachment->id)) ?>" alt="" width="100">
- </span>
- <?php else: ?>
- <a class="image-attachment attachment<?php if (str_starts_with($attachment->mimeType, "video/")): ?> video-attachment<?php endif; ?>" href="?_action=attachment&amp;attachment=<?= htmlentities(urlencode($attachment->id)) ?>" title="<?= htmlentities($attachment->name) ?>" data-attachment-id="<?= htmlentities($attachment->id) ?>">
- <img class="image-attachment-image" src="?_action=thumb&amp;attachment=<?= htmlentities(urlencode($attachment->id)) ?>" alt="" width="100">
- <?php if (!$canViewAttachments): ?>
- <span class="attachment-lock fa fa-ban" aria-hidden="true"></span>
- <?php elseif (str_starts_with($attachment->mimeType, "video/")): ?>
- <span class="video-player-icon fa fa-play-circle" aria-hidden="true"></span>
- <?php endif; ?>
- </a>
- <?php endif; ?>
- <?php endforeach; ?>
- </div>
- <?php endif; ?>
- </div>
- <?php if (count($fileAttachments) > 0): ?>
- <div class="panel-footer">
- <div class="btn-group">
- <?php /** @var Attachment $attachment */ foreach ($fileAttachments as $attachment): ?>
- <?php if ($hide_actions): ?>
- <button class="btn btn-default"><?= htmlentities($attachment->name) ?></button>
- <?php else: ?>
- <a class="btn btn-default attachment" href="?_action=attachment&amp;attachment=<?= htmlentities(urlencode($attachment->id)) ?>">
- <?php if (!$canViewAttachments): ?>
- <span class="fa fa-ban" aria-hidden="true"></span>
- <?php endif; ?>
- <?= htmlentities($attachment->name) ?>
- </a>
- <?php endif; ?>
- <?php endforeach; ?>
- </div>
- </div>
- <?php endif; ?>
- </div>
- </div>
-</div>
-
-<?php endif; ?>
diff --git a/src/application/views/view_search_results.php b/src/application/views/view_search_results.php
deleted file mode 100644
index 660284d..0000000
--- a/src/application/views/view_search_results.php
+++ /dev/null
@@ -1,35 +0,0 @@
-<?php
-use mystic\forum\utils\StringUtils;
-?>
-
-<?php if (count($posts) > 0): ?>
- <p><?= __("%result_count% result(s) in %search_duration% second(s)", [
- "result_count" => count($posts),
- "search_duration" => number_format($search_duration, 2, __(".", context: "Number formatting"), __(",", context: "Number formatting")),
- ]) ?></p>
- <div class="list-group margin-top">
- <?php foreach ($posts as $post):
- if ($post->deleted) continue;
- $hasAttachments = count($attachments[$post->id]) > 0;
- ?>
- <a href="?_action=viewtopic&amp;topic=<?= htmlentities(urlencode($post->topicId)) ?>#post-<?= htmlentities(urlencode($post->id)) ?>" class="list-group-item">
- <?php if ($hasAttachments): ?>
- <span class="badge"><span class="fa fa-paperclip"></span></span>
- <?php endif; ?>
- <?= renderPostSummary($post->content) ?><br>
- <span class="text-muted"><?= __("posted by %author% on %post_date% in %topic%", [
- "author" => '<em>' . htmlentities($users[$post->authorId]?->displayName ?? __("unknown")) . '</em>',
- "post_date" => '<span class="_time">' . htmlentities($post->postDate->format("c")) . '</span>',
- "topic" => '<em>'
- . ($topics[$post->topicId]?->isLocked ? '<span class="fa fa-lock text-muted" aria-hidden="true"></span> ' : '')
- . htmlentities($topics[$post->topicId]?->title ?? "unknown") . '</em>',
- ]) ?></span>
- </a>
- <?php endforeach; ?>
- </div>
-<?php else: ?>
- <div class="well icon-well text-info margin-top margin-bottom">
- <span class="fa fa-info-circle text-info" aria-hidden="true"></span>
- <em><?= __("No results for this search") ?></em>
- </div>
-<?php endif; ?>
diff --git a/src/application/views/view_topic_end.php b/src/application/views/view_topic_end.php
deleted file mode 100644
index e69de29..0000000
--- a/src/application/views/view_topic_end.php
+++ /dev/null
diff --git a/src/application/views/view_topic_locked.php b/src/application/views/view_topic_locked.php
deleted file mode 100644
index b3eb99e..0000000
--- a/src/application/views/view_topic_locked.php
+++ /dev/null
@@ -1,4 +0,0 @@
-<div class="well icon-well text-warning margin-top-4x">
- <span class="fa fa-lock text-warning" aria-hidden="true"></span>
- <em><?= __("This topic has been locked") ?></em>
-</div>
diff --git a/src/application/views/view_topic_start.php b/src/application/views/view_topic_start.php
deleted file mode 100644
index 7f62986..0000000
--- a/src/application/views/view_topic_start.php
+++ /dev/null
@@ -1,254 +0,0 @@
-<?php
-use mystic\forum\orm\UserPermissions;
-use mystic\forum\utils\RequestUtils;
-
-$canReply = !$topic->isLocked && ($GLOBALS["currentUser"]?->hasPermission(UserPermissions::CREATE_OWN_POST) ?? false);
-
-$canEdit = ($GLOBALS["currentUser"]?->id === $topicAuthor->id && $topicAuthor->hasPermission(UserPermissions::EDIT_OWN_TOPIC))
- || ($GLOBALS["currentUser"]?->hasPermission(UserPermissions::EDIT_OTHER_TOPIC));
-
-$canDelete = ($GLOBALS["currentUser"]?->id === $topicAuthor->id && $topicAuthor->hasPermission(UserPermissions::DELETE_OWN_TOPIC))
- || ($GLOBALS["currentUser"]?->hasPermission(UserPermissions::DELETE_OTHER_TOPIC));
-?>
-<?php if ($canEdit): ?>
- <div class="modal fade" tabindex="-1" role="dialog" id="diag-edit-post">
- <form class="modal-dialog" role="document" action="?_action=updatepost" method="post">
- <input type="hidden" id="i_edit_post" name="post">
- <div class="modal-content">
- <div class="modal-header">
- <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
- <h4 class="modal-title"><?= __("Edit post") ?></h4>
- </div>
- <div class="modal-body">
- <label class="sr-only" for="i_edit_message"><?= __("Message:") ?></label>
- <div class="panel panel-default">
- <div class="panel-heading" style="padding:4px">
- <div class="btn-toolbar" role="toolbar">
- <div class="btn-group" role="group">
- <button data-area="#i_edit_message" title="<?= __("Bold") ?>" data-editor-command="bold" type="button" class="btn btn-default btn-xs"><i class="fa fa-fw fa-bold"></i></button>
- <button data-area="#i_edit_message" title="<?= __("Italic") ?>" data-editor-command="italic" type="button" class="btn btn-default btn-xs"><i class="fa fa-fw fa-italic"></i></button>
- <button data-area="#i_edit_message" title="<?= __("Underlined") ?>" data-editor-command="underline" type="button" class="btn btn-default btn-xs"><i class="fa fa-fw fa-underline"></i></button>
- <button data-area="#i_edit_message" title="<?= __("Strikethrough") ?>" data-editor-command="strikethrough" type="button" class="btn btn-default btn-xs"><i class="fa fa-fw fa-strikethrough"></i></button>
- </div>
- <div class="btn-group" role="group">
- <button data-area="#i_edit_message" title="<?= __("Superscript") ?>" data-editor-command="sup" type="button" class="btn btn-default btn-xs"><i class="fa fa-fw fa-superscript"></i></button>
- <button data-area="#i_edit_message" title="<?= __("Subscript") ?>" data-editor-command="sub" type="button" class="btn btn-default btn-xs"><i class="fa fa-fw fa-subscript"></i></button>
- </div>
- <div class="btn-group" role="group">
- <button data-area="#i_edit_message" title="<?= __("Quote") ?>" data-editor-command="quote" type="button" class="btn btn-default btn-xs"><i class="fa fa-fw fa-quote-left"></i></button>
- <button data-area="#i_edit_message" title="<?= __("Spoiler") ?>" data-editor-command="spoiler" type="button" class="btn btn-default btn-xs"><i class="fa fa-fw fa-eye"></i></button>
- </div>
- </div>
- </div>
- <div class="panel-body" style="padding:0">
- <textarea class="form-control" id="i_edit_message" name="message" required rows="12" cols="60" style="resize:vertical;max-height:500px;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;margin:-1px;width:calc(100% + 2px)"></textarea>
- </div>
- </div>
- </div>
- <div class="modal-footer">
- <button type="button" class="btn btn-default" data-dismiss="modal"><span class="fa fa-remove" aria-hidden="true"></span> <?= __("Cancel") ?></button>
- <button type="submit" class="btn btn-success"><span class="fa fa-save" aria-hidden="true"></span> <?= __("Save changes") ?></button>
- </div>
- </div>
- </form>
- </div>
-<?php endif; ?>
-<?php if ($GLOBALS["currentUser"] === null): ?>
- <div class="modal fade" tabindex="-1" role="dialog" id="diag-cant-view-attachment">
- <div class="modal-dialog modal-danger" role="document">
- <div class="modal-content panel-danger">
- <div class="modal-header panel-heading">
- <h4 class="modal-title"><span class="fa fa-exclamation-circle" aria-hidden="true"></span> <?= __("Permission denied") ?></h4>
- </div>
- <div class="modal-body">
- <?= __("You must be logged in to view attachments") ?>
- </div>
- <div class="modal-footer">
- <button type="button" class="btn btn-default" data-dismiss="modal"><span class="fa fa-close" aria-hidden="true"></span> <?= __("Close") ?></button>
- <a href="?_action=auth&amp;next=<?= htmlentities(urlencode($_SERVER["REQUEST_URI"])) ?>" class="btn btn-success"><span class="fa fa-user" aria-hidden="true"></span> <?= __("Log in") ?></a>
- </div>
- </div>
- </div>
- </div>
- <script>
- $(function() {
- $(".attachment").click(function(e) {
- e.preventDefault();
- $("#diag-cant-view-attachment").modal();
- });
- });
- </script>
-<?php else: ?>
- <div class="modal fade" tabindex="-1" role="dialog" id="diag-image-attachment">
- <div class="modal-dialog" role="document">
- <div class="modal-content">
- <div class="modal-header">
- <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
- <h4 class="modal-title"><?= __("Attachment") ?></h4>
- </div>
- <div class="modal-body">
- <img class="image-attachment-view attachment-view" id="image-attachment-view" alt="">
- </div>
- <div class="modal-footer">
- <a href="" download id="image-attachment-dl-btn" class="btn btn-default"><span class="fa fa-download" aria-hidden="true"></span> <?= __("Download") ?></a>
- </div>
- </div>
- </div>
- </div>
- <div class="modal fade" tabindex="-1" role="dialog" id="diag-video-attachment">
- <div class="modal-dialog" role="document">
- <div class="modal-content">
- <div class="modal-header">
- <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
- <h4 class="modal-title"><?= __("Attachment") ?></h4>
- </div>
- <div class="modal-body">
- <video class="video-attachment-view attachment-view" id="video-attachment-view" controls></video>
- </div>
- <div class="modal-footer">
- <a href="" download id="video-attachment-dl-btn" class="btn btn-default"><span class="fa fa-download" aria-hidden="true"></span> <?= __("Download") ?></a>
- </div>
- </div>
- </div>
- </div>
- <script>
- $(function() {
- $(".image-attachment:not(.video-attachment)").click(function(e) {
- e.preventDefault();
- var attUrl = "?_action=attachment&attachment=" + encodeURIComponent($(this).attr("data-attachment-id"));
- $("#image-attachment-view").attr("src", attUrl);
- $("#image-attachment-dl-btn").attr("href", attUrl);
- $("#diag-image-attachment").modal();
- });
- $(".image-attachment.video-attachment").click(function(e) {
- e.preventDefault();
- var attUrl = "?_action=attachment&attachment=" + encodeURIComponent($(this).attr("data-attachment-id"));
- $("#video-attachment-view").attr("src", attUrl);
- $("#video-attachment-dl-btn").attr("href", attUrl);
- $("#diag-video-attachment").modal();
- });
- $("#diag-video-attachment").on("hide.bs.modal", function() {
- $("#video-attachment-view")[0].pause();
- });
- });
- </script>
-<?php endif; ?>
-
-<?php
-if (($_formError = RequestUtils::getAndClearFormError("updatetopic")) !== null) {
- _view("alert_error", ["message" => $_formError]);
-}
-
-if (($_formError = RequestUtils::getAndClearFormError("locktopic")) !== null) {
- _view("alert_error", ["message" => $_formError]);
-}
-?>
-
-<div class="page-header margin-top-0 clearfix">
- <div id="displayHeading">
- <div role="heading" class="h1 seamless-inline">
- <?php if ($topic->isLocked): ?>
- <span class="fa fa-lock text-muted" aria-hidden="true"></span>
- <?php endif; ?>
- <?= htmlentities($topic->title) ?>
- <div class="pull-right text-normal">
- <?php if ($canEdit && !$topic->isLocked): ?>
- <button id="btn-edit-title" class="btn btn-default js-only"><span class="fa fa-pencil" aria-hidden="true"></span> <?= __("Edit title") ?></button>
- <?php endif; ?>
- <?php if ($canReply): ?>
- <button id="btn-reply" class="btn btn-default js-only"><span class="fa fa-comment" aria-hidden="true"></span> <?= __("Reply") ?></button>
- <?php endif; ?>
- <?php if ($canEdit): ?>
- <?php if ($topic->isLocked): ?>
- <form action="?_action=locktopic" method="post" class="seamless-inline">
- <input type="hidden" name="topic" value="<?= htmlentities($topic->id) ?>">
- <input type="hidden" name="locked" value="false">
- <button type="submit" class="btn btn-success"><span class="fa fa-unlock" aria-hidden="true"></span> <?= __("Unlock topic") ?></button>
- </form>
- <?php else: ?>
- <form action="?_action=locktopic" method="post" class="seamless-inline">
- <input type="hidden" name="topic" value="<?= htmlentities($topic->id) ?>">
- <input type="hidden" name="locked" value="true">
- <button type="submit" class="btn btn-warning"><span class="fa fa-lock" aria-hidden="true"></span> <?= __("Lock topic") ?></button>
- </form>
- <?php endif; ?>
- <?php endif; ?>
- <?php if ($canDelete): ?>
- <form action="?_action=deletetopic" method="post" class="seamless-inline">
- <input type="hidden" name="topic" value="<?= htmlentities($topic->id) ?>">
- <button type="submit" class="btn btn-danger"><span class="fa fa-trash" aria-hidden="true"></span> <?= __("Delete topic") ?></button>
- </form>
- <?php endif; ?>
- </div>
- </div><br>
- <?= __("Started by %user% on %date%", [
- "user" => ($topicAuthor !== null) ? ('<a href="?_action=viewuser&amp;user=' . htmlentities(urlencode($topicAuthor->id)) . '">' . htmlentities($topicAuthor->displayName) . '</a>') : __("(deleted)"),
- "date" => '<span class="_time">' . htmlentities($topic->creationDate->format("c")) . '</span>',
- ]); ?>
- </div>
- <?php if ($canEdit): ?>
- <form action="?_action=updatetopic" method="post" id="editHeading" style="display: none;" class="form-inline seamless-inline">
- <input type="hidden" name="topic" value="<?= htmlentities(urlencode($topic->id)) ?>">
- <div class="row">
- <div class="col-md-8">
- <input type="text" class="form-control" name="title" id="i_edit_title" data-original-value="<?= htmlentities($topic->title) ?>" value="<?= htmlentities($topic->title) ?>" style="box-sizing: border-box; width: 100%; font-size: 36px; height: 56px">
- </div>
- <div class="col-md-4 text-right">
- <button type="button" id="topicTitleEditCancel" class="btn btn-default"><span class="fa fa-close" aria-hidden="true"></span> <?= __("Cancel") ?></button>
- <button type="submit" class="btn btn-success"><span class="fa fa-save" aria-hidden="true"></span> <?= __("Save changes") ?></button>
- </div>
- </div>
- </form>
- <?php endif; ?>
-</div>
-<script>
-<?php if ($canEdit): ?>
-$(function() {
- $("#btn-edit-title").click(function() {
- $("#displayHeading").hide();
- $("#editHeading").show();
- $("#i_edit_title").val($("#i_edit_title").attr("data-original-value")).focus();
- });
- $("#topicTitleEditCancel").click(function() {
- $("#displayHeading").show();
- $("#editHeading").hide();
- });
-});
-<?php endif; ?>
-<?php if ($canReply): ?>
-$(function() {
- function focusReplyBox() {
- var msgInput = $("#i_message");
- msgInput[0].scrollIntoView();
- msgInput.focus();
- }
- $("#btn-reply").click(function() {
- focusReplyBox();
- });
- $("._reply-post").click(function() {
- var text = $("#post-" + $(this).attr("data-post-id")).attr("data-text");
- var val = $("#i_message").val();
- var lines = text.split("\n");
- for (var i = 0; i < lines.length; ++i)
- val += "\n> " + lines[i];
- val += "\n\n";
- $("#i_message").val(val.replace(/^\n+/, ""));
- focusReplyBox();
- });
-});
-<?php endif; ?>
-<?php if ($canEdit): ?>
-$(function() {
- $("._edit-post").click(function() {
- var $post = $("#post-" + $(this).attr("data-post-id"));
- var $postContent = $post.find(".post-content");
- $("#i_edit_message").css("height", "").val($post.attr("data-text"));
- $("#i_edit_post").val($(this).attr("data-post-id"));
- $("#diag-edit-post").modal();
- });
- $("#diag-edit-post").on("shown.bs.modal", function() {
- $("#i_edit_message").focus();
- });
-});
-<?php endif; ?>
-</script>
diff --git a/src/application/views/view_topiclog.php b/src/application/views/view_topiclog.php
deleted file mode 100644
index 291dada..0000000
--- a/src/application/views/view_topiclog.php
+++ /dev/null
@@ -1,67 +0,0 @@
-<?php
-
-/** @var \mystic\forum\orm\TopicLogMessage $logMessage */
-/** @var ?\mystic\forum\orm\User $postAuthor */
-
-use mystic\forum\orm\TopicLogMessage;
-
-$hide_actions ??= false;
-$hide_pfp ??= false;
-
-$user = "";
-if ($postAuthor === null) {
- $user = __("(deleted)");
-} else {
- $user = '<a href="?_action=viewuser&user=' . htmlentities(urlencode($postAuthor->id)) . '">' . htmlentities($postAuthor->displayName) . '</a>';
-}
-
-?>
-<div class="media" id="post-<?= htmlentities($logMessage->id) ?>">
-<div class="media-left hidden-sm hidden-xs">
- <?php if ($postAuthor): ?>
- <?php if ($hide_actions): ?>
- <img class="media-object" alt="<?= __("Profile picture") ?>" src="?_action=profilepicture&amp;user=<?= htmlentities(urlencode($postAuthor->id)) ?>" width="64" height="64">
- <?php else: ?>
- <a href="?_action=viewuser&amp;user=<?= htmlentities(urlencode($postAuthor->id)) ?>">
- <img class="media-object" alt="<?= __("Profile picture") ?>" src="?_action=profilepicture&amp;user=<?= htmlentities(urlencode($postAuthor->id)) ?>" width="64" height="64">
- </a>
- <?php endif; ?>
- <?php else: ?>
- <div class="media-object" style="width:64px;height:64px"></div>
- <?php endif; ?>
-</div>
-<div class="media-body">
- <?php if ($logMessage->type === TopicLogMessage::LOCKED): ?>
- <div class="well icon-well text-info">
- <span class="fa fa-lock text-info" aria-hidden="true"></span>
- <em><?= __("%user% locked this topic", [
- "user" => $user,
- ]) ?></em>
- <br>
- <small class="_time"><?= $logMessage->postDate->format("c") ?></small>
- </div>
- <?php elseif ($logMessage->type === TopicLogMessage::UNLOCKED): ?>
- <div class="well icon-well text-success">
- <span class="fa fa-unlock text-success" aria-hidden="true"></span>
- <em><?= __("%user% unlocked this topic", [
- "user" => $user,
- ]) ?></em>
- <br>
- <small class="_time"><?= $logMessage->postDate->format("c") ?></small>
- </div>
- <?php elseif ($logMessage->type === TopicLogMessage::TITLE_CHANGED): ?>
- <div class="well icon-well text-info">
- <span class="fa fa-pencil text-info" aria-hidden="true"></span>
- <em><?= __("%user% changed the title of this topic from %old_title% to %new_title%", [
- "user" => $user,
- "old_title" => '<strong>' . htmlentities($logMessage->params["old_value"] ?? __("unknown")) . '</strong>',
- "new_title" => '<strong>' . htmlentities($logMessage->params["new_value"] ?? __("unknown")) . '</strong>',
- ]) ?></em>
- <br>
- <small class="_time"><?= $logMessage->postDate->format("c") ?></small>
- </div>
- <?php else: ?>
- <?= __("unknown") ?>
- <?php endif; ?>
-</div>
-</div>
diff --git a/src/application/views/view_topics.php b/src/application/views/view_topics.php
deleted file mode 100644
index 8a94775..0000000
--- a/src/application/views/view_topics.php
+++ /dev/null
@@ -1,24 +0,0 @@
-<?php
-
-use mystic\forum\orm\User;
-use mystic\forum\orm\UserPermissions;
-
-if ($GLOBALS["currentUser"]?->hasPermission(UserPermissions::CREATE_OWN_TOPIC)):
-?>
-<p class="text-right">
-<a href="?_action=newtopic" class="btn btn-success"><span class="fa fa-plus" aria-hidden="true"></span> <?= __("New topic") ?></a>
-</p>
-<?php endif; ?>
-<div class="list-group">
-<?php /** @var Topic $topic */ foreach ($topics as $topic): ?>
-<a class="list-group-item" href="?_action=viewtopic&amp;topic=<?= htmlentities(urlencode($topic->id)) ?>">
- <h4 class="list-group-item-heading">
- <?php if ($topic->isLocked): ?>
- <span class="fa fa-lock text-muted" aria-hidden="true"></span>
- <?php endif; ?>
- <?= htmlentities($topic->title) ?>
- </h4>
- <p class="list-group-item-text _time"><?= htmlentities($topic->creationDate->format("c")) ?></p>
-</a>
-<?php endforeach; ?>
-</div>
diff --git a/src/application/views/view_user.php b/src/application/views/view_user.php
deleted file mode 100644
index e7c9753..0000000
--- a/src/application/views/view_user.php
+++ /dev/null
@@ -1,209 +0,0 @@
-<?php
-
-use mystic\forum\orm\UserPermissions;
-use mystic\forum\utils\RequestUtils;
-use mystic\forum\utils\StringUtils;
-
-/** @var mystic\forum\orm\User $user */
-/** @var bool $lastNameChangeTooRecent */
-
-$canEdit = ($user->id === $GLOBALS["currentUser"]?->id && $user->hasPermission(UserPermissions::EDIT_OWN_USER))
- || $GLOBALS["currentUser"]?->hasPermission(UserPermissions::EDIT_OTHER_USER);
-
-$isOwnProfile = $user->id === $GLOBALS["currentUser"]?->id;
-
-$sUserPossessive = "";
-if ($isOwnProfile)
- $sUserPossessive = "Your posts";
-else
- $sUserPossessive = "%display_name%'s posts";
-
-$dateJoined = DateTime::createFromImmutable($user->created);
-$dateJoined->setTime(0, 0, 0, 0);
-
-$emailPending = $isOwnProfile && $user->pendingEmail !== null;
-?>
-
-<div class="clearfix page-header margin-top-0">
- <img class="pull-left margin-right" src="?_action=profilepicture&amp;user=<?= htmlentities(urlencode($user->id)) ?>" alt="<?= __("Profile picture") ?>" width="64" height="64">
- <span class="h1"><?= htmlentities($user->displayName) ?></span>
- <?php if ($isOwnProfile): ?>
- <span class="label label-primary"><?= __("You") ?></span>
- <?php endif; ?><br>
- @<?= htmlentities($user->name) ?> &bull; <span class="text-muted"><?= __("Member since %join_date%", [
- "join_date" => '<span class="_date">' . htmlentities($dateJoined->format("c")) . '</span>',
- ]) ?></span>
-</div>
-
-<?php if ($canEdit): ?>
-<div class="row">
-<div class="col-md-9">
-<?php endif; ?>
-
-<h3><?= __($sUserPossessive, [
- "display_name" => $user->displayName,
-]) ?></h3>
-
-<?php if (count($posts) > 0): ?>
- <div class="post-container">
- <div class="post-container-posts">
- <div class="list-group margin-top">
- <?php foreach ($posts as $post):
- if ($post->deleted) continue;
- $hasAttachments = count($attachments[$post->id]) > 0;
- ?>
- <a href="?_action=viewtopic&amp;topic=<?= htmlentities(urlencode($post->topicId)) ?>#post-<?= htmlentities(urlencode($post->id)) ?>" class="list-group-item">
- <?php if ($hasAttachments): ?>
- <span class="badge"><span class="fa fa-paperclip"></span></span>
- <?php endif; ?>
- <?= renderPostSummary($post->content) ?><br>
- <span class="text-muted"><?= __("posted on %post_date% in %topic%", [
- "post_date" => '<span class="_time">' . htmlentities($post->postDate->format("c")) . '</span>',
- "topic" => '<em>' .
- ($topics[$post->topicId]?->isLocked ? '<span class="fa fa-lock text-muted" aria-hidden="true"></span> ' : '') .
- htmlentities($topics[$post->topicId]?->title ?? "unknown") . '</em>',
- ]) ?></span>
- </a>
- <?php endforeach; ?>
- </div>
- </div>
- <div class="post-container-controls">
- <button class="btn btn-default"><?= __("Show all posts") ?></button>
- </div>
- </div>
-<?php else: ?>
- <div class="well icon-well text-info margin-top margin-bottom">
- <span class="fa fa-info-circle text-info" aria-hidden="true"></span>
- <em><?= __("This user has not posted anything yet") ?></em>
- </div>
-<?php endif; ?>
-
-<?php if ($canEdit): ?>
-</div>
-
-<div class="col-md-3">
-<h3><?= __("Edit profile") ?></h3>
-<?php
-if (($_formError = RequestUtils::getAndClearFormError("update_profile")) !== null) {
- _view("alert_error", ["message" => $_formError]);
-}
-?>
-<form action="<?= htmlentities($_SERVER["REQUEST_URI"]) ?>" method="post" enctype="multipart/form-data">
- <input type="hidden" name="form_id" value="update_profile">
- <div class="form-group">
- <label for="i_display_name"><?= __("Display name:") ?></label>
- <input required class="form-control" type="text" name="display_name" id="i_display_name" value="<?= htmlentities($user->displayName) ?>">
- </div>
- <div class="form-group">
- <label for="i_name"><?= __("Username:") ?></label>
- <?php if ($lastNameChangeTooRecent): ?>
- <input class="form-control" type="text" id="i_name" value="<?= htmlentities($user->name) ?>" disabled>
- <small class="text-danger"><strong><?= __("You can only change your username every 30 days!") ?></strong></small>
- <?php else: ?>
- <input required class="form-control" type="text" name="name" id="i_name" value="<?= htmlentities($user->name) ?>">
- <?php endif; ?>
- </div>
- <div class="form-group">
- <label for="i_email"><?= __("Email address:") ?></label>
- <?php if ($emailPending): ?>
- <input class="form-control" type="email" id="i_email" value="<?= htmlentities($user->email) ?>" disabled>
- <?php else: ?>
- <input required class="form-control" type="email" id="i_email" name="email" value="<?= htmlentities($user->email) ?>">
- <?php endif; ?>
- </div>
- <div class="form-group">
- <label><?= __("Profile picture:") ?></label>
-<?php
-$_checkbox_disabled = empty($user->profilePicture);
-$_checkbox_disabled_class = $_checkbox_disabled ? " disabled text-muted" : "";
-?>
- <div class="radio margin-top-0 <?= $_checkbox_disabled_class ?>">
- <label>
- <input type="radio" name="pfp_action" id="pfp_action_1" value="keep"<?= !empty($user->profilePicture) ? ' checked' : ' disabled' ?>>
- <?= __("Keep current profile picture") ?>
- </label>
- </div>
- <div class="radio">
- <label>
- <input type="radio" name="pfp_action" id="pfp_action_2" value="remove"<?= empty($user->profilePicture) ? ' checked' : '' ?>>
- <?php if (empty($user->profilePicture)): ?>
- <?= __("No profile picture") ?>
- <?php else: ?>
- <?= __("Remove profile picture") ?>
- <?php endif; ?>
- </label>
- </div>
- <div class="radio">
- <label>
- <input type="radio" name="pfp_action" value="replace" id="pfp_action_3">
- <?= __("Upload new profile picture") ?>
- </label>
- </div>
- <input type="file" name="pfp" id="i_pfp" accept="image/png,image/jpeg" class="margin-left-3x small">
- </div>
- <div class="form-group">
- <button type="submit" class="btn btn-success"><?= __("Save changes") ?></button>
- </div>
-</form>
-<?php if ($isOwnProfile): ?>
-<h3><?= __("Change password") ?></h3>
-<?php
-if (($_formError = RequestUtils::getAndClearFormError("update_password")) !== null) {
- _view("alert_error", ["message" => $_formError]);
-}
-?>
-<form action="<?= htmlentities($_SERVER["REQUEST_URI"]) ?>" method="post">
- <input type="hidden" name="form_id" value="update_password">
- <div class="form-group">
- <label for="i_current_password"><?= __("Current password:") ?></label>
- <input autocomplete="current-password" required class="form-control" type="password" name="current_password" id="i_current_password" required>
- </div>
- <div class="form-group">
- <label for="i_new_password"><?= __("New password:") ?></label>
- <input autocomplete="new-password" required class="form-control" type="password" name="new_password" id="i_new_password" required>
- </div>
- <div class="form-group">
- <label for="i_retype_password"><?= __("Retype password:") ?></label>
- <input autocomplete="new-password" required class="form-control" type="password" name="retype_password" id="i_retype_password" required>
- </div>
- <div class="form-group">
- <button type="submit" class="btn btn-success"><?= __("Change password") ?></button>
- </div>
-</form>
-<?php endif; ?>
-
-</div>
-
-</div>
-<?php endif; ?>
-
-<script>
-$(function() {
- $(".post-container").each(function(i, e) {
- if ($(e).height() > 900) { // more than 800 so it doesn't collapse just a few pixels
- $(e).addClass("collapsed");
- }
- $(e).find(".post-container-controls button").click(function() {
- $(e).removeClass("collapsed");
- });
- });
-});
-<?php if ($canEdit): ?>
-$(function() {
- function _hide() {
- $("#i_pfp").hide().prop("disabled", true).prop("required", false);
- $("#i_pfp + .file-input-group").hide().find("button").prop("disabled", true);
- }
- _hide();
- setTimeout(_hide, 10);
- $("[name='pfp_action']").on("change input check click", function() {
- if ($("#pfp_action_3").is(":checked")) {
- $("#i_pfp").show().prop("disabled", false).prop("required", true);
- $("#i_pfp + .file-input-group").show().find("button").prop("disabled", false);
- } else {
- _hide();
- }
- })
-});
-<?php endif; ?>
-</script>
diff --git a/src/composer.json b/src/composer.json
index 630c8ac..d532fc8 100644
--- a/src/composer.json
+++ b/src/composer.json
@@ -8,6 +8,7 @@
"symfony/mailer": "^7.1",
"gregwar/captcha": "^1.2",
"andig/php-shunting-yard": "dev-master",
- "php-ffmpeg/php-ffmpeg": "^1.2"
+ "php-ffmpeg/php-ffmpeg": "^1.2",
+ "twig/twig": "^3.14"
}
}
diff --git a/src/composer.lock b/src/composer.lock
index c1405cf..ca42d3b 100644
--- a/src/composer.lock
+++ b/src/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "51563e81645c915860c5db7676154cdc",
+ "content-hash": "050fa6cca991c3f783a7a4ff541d1690",
"packages": [
{
"name": "andig/php-shunting-yard",
@@ -1275,6 +1275,85 @@
"time": "2024-08-13T14:28:19+00:00"
},
{
+ "name": "symfony/polyfill-ctype",
+ "version": "v1.31.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-ctype.git",
+ "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638",
+ "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "provide": {
+ "ext-ctype": "*"
+ },
+ "suggest": {
+ "ext-ctype": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "name": "symfony/polyfill",
+ "url": "https://github.com/symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Ctype\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Gert de Pagter",
+ "email": "BackEndTea@gmail.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for ctype functions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "ctype",
+ "polyfill",
+ "portable"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-09-09T11:45:10+00:00"
+ },
+ {
"name": "symfony/polyfill-intl-idn",
"version": "v1.31.0",
"source": {
@@ -1519,6 +1598,82 @@
"time": "2024-09-09T11:45:10+00:00"
},
{
+ "name": "symfony/polyfill-php81",
+ "version": "v1.31.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-php81.git",
+ "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c",
+ "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "name": "symfony/polyfill",
+ "url": "https://github.com/symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Php81\\": ""
+ },
+ "classmap": [
+ "Resources/stubs"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-php81/tree/v1.31.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-09-09T11:45:10+00:00"
+ },
+ {
"name": "symfony/process",
"version": "v7.1.3",
"source": {
@@ -1737,6 +1892,85 @@
}
],
"time": "2024-06-28T08:00:31+00:00"
+ },
+ {
+ "name": "twig/twig",
+ "version": "v3.14.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/twigphp/Twig.git",
+ "reference": "126b2c97818dbff0cdf3fbfc881aedb3d40aae72"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/twigphp/Twig/zipball/126b2c97818dbff0cdf3fbfc881aedb3d40aae72",
+ "reference": "126b2c97818dbff0cdf3fbfc881aedb3d40aae72",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.0.2",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/polyfill-ctype": "^1.8",
+ "symfony/polyfill-mbstring": "^1.3",
+ "symfony/polyfill-php81": "^1.29"
+ },
+ "require-dev": {
+ "psr/container": "^1.0|^2.0",
+ "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/Resources/core.php",
+ "src/Resources/debug.php",
+ "src/Resources/escaper.php",
+ "src/Resources/string_loader.php"
+ ],
+ "psr-4": {
+ "Twig\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com",
+ "homepage": "http://fabien.potencier.org",
+ "role": "Lead Developer"
+ },
+ {
+ "name": "Twig Team",
+ "role": "Contributors"
+ },
+ {
+ "name": "Armin Ronacher",
+ "email": "armin.ronacher@active-4.com",
+ "role": "Project Founder"
+ }
+ ],
+ "description": "Twig, the flexible, fast, and secure template language for PHP",
+ "homepage": "https://twig.symfony.com",
+ "keywords": [
+ "templating"
+ ],
+ "support": {
+ "issues": "https://github.com/twigphp/Twig/issues",
+ "source": "https://github.com/twigphp/Twig/tree/v3.14.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/twig/twig",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-09-09T17:55:12+00:00"
}
],
"packages-dev": [],
diff --git a/src/index.php b/src/index.php
index 0c5cc3b..2403a9b 100644
--- a/src/index.php
+++ b/src/index.php
@@ -8,8 +8,13 @@ use mystic\forum\orm\Post;
use mystic\forum\orm\Topic;
use mystic\forum\orm\TopicLogMessage;
use mystic\forum\orm\User;
+use mystic\forum\orm\UserPermissions;
use mystic\forum\utils\RequestUtils;
use mystic\forum\utils\StringUtils;
+use Twig\Environment;
+use Twig\Loader\FilesystemLoader;
+use Twig\TwigFilter;
+use Twig\TwigFunction;
header_remove("X-Powered-By");
@@ -39,23 +44,17 @@ $_action = $_GET["_action"] ?? null;
$GLOBALS["action"] = $_action;
function msg_error(string $err, bool $skipLoginCheck = false): void {
- _view("template_start", ["_title" => __("Error")]);
- _view("template_navigation_start");
- if (!$skipLoginCheck)
- _view("template_navigation", ["user" => RequestUtils::getAuthorizedUser($GLOBALS["db"])]);
- _view("template_navigation_end");
- _view("alert_error", ["message" => $err]);
- _view("template_end", [...getThemeAndLangInfo()]);
+ render("error_page.twig", [
+ "message" => $err,
+ "skipLoginCheck" => $skipLoginCheck,
+ ]);
}
function msg_info(string $msg, bool $skipLoginCheck = false): void {
- _view("template_start", ["_title" => __("Information")]);
- _view("template_navigation_start");
- if (!$skipLoginCheck)
- _view("template_navigation", ["user" => RequestUtils::getAuthorizedUser($GLOBALS["db"])]);
- _view("template_navigation_end");
- _view("alert_info", ["message" => $msg]);
- _view("template_end", [...getThemeAndLangInfo()]);
+ render("info_page.twig", [
+ "message" => $msg,
+ "skipLoginCheck" => $skipLoginCheck,
+ ]);
}
function generateCaptchaText(): string {
@@ -129,20 +128,6 @@ function getThemeAndLangInfo(): array {
];
}
-function _view(string $___NAME, array $___PARAMS = []): void {
- $___PATH = __DIR__ . "/application/views/" . $___NAME . ".php";
- if (!is_file($___PATH)) {
- echo "<!--!ERROR Failed to include {" . htmlentities($___NAME) . "}-->\n";
- echo "<br><p><strong style=\"color:red !important\">Failed to include <em>" . htmlentities($___NAME) . "</em></strong></p><br>\n";
- echo "<!--/ERROR-->\n";
- } else {
- extract($___PARAMS);
- echo "<!--{" . htmlentities($___NAME) . "}-->\n";
- include $___PATH;
- echo "<!--{/" . htmlentities($___NAME) . "}-->\n";
- }
-}
-
function isTrue(string $str): bool {
$str = strtolower($str);
return in_array($str, ["yes","true","y","t","on","enabled","1","?1"]);
@@ -164,6 +149,69 @@ function reArrayFiles(&$file_post) {
return $file_ary;
}
+function render(string $page, array $context = []): void {
+ $defaultTemplate = "bootstrap-3"; // bootstrap-3 is the default for backwards compatibility
+ $templateDir = __ROOT__ . "/application/templates";
+
+ [
+ "availableThemes" => $availableThemes,
+ "currentTheme" => $currentTheme,
+ ] = getThemeAndLangInfo();
+
+ $currentThemeInfo = $availableThemes[$currentTheme] ?? (object)[];
+ $currentTemplate = $currentThemeInfo->template ?? $defaultTemplate;
+
+ $availableTemplates = array_values(array_filter(scandir($templateDir), function($theme) use ($templateDir) {
+ return $theme[0] !== "." && is_dir($templateDir . "/" . $theme);
+ }));
+
+ if (!in_array($currentTemplate, $availableTemplates))
+ $currentTemplate = $defaultTemplate;
+
+ $loader = new FilesystemLoader([
+ $templateDir . "/" . $currentTemplate,
+ ]);
+ $twig = new Environment($loader, [
+ // TODO Enable caching
+ ]);
+
+ $twig->addFunction(new TwigFunction("__", __(...), [ "is_safe" => ["html"] ]));
+ $twig->addFunction(new TwigFunction("___", ___(...), [ "is_safe" => ["html"] ]));
+ $twig->addFunction(new TwigFunction("renderPost", renderPost(...), [ "is_safe" => ["html"] ]));
+ $twig->addFunction(new TwigFunction("renderPostSummary", renderPostSummary(...), [ "is_safe" => ["html"] ]));
+ $twig->addFunction(new TwigFunction("permission", fn(string $name): int => constant(UserPermissions::class . "::" . $name)));
+ $twig->addFunction(new TwigFunction("getAndClearFormError", RequestUtils::getAndClearFormError(...)));
+ $twig->addFunction(new TwigFunction("lastFormField", function(string $formId, string $field): ?string {
+ $lastFormId = "";
+ $lastForm = RequestUtils::getLastForm($lastFormId) ?? null;
+ if ($lastForm === null || $lastFormId !== $formId)
+ return null;
+ return $lastForm[$field] ?? null;
+ }));
+
+ $twig->addFilter(new TwigFilter("hash", fn(string $data, string $algo, bool $binary = false, array $options = []) => hash($algo, $data, $binary, $options)));
+ $twig->addFilter(new TwigFilter("base64_encode", base64_encode(...)));
+ $twig->addFilter(new TwigFilter("base64_decode", base64_decode(...)));
+
+ echo $twig->render($page, [
+ "g" => [
+ "get" => &$_GET,
+ "post" => &$_POST,
+ "request" => &$_REQUEST,
+ "session" => &$_SESSION,
+ "cookie" => &$_COOKIE,
+ "env" => &$_ENV,
+ "server" => &$_SERVER,
+ "globals" => $GLOBALS,
+ ],
+ "ctx" => &$context,
+
+ "currentUser" => &$GLOBALS["currentUser"],
+ ...getThemeAndLangInfo(),
+ ]);
+ RequestUtils::clearLastForm();
+}
+
const TAGS_REGEX = '/\[(b|i|u|s|\\^|_|spoiler)\](.+?)\[\/\1\]/s';
function expandTags(string $contents): string {