summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJonas Kohl2024-09-17 14:51:23 +0200
committerJonas Kohl2024-09-17 14:51:23 +0200
commita65d424263adfbff9629c7d91a613e4504c84613 (patch)
tree7d89c6d85168427782ae8c24625db14df90e376a
parentf6dd78734f86f8daed7c5d472e7a199301095ff8 (diff)
Add search
-rw-r--r--src/application/i18n.php2
-rw-r--r--src/application/messages/de.msg20
-rw-r--r--src/application/mystic/forum/Database.php27
-rw-r--r--src/application/views/form_search.php28
-rw-r--r--src/application/views/nav_guest.php1
-rw-r--r--src/application/views/nav_logged_in.php1
-rw-r--r--src/application/views/view_search_results.php33
-rw-r--r--src/index.php59
8 files changed, 170 insertions, 1 deletions
diff --git a/src/application/i18n.php b/src/application/i18n.php
index a1f2f24..a7bdd82 100644
--- a/src/application/i18n.php
+++ b/src/application/i18n.php
@@ -173,7 +173,7 @@ function i18n_get(string $msgid, array $params = [], ?string $context = null): s
if ($context !== null)
$key = $context . "\x7F" . $msgid;
- $msg = ($__i18n_msg_store[$__i18n_current_locale] ?? [])[$msgid] ?? $key;
+ $msg = ($__i18n_msg_store[$__i18n_current_locale] ?? [])[$key] ?? $msgid;
uksort($params, fn(string $a, string $b): int => strlen($b) <=> strlen($a));
return str_replace(
diff --git a/src/application/messages/de.msg b/src/application/messages/de.msg
index ed8178e..784c257 100644
--- a/src/application/messages/de.msg
+++ b/src/application/messages/de.msg
@@ -5,6 +5,14 @@ metadata({
}
})
+: "."
+? "Number formatting"
+= ","
+
+: ","
+? "Number formatting"
+= "."
+
: "Log in"
= "Anmelden"
@@ -318,3 +326,15 @@ metadata({
: "Language:"
= "Sprache:"
+
+: "Search"
+= "Suche"
+
+: "Search results for “%query%”"
+= "Suchergebnisse für „%query%“"
+
+: "No results for this search"
+= "Keine Ergebnisse für diese Suche"
+
+: "%result_count% result(s) in %search_duration% second(s)"
+= "%result_count% Treffer in %search_duration% Sekunde(n)"
diff --git a/src/application/mystic/forum/Database.php b/src/application/mystic/forum/Database.php
index a1e67ea..8ebc36b 100644
--- a/src/application/mystic/forum/Database.php
+++ b/src/application/mystic/forum/Database.php
@@ -414,6 +414,33 @@ class Database {
return $items;
}
+ public function execCustomQuery(string $query, ?array $customQueryParams = null, ?string $entityClassName = null): array {
+ if ($customQueryParams === null)
+ $result = \pg_query($this->connection, $query);
+ else
+ $result = \pg_query_params($this->connection, $query, $customQueryParams);
+ if ($result === false)
+ throw new \RuntimeException("Query failed: " . \pg_last_error($this->connection));
+ $cols = null;
+ if ($entityClassName !== null) {
+ $reflClass = new ReflectionClass($entityClassName);
+ $cols = self::getColumns($reflClass);
+ }
+ $rowsOrItems = [];
+ while (($row = \pg_fetch_assoc($result)) !== false) {
+ if ($entityClassName !== null) {
+ $entity = new $entityClassName();
+ foreach ($cols as $colName => $colProps)
+ self::assignValue($entity, $colProps, $row[$colName]);
+ $rowsOrItems []= $entity;
+ } else {
+ $rowsOrItems []= $row;
+ }
+ }
+ \pg_free_result($result);
+ return $rowsOrItems;
+ }
+
public function delete(Entity &$entity): bool {
$entityClassName = get_class($entity);
$tableName = self::getTableName($entityClassName);
diff --git a/src/application/views/form_search.php b/src/application/views/form_search.php
new file mode 100644
index 0000000..edc68b8
--- /dev/null
+++ b/src/application/views/form_search.php
@@ -0,0 +1,28 @@
+<?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()) !== null) {
+ _view("alert_error", ["message" => $_formError]);
+}
+?>
+<form action="<?= htmlentities($_SERVER["REQUEST_URI"]) ?>" method="post">
+ <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>
+ <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
index 1c65a50..88c551b 100644
--- a/src/application/views/nav_guest.php
+++ b/src/application/views/nav_guest.php
@@ -1,4 +1,5 @@
<ul class="nav navbar-nav navbar-right">
+<li<?= $GLOBALS["action"] === "search" ? ' class="active"' : '' ?>><a href="?_action=search"><span class="glyphicon glyphicon-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($_SERVER["REQUEST_URI"])) ?>"><?= __("Log in") ?></a></li>
<?php if (REGISTRATION_ENABLED): ?>
<li<?= $GLOBALS["action"] === "register" ? ' class="active"' : '' ?>><a href="?_action=register&amp;next=<?= htmlentities(urlencode($_SERVER["REQUEST_URI"])) ?>"><?= __("Register") ?></a></li>
diff --git a/src/application/views/nav_logged_in.php b/src/application/views/nav_logged_in.php
index 39c65cb..0f77f90 100644
--- a/src/application/views/nav_logged_in.php
+++ b/src/application/views/nav_logged_in.php
@@ -6,6 +6,7 @@ use mystic\forum\orm\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="glyphicon glyphicon-search" aria-hidden="true"></span><span class="sr-only"><?= __("Search") ?></span></a></li>
<li><a href="?_action=viewuser&amp;user=<?= htmlentities(urlencode($user->id)) ?>"><span class="glyphicon glyphicon-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="glyphicon glyphicon-log-out" aria-hidden="true"></span><span class="sr-only">Log out</span></a></li>
</ul>
diff --git a/src/application/views/view_search_results.php b/src/application/views/view_search_results.php
new file mode 100644
index 0000000..19a6978
--- /dev/null
+++ b/src/application/views/view_search_results.php
@@ -0,0 +1,33 @@
+<?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="glyphicon glyphicon-paperclip"></span></span>
+ <?php endif; ?>
+ <?= htmlentities(StringUtils::truncate(strip_tags(renderPost($post->content)), 100)) ?><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>' . 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="glyphicon glyphicon-info-sign color-info" aria-hidden="true"></span>
+ <em><?= __("No results for this search") ?></em>
+ </div>
+<?php endif; ?>
diff --git a/src/index.php b/src/index.php
index 1b13e97..f3a59b2 100644
--- a/src/index.php
+++ b/src/index.php
@@ -1139,6 +1139,65 @@ if ($_action === "auth") {
}
header("Location: ./?_action=viewtopic&topic=" . urlencode($topicId));
+} elseif ($_action === "search") {
+ if (RequestUtils::isRequestMethod("POST")) {
+ $query = RequestUtils::getRequiredField("query");
+ /** @var Post[] $posts */
+
+ $start_time = microtime(true);
+ $posts = $db->execCustomQuery(<<<SQL
+ SELECT posts.* FROM topics, posts
+ WHERE
+ NOT posts.deleted
+ AND to_tsvector('english', topics.title || ' ' || posts.content) @@ websearch_to_tsquery('english', $1)
+ ORDER BY posts.post_date DESC
+ ;
+ SQL, [ $query ], Post::class);
+
+ $topicLookup = [];
+ $attachmentLookup = [];
+ $userLookup = [];
+ foreach ($posts as $post) {
+ if (!isset($topicLookup[$post->topicId])) {
+ $topic = new Topic;
+ $topic->id = $post->topicId;
+ if ($db->fetch($topic))
+ $topicLookup[$topic->id] = &$topic;
+ }
+ if (!isset($attachmentLookup[$post->id])) {
+ $attachmentLookup[$post->id] = $db->fetchCustom(Attachment::class, 'WHERE post_id = $1', [ $post->id ]);
+ }
+ if (!isset($userLookup[$post->authorId])) {
+ $user = new User;
+ $user->id = $post->authorId;
+ if ($db->fetch($user))
+ $userLookup[$post->authorId] = &$user;
+ }
+ }
+ $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", [
+ "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()]);
+ }
} elseif ($_action === "captcha") {
$phrase = generateCaptchaText();
$builder = new CaptchaBuilder($phrase);