diff options
| -rw-r--r-- | .env.example | 4 | ||||
| -rw-r--r-- | Dockerfile | 11 | ||||
| -rw-r--r-- | src/application/i18n.php | 98 | ||||
| -rw-r--r-- | src/application/messages/de.msg | 117 | ||||
| -rw-r--r-- | src/application/messages/strings.mst | 111 | ||||
| -rw-r--r-- | src/application/mystic/forum/Database.php | 23 | ||||
| -rw-r--r-- | src/application/views/form_login.php | 2 | ||||
| -rw-r--r-- | src/application/views/form_register.php | 5 | ||||
| -rw-r--r-- | src/application/views/nav_guest.php | 4 | ||||
| -rw-r--r-- | src/index.php | 159 | ||||
| -rw-r--r-- | src/ui/site.css | 3 | 
11 files changed, 477 insertions, 60 deletions
| diff --git a/.env.example b/.env.example index 0eba495..7682611 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,10 @@ +SECRET=  POSTGRES_USER=postgres  POSTGRES_DB=postgres  POSTGRES_PASSWORD=postgres +MAILER_DSN= +MAILER_FROM= +PUBLIC_URL=  #MYSTIC_FORUM_THEME=default  #REGISTRATION_ENABLED=1  #MYSTIC_FORUM_TITLE= @@ -10,13 +10,18 @@ RUN apt update && apt install -y \      libjpeg62-turbo-dev \      libxmp-dev \      libfreetype6-dev \ -    libpq-dev +    libpq-dev \ +    libicu-dev \ +    ;  RUN docker-php-ext-configure gd \      --with-webp \      --with-jpeg \      --with-freetype -RUN docker-php-ext-install gd zip pgsql -#RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer +RUN docker-php-ext-install gd zip pgsql intl +RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer  FROM base  COPY ./000-default.conf /etc/apache2/sites-available/000-default.conf +#COPY ./src /var/www/html +#WORKDIR /var/www/html +#RUN composer install diff --git a/src/application/i18n.php b/src/application/i18n.php new file mode 100644 index 0000000..964439f --- /dev/null +++ b/src/application/i18n.php @@ -0,0 +1,98 @@ +<?php + +const MESSAGE_DIR = __DIR__ . "/messages"; + +$__i18n_msg_store = []; +$__i18n_current_locale = null; + +function i18n_parse(string $contents, ?string $filename = null): array { +    $syntax_error = fn(string $msg, ?int $line = null): never => +        throw new Exception("i18n syntax error: $msg (in " . ($filename ?? "unknown") . ":" . ($line ?? 0) . ")"); + +    $msgs = []; +    $lines = explode("\n", $contents); +    $currentId = ""; +    $currentContext = ""; +    $currentMessage = ""; +    $isInMessage = false; +    foreach ($lines as $i => $ln) { +        $lnNum = $i + 1; +        if (trim($ln) === "") +            continue; + +        switch ($ln[0]) { +            case "#": +                continue 2; +            case ":": +                if ($currentId !== "") { +                    if ($currentContext !== "") +                        $currentId = $currentContext . "\004" . $currentId; +                    $msgs[$currentId] ??= $currentMessage; +                } +                $currentId = json_decode(substr($ln, 2)); +                $currentContext = ""; +                $currentMessage = ""; +                $isInMessage = false; +                break; +            case "=": +                $isInMessage = true; +                $currentMessage = json_decode(substr($ln, 2)); +                break; +            case "?": +                if ($isInMessage) +                    $syntax_error("context must be defined before start of message", $lnNum); +                $currentContext = json_decode(substr($ln, 2)); +                break; +            case " ": +                if ($isInMessage) +                    $currentMessage .= json_decode(substr($ln, 2)); +                else +                    $currentId .= json_decode(substr($ln, 2)); +                break; +            default: +                $syntax_error("invalid start of line '" . $ln[0] . "' (0x" . str_pad(strtoupper(dechex(ord($ln[0]))), 2, "0", STR_PAD_LEFT) . ")", $lnNum); +                break; +        } +    } + +    if ($currentId !== "") { +        if ($currentContext !== "") +            $currentId = $currentContext . "\x7F" . $currentId; +        $msgs[$currentId] ??= $currentMessage; +    } +    return $msgs; +} + +function i18n_locale(string $locale) { +    global $__i18n_current_locale; +    $__i18n_current_locale = $locale; +} + +function i18n_get(string $msgid, array $params = [], ?string $context = null): string { +    global $__i18n_current_locale, $__i18n_msg_store; + +    $key = $msgid; +    if ($context !== null) +        $key = $context . "\x7F" . $msgid; + +    $msg = ($__i18n_msg_store[$__i18n_current_locale] ?? [])[$msgid] ?? $key; + +    uksort($params, fn(string $a, string $b): int => strlen($b) <=> strlen($a)); +    return str_replace( +        array_map(fn(string $k): string => "%$k%", array_keys($params)), +        array_values($params), +        $msg +    ); +} + +function __(string $msgid, array $params = [], ?string $context = null): string { +    return i18n_get($msgid, $params, $context); +} + +foreach (scandir(MESSAGE_DIR) as $ent) { +    $path = MESSAGE_DIR . "/" . $ent; +    if ($ent[0] === "." || !is_file($path) || strcasecmp(pathinfo($ent, PATHINFO_EXTENSION), "msg") !== 0) +        continue; +    $lang = pathinfo($ent, PATHINFO_FILENAME); +    $__i18n_msg_store[$lang] = i18n_parse(file_get_contents($path), $ent); +} diff --git a/src/application/messages/de.msg b/src/application/messages/de.msg new file mode 100644 index 0000000..5ee2f6c --- /dev/null +++ b/src/application/messages/de.msg @@ -0,0 +1,117 @@ +: "Log in" += "Anmelden" + +: "Register" += "Registrieren" + +: "Failed to connect to database!" += "Fehler bei Datenbankverbindung" + +: "Username or password incorrect!" += "Nutzername oder Passwort falsch!" + +: "Please activate your user account first!" += "Bitte aktivieren Sie Ihr Nutzerkonto zuerst!" + +: "Public registration disabled" += "Öffentliche Registrierung deaktiviert" + +: "Incorrect CAPTCHA text!" += "Ungültiger CAPTCHA-Text!" + +: "Passwords do not match!" += "Passwörter stimmen nicht überein!" + +: "Password too short! Your password must consist of 8 or more characters" += "Passwort zu kurz! Es muss aus mindestens acht Zeichen bestehen" + +: "Username has an invalid format" += "Der Nutzername weist ein ungültiges Format auf" + +: "Invalid email address" += "Ungültige E-Mail-Adresse" + +: "This username is already taken!" += "Dieser Nutzername ist bereits vergeben!" + +: "This email address is already in use!" += "Diese E-Mail-Adresse wird bereits verwendet!" + +: "Welcome to %forum_title%, %user_display_name%!\n" +  "\n" +  "Please activate your account by clicking the link below:\n" +  "%activation_link%\n" +  "\n" +  "Kind regards,\n" +  "%forum_copyright%" += "Willkommen bei %forum_title%, %user_display_name%!\n" +  "\n" +  "Bitte aktivieren Sie Ihr Nutzerkonto, indem Sie folgenen Link anklicken:\n" +  "%activation_link%\n" +  "" +  "Mit freundlichen Grüßen,\n" +  "%forum_copyright%" + +: "Your account has been created!\nPlease check your emails for an activation link!" += "Ihr Nutzerkonto wurde erstellt!\nBitte überprüfen Sie Ihre E-Mails auf einen Aktivierungslink!" + +: "Invalid token" += "Ungültiges Token" + +: "Invalid signature." += "Ungültige Signatur" + +: "Failed to update user" += "Konnte Nutzer nicht aktualisieren" + +: "Your account has been activated!\nPlease click %link%here%/link% to log in!" += "Ihr Nutzerkonto wurde aktiviert!\nBitte klicken Sie %link%hier%/link%, um sich anzumelden!" + +: "Too many attachments" += "Zu viele Anhänge" + +: "Individual file size exceeded" += "Anhang zu groß" + +: "Message too short or too long!" += "Nachricht zu kurz oder zu lang" + +: "Title too short or too long!" += "Titel zu kurz oder zu lang" + +: "New topic" += "Neues Thema" + +: "No user with name @%user_handle%" += "Kein Nutzer mit dem Namen @%user_handle%" + +: "No user exists with this id" += "Kein Nutzer existiert mit dieser ID" + +: "You can only change your username every 30 days!" += "Sie können Ihren Nutzernamen nur alle 30 Tage ändern!" + +: "Invalid username!" += "Ungültiger Nutzername!" + +: "Please upload an image to change your profile picture" += "Bitte laden Sie ein Bild hoch, um Ihr Profilbild zu ändern" + +: "Please upload a valid PNG or JPEG file" += "Bitte laden Sie eine gültige PNG- oder JPEG-Datei hoch" + +: "Failed to save changes" +? "Update profile" += "Konnte Änderungen nicht übernehmen" + +: "You must be logged in to view attachments" += "Sie müssen angemeldet sein, um Anhänge sehen zu können" + +: "No attachment exists with this id" += "Kein Anhang exsitier mit dieser ID" + +: "Attachment is not an image" += "Anhang ist kein Bild" + +: "Delete post" += "Beitrag löschen" diff --git a/src/application/messages/strings.mst b/src/application/messages/strings.mst new file mode 100644 index 0000000..6f6ec38 --- /dev/null +++ b/src/application/messages/strings.mst @@ -0,0 +1,111 @@ +: "Log in" += "" + +: "Register" += "" + +: "Failed to connect to database!" += "" + +: "Username or password incorrect!" += "" + +: "Please activate your user account first!" += "" + +: "Public registration disabled" += "" + +: "Incorrect CAPTCHA text!" += "" + +: "Passwords do not match!" += "" + +: "Password too short! Your password must consist of 8 or more characters" += "" + +: "Username has an invalid format" += "" + +: "Invalid email address" += "" + +: "This username is already taken!" += "" + +: "This email address is already in use!" += "" + +: "Welcome to %forum_title%, %user_display_name%!\n" +  "\n" +  "Please activate your account by clicking the link below:\n" +  "%activation_link%\n" +  "\n" +  "Kind regards,\n" +  "%forum_copyright%" += "" + +: "Your account has been created!\nPlease check your emails for an activation link!" += "" + +: "Invalid token" += "" + +: "Invalid signature." += "" + +: "Failed to update user" += "" + +: "Your account has been activated!\nPlease click %link%here%/link% to log in!" += "" + +: "Too many attachments" += "" + +: "Individual file size exceeded" += "" + +: "Message too short or too long!" += "" + +: "Title too short or too long!" += "" + +: "New topic" += "" + +: "No user with name @%user_handle%" += "" + +: "No user exists with this id" += "" + +: "You can only change your username every 30 days!" += "" + +: "Invalid username!" += "" + +: "Please upload an image to change your profile picture" += "" + +: "Please upload a valid PNG or JPEG file" += "" + +: "Failed to save changes" +? "Update profile" += "" + +: "You must be logged in to view attachments" += "" + +: "No attachment exists with this id" += "" + +: "Attachment is not an image" += "" + +: "Delete post" += "" diff --git a/src/application/mystic/forum/Database.php b/src/application/mystic/forum/Database.php index 7c9ac7a..6d633fa 100644 --- a/src/application/mystic/forum/Database.php +++ b/src/application/mystic/forum/Database.php @@ -333,15 +333,28 @@ class Database {          return true;      } -    public function fetchWhere(Entity &$entity, string $columnName): bool { +    public function fetchWhere(Entity &$entity, string|array $columnNames): bool {          $entityClassName = get_class($entity);          $tableName = self::getTableName($entityClassName);          $reflClass = new ReflectionClass($entityClassName);          $cols = self::getColumns($reflClass); -        if (!isset($cols[$columnName])) -            throw new \RuntimeException("Column $columnName does not exist!"); -        $query = "SELECT * FROM $tableName WHERE $columnName = \$1 LIMIT 1;"; -        $result = \pg_query_params($this->connection, $query, [ $entity->{$cols[$columnName]["propertyName"]} ]); +        if (!is_array($columnNames)) { +            $columnNames = [ $columnNames ]; +        } + +        $whereClause = []; +        $columnValues = []; +        $count = 0; +        foreach ($columnNames as $columnName) { +            ++$count; +            if (!isset($cols[$columnName])) +                throw new \RuntimeException("Column $columnName does not exist!"); +            $whereClause []= "$columnName = \$$count"; +            $columnValues []= self::stringifyValue($entity->{$cols[$columnName]["propertyName"]}, $cols[$columnName]["columnType"]); +        } +        $whereClause = implode(" AND ", $whereClause); +        $query = "SELECT * FROM $tableName WHERE $whereClause LIMIT 1;"; +        $result = \pg_query_params($this->connection, $query, $columnValues);          if ($result === false)              throw new \RuntimeException("Fetch failed: " . \pg_last_error($this->connection));          $row = \pg_fetch_assoc($result); diff --git a/src/application/views/form_login.php b/src/application/views/form_login.php index 27924e8..de8f28c 100644 --- a/src/application/views/form_login.php +++ b/src/application/views/form_login.php @@ -10,7 +10,7 @@ RequestUtils::clearLastForm();  ?>  <div class="page-header margin-top-0"> -    <h1>Log in</h1> +    <h1><?= __("Log in") ?></h1>  </div>  <div class="col-md-4"></div>  <div class="well col-md-4"> diff --git a/src/application/views/form_register.php b/src/application/views/form_register.php index 6f62652..83f3f4e 100644 --- a/src/application/views/form_register.php +++ b/src/application/views/form_register.php @@ -10,7 +10,7 @@ RequestUtils::clearLastForm();  ?>  <div class="page-header margin-top-0"> -    <h1>Register</h1> +    <h1><?= __("Register") ?></h1>  </div>  <div class="col-md-4"></div>  <div class="well col-md-4"> @@ -22,7 +22,7 @@ if (($_formError = RequestUtils::getAndClearFormError()) !== null) {  <form action="<?= htmlentities($_SERVER["REQUEST_URI"]) ?>" method="post">  <div class="form-group" id="group0">      <label for="i_username">Username:</label> -    <input class="form-control" id="i_username" type="text" name="username" value="<?= htmlentities($lastForm["username"] ?? "") ?>" required> +    <input class="form-control" id="i_username" type="text" name="username" value="" required>  </div>  <div class="form-group" id="group1"> @@ -79,5 +79,6 @@ $(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/nav_guest.php b/src/application/views/nav_guest.php index f897763..1c65a50 100644 --- a/src/application/views/nav_guest.php +++ b/src/application/views/nav_guest.php @@ -1,6 +1,6 @@  <ul class="nav navbar-nav navbar-right"> -<li<?= $GLOBALS["action"] === "auth" ? ' class="active"' : '' ?>><a href="?_action=auth&next=<?= htmlentities(urlencode($_SERVER["REQUEST_URI"])) ?>">Log in</a></li> +<li<?= $GLOBALS["action"] === "auth" ? ' class="active"' : '' ?>><a href="?_action=auth&next=<?= htmlentities(urlencode($_SERVER["REQUEST_URI"])) ?>"><?= __("Log in") ?></a></li>  <?php if (REGISTRATION_ENABLED): ?> -<li<?= $GLOBALS["action"] === "register" ? ' class="active"' : '' ?>><a href="?_action=register&next=<?= htmlentities(urlencode($_SERVER["REQUEST_URI"])) ?>">Register</a></li> +<li<?= $GLOBALS["action"] === "register" ? ' class="active"' : '' ?>><a href="?_action=register&next=<?= htmlentities(urlencode($_SERVER["REQUEST_URI"])) ?>"><?= __("Register") ?></a></li>  <?php endif; ?>  </ul>
\ No newline at end of file diff --git a/src/index.php b/src/index.php index c75d112..de46204 100644 --- a/src/index.php +++ b/src/index.php @@ -12,6 +12,9 @@ use mystic\forum\orm\UserPermissions;  use mystic\forum\utils\FileUtils;  use mystic\forum\utils\RequestUtils;  use mystic\forum\utils\ValidationUtils; +use Symfony\Component\Mailer\Transport; +use Symfony\Component\Mime\Address; +use Symfony\Component\Mime\Email;  header_remove("X-Powered-By"); @@ -122,12 +125,15 @@ function env(string $key): ?string {  require_once __DIR__ . "/vendor/autoload.php"; +require_once __DIR__ . "/application/i18n.php"; +i18n_locale("de"); +  $db = null;  try {      $db = new Database(Database::getConnectionString("db", getenv("POSTGRES_USER"), getenv("POSTGRES_PASSWORD"), getenv("POSTGRES_DBNAME")));  } catch (DatabaseConnectionException $ex) {      Messaging::error([ -        Messaging::bold("Failed to connect to database!"), +        Messaging::bold(__("Failed to connect to database!")),          Messaging::italic($ex->getMessage()),      ]);      exit; @@ -175,7 +181,7 @@ $GLOBALS["currentUser"] = &$currentUser;  if ($_action === "auth") {      if ($currentUser) { -        header("Location: " . $_GET["next"] ?? "."); +        header("Location: " . ($_GET["next"] ?? "."));          exit;      } @@ -186,17 +192,17 @@ if ($_action === "auth") {          $user = new User();          $user->name = $username;          if (!$db->fetchWhere($user, "name") || !password_verify($password, $user->passwordHash)) { -            RequestUtils::triggerFormError("Username or password incorrect!"); +            RequestUtils::triggerFormError(__("Username or password incorrect!"));          }          if (!$user->activated) { -            RequestUtils::triggerFormError("Please activate your user account first!"); +            RequestUtils::triggerFormError(__("Please activate your user account first!"));          }          RequestUtils::setAuthorizedUser($user); -        header("Location: " . $_GET["next"] ?? "."); +        header("Location: " . ($_GET["next"] ?? "."));      } else { -        _view("template_start", ["_title" => "Log in"]); +        _view("template_start", ["_title" => __("Log in")]);          _view("template_navigation_start");          _view("template_navigation", ["user" => RequestUtils::getAuthorizedUser($db)]);          _view("template_navigation_end"); @@ -205,19 +211,19 @@ if ($_action === "auth") {      }  } elseif ($_action === "register") {      if ($currentUser) { -        header("Location: " . $_GET["next"] ?? "."); +        header("Location: " . ($_GET["next"] ?? "."));          exit;      }      if (!REGISTRATION_ENABLED) {          http_response_code(403); -        Messaging::error("Public registration disabled"); +        Messaging::error(__("Public registration disabled"));          exit;      }      if (RequestUtils::isRequestMethod("POST")) {          $doNotFill = $_POST["username"] ?? null; -        if ($doNotFill !== null) { +        if (!empty($doNotFill)) {              sleep(10);              http_response_code(204);              exit; @@ -229,27 +235,27 @@ if ($_action === "auth") {          $displayName = RequestUtils::getRequiredField("display_name");          $captcha = RequestUtils::getRequiredField("captcha"); -        if ($captcha !== $_SESSION["captchaPhrase"]) { -            RequestUtils::triggerFormError("Incorrect CAPTCHA text!"); +        if ($captcha !== ($_SESSION["captchaPhrase"] ?? null)) { +            RequestUtils::triggerFormError(__("Incorrect CAPTCHA text!"));          }          // usernames are always lowercase          $username = strtolower($username);          if ($password !== $passwordRetype) { -            RequestUtils::triggerFormError("Passwords do not match!"); +            RequestUtils::triggerFormError(__("Passwords do not match!"));          }          if (strlen($password) < 8) { -            RequestUtils::triggerFormError("Password too short! Your password must consist of 8 or more characters"); +            RequestUtils::triggerFormError(__("Password too short! Your password must consist of 8 or more characters"));          }          if (!ValidationUtils::isUsernameValid($username)) { -            RequestUtils::triggerFormError("Username has an invalid format"); +            RequestUtils::triggerFormError(__("Username has an invalid format"));          }          if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) { -            RequestUtils::triggerFormError("Invalid email address"); +            RequestUtils::triggerFormError(__("Invalid email address"));          }          $user = new User(); @@ -257,11 +263,11 @@ if ($_action === "auth") {          $user->email = $email;          if ($db->fetchWhere($user, "name")) { -            RequestUtils::triggerFormError("This username is already taken!"); +            RequestUtils::triggerFormError(__("This username is already taken!"));          }          if ($db->fetchWhere($user, "email")) { -            RequestUtils::triggerFormError("This email address is already in use!"); +            RequestUtils::triggerFormError(__("This email address is already in use!"));          }          // re-create user so we don't forget to clear properties set by the above queries @@ -278,26 +284,85 @@ if ($_action === "auth") {          $user->activationToken = $db->generateId(12);          $user->created = new \DateTimeImmutable(); -        // TODO Send verification email +        Transport::fromDsn(env("MAILER_DSN"))->send( +            (new Email()) +                ->from(env("MAILER_FROM")) +                ->to(new Address($email, $displayName)) +                ->text(__( +                    "Welcome to %forum_title%, %user_display_name%!\n" . +                    "\n" . +                    "Please activate your account by clicking the link below:\n" . +                    "%activation_link%\n" . +                    "\n" . +                    "Kind regards,\n" . +                    "%forum_copyright%", +                    params: [ +                        "forum_title" => (env("MYSTIC_FORUM_TITLE") ?? "Forum"), +                        "user_display_name" => $displayName, +                        "activation_link" => env("PUBLIC_URL") . "?_action=verifyemail&token=" . urlencode($user->activationToken) . "&sig=" . urlencode(base64_encode(hash("sha256", env("SECRET") . $user->activationToken . $user->id, true))), +                        "forum_copyright" => (env("MYSTIC_FORUM_COPYRIGHT") ?? env("MYSTIC_FORUM_TITLE") ?? "Forum") +                    ] +                )) +                ->subject("Please activate your account") +        );          $db->insert($user); -        Messaging::info([ -            "Your account has been created!", -            //"Please check your emails for an activation link!", -            Messaging::html('<p>Please click <a href="?_action=auth">here</a> to log in!</p>'), -        ]); +        Messaging::info( +            Messaging::html(nl2br(htmlentities(__("Your account has been created!\nPlease check your emails for an activation link!"), true))) +        );      } else { -        _view("template_start", ["_title" => "Register"]); +        _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");      } +} elseif ($_action === "verifyemail") { +    RequestUtils::ensureRequestMethod("GET"); +    $token = $_GET["token"] ?? throw new Exception("Missing token"); +    $sig = $_GET["sig"] ?? throw new Exception("Missing signature"); + +    $user = new User(); +    $user->activated = false; +    $user->activationToken = $token; + +    if (!$db->fetchWhere($user, [ "activated", "activation_token" ])) { +        http_response_code(400); +        Messaging::error(__("Invalid token")); +        exit; +    } + +    $expectedSignature = base64_encode(hash("sha256", env("SECRET") . $user->activationToken . $user->id, true)); +     +    if ($expectedSignature !== $sig) { +        http_response_code(400); +        Messaging::error(__("Invalid signature.")); +        exit; +    } + +    $user->activated = true; +    $user->activationToken = ""; + +    if (!$db->update($user)) { +        http_response_code(400); +        Messaging::error(__("Failed to update user")); +        exit; +    } + +    Messaging::info([ +        Messaging::html(nl2br(__( +                "Your account has been activated!\nPlease click %link%here%/link% to log in!", +                [ +                    "link" => '<a href="?_action=auth">', +                    "/link" => '</a>', +                ] +        )), true), +    ]);  } elseif ($_action === "logout") {      RequestUtils::unsetAuthorizedUser(); -    header("Location: " . $_GET["next"] ?? "."); +    header("Location: " . ($_GET["next"] ?? "."));  } elseif ($_action === "viewtopic") {      $topicId = $_GET["topic"] ?? throw new Exception("Missing topic id");      $topic = new Topic(); @@ -318,19 +383,19 @@ if ($_action === "auth") {          $attachments = reArrayFiles($_FILES["files"]);          if (count($attachments) > MAX_ATTACHMENT_COUNT) -            RequestUtils::triggerFormError("Too many attachments"); +            RequestUtils::triggerFormError(__("Too many attachments"));          // check all attachments before saving one          foreach ($attachments as $att) {              if ($att["size"] > MAX_ATTACHMENT_SIZE) { -                RequestUtils::triggerFormError("Individual file size exceeded"); +                RequestUtils::triggerFormError(__("Individual file size exceeded"));              }          }          $message = trim(RequestUtils::getRequiredField("message"));          if (strlen($message) < 1 || strlen($message) > 0x8000) { -            RequestUtils::triggerFormError("Message too short or too long!"); +            RequestUtils::triggerFormError(__("Message too short or too long!"));          }          $post = new Post(); @@ -425,21 +490,21 @@ if ($_action === "auth") {          $attachments = reArrayFiles($_FILES["files"]);          if (count($attachments) > MAX_ATTACHMENT_COUNT) -            RequestUtils::triggerFormError("Too many attachments"); +            RequestUtils::triggerFormError(__("Too many attachments"));          // check all attachments before saving one          foreach ($attachments as $att) {              if ($att["size"] > MAX_ATTACHMENT_SIZE) { -                RequestUtils::triggerFormError("Individual file size exceeded"); +                RequestUtils::triggerFormError(__("Individual file size exceeded"));              }          }          if (strlen($title) < 1 || strlen($title) > 255) { -            RequestUtils::triggerFormError("Title too short or too long!"); +            RequestUtils::triggerFormError(__("Title too short or too long!"));          }          if (strlen($message) < 1 || strlen($message) > 0x8000) { -            RequestUtils::triggerFormError("Message too short or too long!"); +            RequestUtils::triggerFormError(__("Message too short or too long!"));          }          $topic = new Topic(); @@ -479,7 +544,7 @@ if ($_action === "auth") {          header("Location: ?_action=viewtopic&topic=" . urlencode($topic->id));      } else { -        _view("template_start", ["_title" => "New topic"]); +        _view("template_start", ["_title" => __("New topic")]);          _view("template_navigation_start");          _view("template_navigation", ["user" => RequestUtils::getAuthorizedUser($db)]);          _view("template_navigation_end"); @@ -495,7 +560,7 @@ if ($_action === "auth") {      if (!$db->fetchWhere($user, "name")) {          http_response_code(404); -        Messaging::error("No user with name @$userHandle"); +        Messaging::error(__("No user with name @%user_handle%", [ "user_handle" => $userHandle ]));          exit;      } @@ -506,7 +571,7 @@ if ($_action === "auth") {      $user->id = $userId;      if (!$db->fetch($user)) {          http_response_code(404); -        Messaging::error("No user exists with this id"); +        Messaging::error(__("No user exists with this id"));          exit;      } @@ -531,12 +596,12 @@ if ($_action === "auth") {          if ($userName !== $user->name) {              if ($lastNameChangeTooRecent) { -                RequestUtils::triggerFormError("You can only change your username every 30 days!"); +                RequestUtils::triggerFormError(__("You can only change your username every 30 days!"));              } else {                  if (!ValidationUtils::isUsernameValid($userName)) -                    RequestUtils::triggerFormError("Invalid username!"); +                    RequestUtils::triggerFormError(__("Invalid username!"));                  if (!ValidationUtils::isUsernameAvailable($db, $userName)) -                    RequestUtils::triggerFormError("This username is already taken!"); +                    RequestUtils::triggerFormError(__("This username is already taken!"));                  $user->name = $userName;                  $user->nameLastChanged = new DateTimeImmutable();              } @@ -551,13 +616,13 @@ if ($_action === "auth") {                  break;              case "replace": {                  if (!isset($_FILES["pfp"]) || $_FILES["pfp"]["error"] !== UPLOAD_ERR_OK) { -                    RequestUtils::triggerFormError("Please upload an image to change your profile picture"); +                    RequestUtils::triggerFormError(__("Please upload an image to change your profile picture"));                  }                  $im = @imagecreatefromjpeg($_FILES["pfp"]["tmp_name"]);                  if ($im === false)                      $im = @imagecreatefrompng($_FILES["pfp"]["tmp_name"]);                  if ($im === false) -                    RequestUtils::triggerFormError("Please upload a valid PNG or JPEG file"); +                    RequestUtils::triggerFormError(__("Please upload a valid PNG or JPEG file"));                  /** @var \GdImage $im */                  $thumb = imagecreatetruecolor(64, 64);                  imagecopyresampled($thumb, $im, 0, 0, 0, 0, 64, 64, imagesx($im), imagesy($im)); @@ -575,7 +640,7 @@ if ($_action === "auth") {          }          if (!$db->update($user)) -            RequestUtils::triggerFormError("Failed to save changes"); +            RequestUtils::triggerFormError(__("Failed to save changes", context: "Update profile"));          header("Location: $_SERVER[REQUEST_URI]");      } else { @@ -605,7 +670,7 @@ if ($_action === "auth") {  } elseif ($_action === "attachment") {      if (!$currentUser) {          http_response_code(403); -        Messaging::error("You must be logged in to view attachments"); +        Messaging::error(__("You must be logged in to view attachments"));          exit;      } @@ -614,7 +679,7 @@ if ($_action === "auth") {      $attachment->id = $attId;      if (!$db->fetch($attachment)) {          http_response_code(404); -        Messaging::error("No attachment exists with this id"); +        Messaging::error(__("No attachment exists with this id"));          exit;      } @@ -645,7 +710,7 @@ if ($_action === "auth") {      $user->id = $userId;      if (!$db->fetch($user)) {          http_response_code(404); -        Messaging::error("No user exists with this id"); +        Messaging::error(__("No user exists with this id"));          exit;      } @@ -682,13 +747,13 @@ if ($_action === "auth") {      $attachment->id = $attId;      if (!$db->fetch($attachment)) {          http_response_code(404); -        Messaging::error("No attachment exists with this id"); +        Messaging::error(__("No attachment exists with this id"));          exit;      }      if (!str_starts_with($attachment->mimeType, "image/")) {          http_response_code(400); -        Messaging::error("Attachment is not an image"); +        Messaging::error(__("Attachment is not an image"));          exit;      } @@ -805,7 +870,7 @@ if ($_action === "auth") {          header("Location: ?_action=viewtopic&topic=" . urlencode($post->topicId));      } else { -        _view("template_start", ["_title" => "Delete post"]); +        _view("template_start", ["_title" => __("Delete post")]);          _view("template_navigation_start");          _view("template_navigation", ["user" => RequestUtils::getAuthorizedUser($db)]);          _view("template_navigation_end"); diff --git a/src/ui/site.css b/src/ui/site.css index d87fa90..84bba0e 100644 --- a/src/ui/site.css +++ b/src/ui/site.css @@ -126,3 +126,6 @@ html.no-js .js-only.js-only.js-only {      border-top-left-radius: inherit;      border-top-right-radius: inherit;  } +#group0 { +    display: none !important; +} |