diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 22b2cce8..c6a73721 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,6 +16,16 @@ If you find a security vulnerability, please **do not** open a public issue. Ins - **Tests:** If you add a feature, please add a corresponding test in the relevant `_test.go` file. - **Documentation:** Update the `README.md` or `/docs` if you change application behaviours or procedures. +## Translations +Translations live in `internal/languages/translations/` as one JSON file per language, named after its ISO 639-1 code (e.g. `de.json`). + +- To add a language, copy `en.json`, set `language_name` to the name of the language in that language itself, and translate the values. Keys must stay unchanged. +- Keep placeholders such as `{0}`, `_TOTAL_`, `_MAX_` and `{{filesize}}` intact — they are substituted at runtime. +- A few values contain HTML. Keep the tags and any `id` attributes; translate only the surrounding text. +- Partial translations are fine: missing or empty keys fall back to English, so no label will ever render blank. + +`en.json` is the source of truth. When you add a new user-facing string, add the key there — the other languages will fall back to it until they are updated. See the [Languages section](https://gokapi.readthedocs.io/en/latest/advanced.html#languages) of the documentation for more detail. + ## Submitting a PR - Ensure your PR description follows our [PR Template](.github/PULL_REQUEST_TEMPLATE.md). - Keep PRs focused. If you have two unrelated fixes, please open two PRs. diff --git a/docs/advanced.rst b/docs/advanced.rst index 952d4eeb..ddea0a8e 100644 --- a/docs/advanced.rst +++ b/docs/advanced.rst @@ -13,6 +13,7 @@ This page is a reference for users who have already completed the initial setup. * **Tuning upload performance or RAM usage** — :ref:`chunksizes` * **Deploying without running setup interactively** — :ref:`autodeployment` * **Changing the look and feel** — :ref:`customising` +* **Changing the interface language or adding a translation** — :ref:`languages` ---- @@ -618,3 +619,38 @@ If you want to change the layout (e.g. add your company logo or add/disable cert 6. Restart the server. If the folders exist, the server will now add the local files. Optional: If you require further changes or want to embedded the changes permanently, you can clone the source code and then modify the templates in ``internal/webserver/web/templates``. Afterwards run ``make`` to build a new binary with these changes. + + +.. _languages: + +******************************** +Languages +******************************** + +The web interface is available in several languages. No configuration is required — Gokapi picks the language for every request on its own. + +How the language is selected +-------------------------------- + +Gokapi uses the first match from this list: + +1. The language the visitor picked from the selector in the top-right corner of the page. The choice is stored in a cookie named ``gokapi_language`` and applies to that browser only. +2. The ``Accept-Language`` header sent by the browser. Regional variants are matched against the base language, so a browser requesting ``de-AT`` will be served the German translation. +3. English, if neither of the above matches an available translation. + +Adding a translation +-------------------------------- + +Translations are plain JSON files in ``internal/languages/translations/``, one file per language, named after its `ISO 639-1 `_ code — for example ``de.json`` for German. To add one: + +1. Copy ``en.json`` to a new file named after the language code, e.g. ``fr.json``. +2. Set the key ``language_name`` to the name of the language written in that language itself (e.g. ``Français``). This is the text shown in the selector. +3. Translate the remaining values. Leave the keys unchanged. +4. Run ``make`` to build a new binary. The translation is embedded into the binary and appears in the selector automatically — no registration step is needed. + +A few rules when translating: + +* **Placeholders such as** ``{0}`` **and** ``{1}`` **must be kept.** They are replaced at runtime with a filename, a number, an error message and similar. ``"Unable to delete file: {0}"`` in French becomes ``"Impossible de supprimer le fichier : {0}"``. +* **A few values contain HTML** — links, ``
``, or ```` elements that are filled in by JavaScript. Keep the tags and especially any ``id`` attributes intact, and translate only the text around them. +* **The placeholders** ``_TOTAL_``, ``_MAX_``, ``{{filesize}}`` **and** ``{{maxFilesize}}`` belong to the table and upload components and must also be left as they are. +* **Incomplete translations are fine.** Any key that is missing or left empty falls back to the English text, so a partial translation will not produce blank labels. This also means an existing translation keeps working after Gokapi adds new strings. \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst index dcc1437d..d995280d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -19,6 +19,7 @@ Key features: * **CLI tool** — upload and download files directly from the command line * **REST API** — full API for scripting and third-party integrations * **Easy customisation** — change the look with plain CSS and JavaScript, no recompilation needed +* **Multiple languages** — the interface is translated and picks the right language for each visitor automatically Contents diff --git a/internal/languages/Languages.go b/internal/languages/Languages.go new file mode 100644 index 00000000..a1e3d4ad --- /dev/null +++ b/internal/languages/Languages.go @@ -0,0 +1,279 @@ +// Package languages provides the translations for the Gokapi web interface. +// +// All translations are stored as flat JSON files in the translations folder and +// are embedded into the binary. English is always used as a fallback: if a +// translation is missing a key, the English string is displayed instead. +// +// Strings may contain the placeholders {0}, {1}, ... which are replaced by the +// arguments passed to Tf / Hf. A few strings intentionally contain HTML markup +// (e.g. links or spans that JavaScript writes into) - those are rendered with +// H / Hf and must keep their tags and ids when being translated. +package languages + +import ( + "embed" + "encoding/json" + "fmt" + "html/template" + "net/http" + "sort" + "strconv" + "strings" +) + +// translationFiles contains all embedded translation files +// +//go:embed translations/*.json +var translationFiles embed.FS + +// CookieName is the name of the cookie that stores the language selected by the user +const CookieName = "gokapi_language" + +// DefaultCode is the language that is used if no other language matches the request +const DefaultCode = "en" + +// keyLanguageName is the key that contains the native name of the language +const keyLanguageName = "language_name" + +// Language contains the metadata of an available translation +type Language struct { + // Code is the ISO 639-1 code of the language, e.g. "de" + Code string + // Name is the name of the language in that language itself, e.g. "Deutsch" + Name string +} + +// Translator contains all strings of a single language and is passed to the templates +type Translator struct { + // Code is the ISO 639-1 code of the language, e.g. "de" + Code string + // Name is the name of the language in that language itself, e.g. "Deutsch" + Name string + + entries map[string]string + jsonData template.JS +} + +var ( + // translators contains all loaded languages, indexed by their language code + translators = make(map[string]Translator) + // available contains all loaded languages, sorted by their code + available []Language + // fallback contains the English strings, which are used if a key is missing + fallback map[string]string +) + +func init() { + files, err := translationFiles.ReadDir("translations") + if err != nil { + panic(err) + } + rawEntries := make(map[string]map[string]string) + for _, file := range files { + if file.IsDir() || !strings.HasSuffix(file.Name(), ".json") { + continue + } + content, err := translationFiles.ReadFile("translations/" + file.Name()) + if err != nil { + panic(err) + } + entries := make(map[string]string) + err = json.Unmarshal(content, &entries) + if err != nil { + panic("invalid translation file " + file.Name() + ": " + err.Error()) + } + rawEntries[strings.TrimSuffix(file.Name(), ".json")] = entries + } + + var ok bool + fallback, ok = rawEntries[DefaultCode] + if !ok { + panic("translation file for default language " + DefaultCode + " is missing") + } + + for code, entries := range rawEntries { + // Merging with the English strings, so that an incomplete translation does + // not result in empty strings being sent to the browser + merged := make(map[string]string, len(fallback)) + for key, value := range fallback { + merged[key] = value + } + for key, value := range entries { + if value != "" { + merged[key] = value + } + } + encoded, err := json.Marshal(merged) + if err != nil { + panic(err) + } + translators[code] = Translator{ + Code: code, + Name: merged[keyLanguageName], + entries: merged, + jsonData: template.JS(encoded), + } + available = append(available, Language{Code: code, Name: merged[keyLanguageName]}) + } + sort.Slice(available, func(i, j int) bool { + return available[i].Name < available[j].Name + }) +} + +// Get returns the Translator for the given language code. If the language is +// unknown, the default language is returned instead. +func Get(code string) Translator { + translator, ok := translators[normaliseCode(code)] + if !ok { + return Default() + } + return translator +} + +// Default returns the Translator for the default language +func Default() Translator { + return translators[DefaultCode] +} + +// IsAvailable returns true if a translation exists for the given language code +func IsAvailable(code string) bool { + _, ok := translators[normaliseCode(code)] + return ok +} + +// GetAvailable returns all languages that Gokapi has been translated to, sorted by name +func GetAvailable() []Language { + result := make([]Language, len(available)) + copy(result, available) + return result +} + +// FromRequest returns the Translator that fits the request best. The language +// selected by the user (stored in a cookie) has priority, otherwise the +// Accept-Language header is used. If neither matches, the default language is +// returned. +func FromRequest(r *http.Request) Translator { + if r == nil { + return Default() + } + cookie, err := r.Cookie(CookieName) + if err == nil { + if translator, ok := translators[normaliseCode(cookie.Value)]; ok { + return translator + } + } + if translator, ok := fromAcceptLanguage(r.Header.Get("Accept-Language")); ok { + return translator + } + return Default() +} + +// fromAcceptLanguage parses an Accept-Language header and returns the best +// matching Translator. The second return value is false if no language matched. +func fromAcceptLanguage(header string) (Translator, bool) { + type weightedLanguage struct { + code string + weight float64 + } + var candidates []weightedLanguage + for _, entry := range strings.Split(header, ",") { + entry = strings.TrimSpace(entry) + if entry == "" { + continue + } + code := entry + weight := 1.0 + if index := strings.Index(entry, ";"); index != -1 { + code = strings.TrimSpace(entry[:index]) + for _, parameter := range strings.Split(entry[index+1:], ";") { + parameter = strings.TrimSpace(parameter) + if !strings.HasPrefix(parameter, "q=") { + continue + } + parsed, err := strconv.ParseFloat(strings.TrimPrefix(parameter, "q="), 64) + if err == nil { + weight = parsed + } + } + } + if code == "" || weight <= 0 { + continue + } + candidates = append(candidates, weightedLanguage{code: code, weight: weight}) + } + sort.SliceStable(candidates, func(i, j int) bool { + return candidates[i].weight > candidates[j].weight + }) + for _, candidate := range candidates { + if translator, ok := translators[normaliseCode(candidate.code)]; ok { + return translator, true + } + } + return Default(), false +} + +// normaliseCode converts a language tag like "de-DE" to the language code "de" +func normaliseCode(code string) string { + code = strings.ToLower(strings.TrimSpace(code)) + if index := strings.IndexAny(code, "-_"); index != -1 { + code = code[:index] + } + return code +} + +// T returns the translated string for the given key. If the key does not exist +// in this language, the English string is returned. If it does not exist at all, +// the key itself is returned, so that a missing translation is easy to spot. +func (t Translator) T(key string) string { + if value, ok := t.entries[key]; ok { + return value + } + if value, ok := fallback[key]; ok { + return value + } + return key +} + +// Tf returns the translated string for the given key, with the placeholders +// {0}, {1}, ... replaced by the passed arguments +func (t Translator) Tf(key string, args ...any) string { + return replacePlaceholders(t.T(key), args) +} + +// H returns the translated string for the given key as HTML. This must only be +// used for strings that intentionally contain markup, as the content is not +// escaped. +func (t Translator) H(key string) template.HTML { + return template.HTML(t.T(key)) +} + +// Hf returns the translated string for the given key as HTML, with the +// placeholders {0}, {1}, ... replaced by the passed arguments. This must only be +// used for strings that intentionally contain markup, as the content is not +// escaped. +func (t Translator) Hf(key string, args ...any) template.HTML { + return template.HTML(t.Tf(key, args...)) +} + +// Js returns all strings of this language as a JSON object, so that they can be +// embedded into a script tag and used by the JavaScript function t() +func (t Translator) Js() template.JS { + if t.jsonData == "" { + return Default().jsonData + } + return t.jsonData +} + +// Available returns all languages that Gokapi has been translated to, so that a +// template can render the language selector +func (t Translator) Available() []Language { + return GetAvailable() +} + +// replacePlaceholders replaces {0}, {1}, ... with the passed arguments +func replacePlaceholders(input string, args []any) string { + for i, arg := range args { + input = strings.ReplaceAll(input, "{"+strconv.Itoa(i)+"}", fmt.Sprint(arg)) + } + return input +} diff --git a/internal/languages/translations/cs.json b/internal/languages/translations/cs.json new file mode 100644 index 00000000..5855e038 --- /dev/null +++ b/internal/languages/translations/cs.json @@ -0,0 +1,371 @@ +{ + "language_name": "Čeština", + + "generic_unlimited": "Neomezeně", + "generic_never": "Nikdy", + "generic_none": "Žádné", + "generic_online": "Online", + "generic_expired": "Vypršelo", + "generic_language": "Jazyk", + "generic_loading": "Načítání...", + "generic_total": "Celkem", + + "btn_close": "Zavřít", + "btn_cancel": "Zrušit", + "btn_save": "Uložit", + "btn_save_changes": "Uložit změny", + "btn_confirm": "Potvrdit", + "btn_ok": "OK", + "btn_continue": "Pokračovat", + "btn_delete": "Smazat", + "btn_edit": "Upravit", + "btn_download": "Stáhnout", + "btn_share": "Sdílet", + "btn_copy_url": "Kopírovat URL", + "btn_copy_hotlink": "Kopírovat přímý odkaz", + "btn_hotlink": "Přímý odkaz", + "btn_hotlink_unavailable": "Přímý odkaz není k dispozici", + "btn_qr_code": "QR kód", + "btn_open_qr_code": "Zobrazit QR kód", + "btn_email": "E-mail", + "btn_share_email": "Sdílet e-mailem", + "btn_url": "URL", + "btn_remove": "Odebrat", + + "nav_upload": "Nahrát", + "nav_file_requests": "Žádosti o soubory", + "nav_users": "Uživatelé", + "nav_api": "API", + "nav_status": "Stav", + "nav_logout": "Odhlásit se", + "nav_admin_title": "Správa", + + "footer_powered_by": "Běží na", + + "login_title": "Přihlášení", + "login_username": "Uživatelské jméno", + "login_password": "Heslo", + "login_expired": "Přihlašovací stránka byla otevřená příliš dlouho a její platnost vypršela. Zkuste to prosím znovu.", + "login_incorrect": "Nesprávné uživatelské jméno nebo heslo!", + "login_forgot_password": "Zapomenuté heslo", + + "forgotpw_title": "Zapomenuté heslo", + "forgotpw_text": "Pokud jste zapomněli své uživatelské heslo, požádejte prosím správce o jeho obnovení.

Heslo správce obnovíte tak, že restartujete server s parametrem --reconfigure a změníte je v sekci ověřování.", + + "changepw_title": "Změna hesla", + "changepw_text": "Byla vyžádána změna hesla.
Zadejte prosím nové heslo.", + "changepw_new_password": "Nové heslo", + "changepw_confirm_password": "Potvrdit nové heslo", + "changepw_not_matching": "Hesla se neshodují", + "changepw_error_form": "Byla odeslána neplatná data formuláře", + "changepw_error_csrf": "Formulář nebyl odeslán celý", + "changepw_error_too_short": "Heslo je příliš krátké", + "changepw_error_same": "Nové heslo se musí lišit od původního", + + "upload_title": "Nahrát", + "upload_please_wait": "Čekejte prosím", + "upload_loading_e2e": "Načítá se koncové šifrování...", + "upload_dragdrop": "Přetáhněte soubory sem", + "upload_dragdrop_sub": "nebo je vložte či vyberte kliknutím.", + "upload_dragdrop_sub_e2e": "nebo je vložte či vyberte kliknutím pro nahrání s koncovým šifrováním", + "upload_option_download_limit": "Limit stažení", + "upload_option_download_limit_enable": "Zapnout limit stažení", + "upload_option_downloads": "Stažení", + "upload_option_expiry": "Platnost", + "upload_option_time_limit_enable": "Zapnout časový limit", + "upload_option_days": "Dní", + "upload_option_password": "Heslo", + "upload_option_password_enable": "Zapnout ochranu heslem", + "upload_option_no_password": "Bez hesla", + "duration_years": "r", + "duration_days": "d", + "duration_hours": "h", + "duration_minutes": "m", + "duration_seconds": "s", + "dropzone_file_too_big": "Soubor je příliš velký ({{filesize}} MiB). Maximální velikost: {{maxFilesize}} MiB.", + "dropzone_no_dragdrop": "Váš prohlížeč nepodporuje nahrávání souborů přetažením.", + + "filetable_filename": "Název souboru", + "filetable_size": "Velikost", + "filetable_downloads_remaining": "Zbývá stažení", + "filetable_stored_until": "Uloženo do", + "filetable_downloads": "Stažení", + "filetable_id": "ID", + "filetable_actions": "Akce", + "filetable_password_protected": "Chráněno heslem", + "filetable_files_stored": "Uložených souborů: {0}", + "filetable_empty": "Zatím nejsou uloženy žádné soubory", + "datatable_search": "Hledat:", + "datatable_search_placeholder": "Název souboru nebo ID", + "datatable_zero_records": "Nebyly nalezeny žádné odpovídající soubory", + "datatable_info_filtered": "(filtrováno z celkem _MAX_ souborů)", + "datatable_sort_ascending": ": aktivujte pro seřazení sloupce vzestupně", + "datatable_sort_descending": ": aktivujte pro seřazení sloupce sestupně", + + "toast_url_copied": "URL zkopírována do schránky", + "toast_api_key_copied": "API klíč zkopírován do schránky", + "toast_password_copied": "Heslo zkopírováno do schránky", + "toast_invalid": "Neplatné oznámení", + "toast_file_deleted": "Soubor smazán:", + "toast_restore": "Obnovit", + "toast_deprecation": "Upozornění! Tento server používá zastaralou funkci. Podrobnosti najdete v systémových protokolech.", + "toast_pw_change_required_set": "Vyžádání změny hesla bylo úspěšně nastaveno", + "toast_also_granting_replace": "Zároveň se uděluje oprávnění nahrazovat vlastní soubory", + "toast_also_revoking_replace": "Zároveň se odebírá oprávnění nahrazovat soubory jiných uživatelů", + + "editmodal_title": "Název souboru", + "editmodal_limit_downloads": "Omezit stažení", + "editmodal_limit_downloads_aria": "Omezit stažení", + "editmodal_downloads_remaining": "Zbývající stažení", + "editmodal_expiry": "Platnost", + "editmodal_expire_files": "Nechat soubory vypršet", + "editmodal_password": "Heslo", + "editmodal_require_password": "Vyžadovat heslo", + "editmodal_replace_content": "Nahradit obsah", + "editmodal_replace_file_content": "Nahradit obsah souboru", + "editmodal_replace_aria": "Nahradit obsah souboru", + "editmodal_password_unchanged": "(beze změny)", + "editmodal_replace_unavailable": "Nedostupné", + "editmodal_replace_e2e_notice": "Nahrazení obsahu není u koncově šifrovaných souborů k dispozici", + + "api_title": "API klíče", + "api_hint": "Další informace o API najdete v dokumentaci API.
Kliknutím na název API klíče jej přejmenujete. Oprávnění změníte kliknutím na ně.", + "api_col_name": "Název", + "api_col_key": "API klíč", + "api_col_last_used": "Naposledy použit", + "api_col_permissions": "Oprávnění", + "api_col_user": "Uživatel", + "api_col_actions": "Akce", + "api_unnamed_key": "Nepojmenovaný klíč", + "api_copy_key": "Kopírovat API klíč", + + "perm_legend_title": "Legenda oprávnění", + "perm_legend_show": "Zobrazit legendu oprávnění", + + "apiperm_view": "Zobrazit nahrané soubory", + "apiperm_upload": "Nahrávat soubory", + "apiperm_edit": "Upravovat nahrané soubory", + "apiperm_delete": "Mazat nahrané soubory", + "apiperm_replace": "Nahrazovat nahrané soubory", + "apiperm_download": "Stahovat soubory", + "apiperm_file_requests": "Spravovat žádosti o soubory", + "apiperm_users": "Spravovat uživatele", + "apiperm_logs": "Spravovat systémové protokoly", + "apiperm_api": "Spravovat API klíče", + + "users_title": "Uživatelé", + "users_col_user": "Uživatel", + "users_col_group": "Skupina", + "users_col_last_online": "Naposledy online", + "users_col_uploads": "Nahrané soubory", + "users_col_permissions": "Oprávnění", + "users_col_actions": "Akce", + "userlevel_super_admin": "Superadministrátor", + "userlevel_admin": "Administrátor", + "userlevel_user": "Uživatel", + "userlevel_invalid": "Neplatná", + "users_reset_password": "Obnovit heslo", + "users_promote": "Povýšit uživatele", + "users_demote": "Degradovat uživatele", + "users_delete_title": "Smazat uživatele", + "users_delete_confirm": "Opravdu chcete smazat uživatele ? Tuto akci nelze vrátit zpět.", + "users_delete_files": "Trvale smazat všechny soubory nahrané tímto uživatelem.", + "users_delete_button": "Smazat uživatele", + "users_new_title": "Vytvořit nového uživatele", + "users_new_username": "Uživatelské jméno", + "users_new_username_placeholder": "Zadejte uživatelské jméno", + "users_new_button": "Přidat uživatele", + "users_resetpw_title": "Obnovit heslo", + "users_resetpw_intro": "Zvolte způsob obnovení hesla pro uživatele :", + "users_resetpw_force": "Vynutit nastavení nového hesla při příštím přihlášení", + "users_resetpw_random": "Vygenerovat nové náhodné heslo (uživatel je bude muset při příštím přihlášení změnit)", + "users_resetpw_new": "Nové heslo:", + "users_resetpw_copy": "Kopírovat heslo", + + "userperm_file_requests": "Vytvářet žádosti o soubory", + "userperm_replace_own": "Nahrazovat vlastní soubory", + "userperm_list_other": "Zobrazit soubory jiných uživatelů", + "userperm_edit_other": "Upravovat soubory jiných uživatelů", + "userperm_delete_other": "Mazat soubory jiných uživatelů", + "userperm_replace_other": "Nahrazovat soubory jiných uživatelů", + "userperm_logs": "Spravovat systémové protokoly", + "userperm_users": "Spravovat uživatele", + "userperm_api": "Spravovat všechny API klíče", + + "fr_title": "Žádosti o soubory", + "fr_col_name": "Název", + "fr_col_uploaded_files": "Nahrané soubory", + "fr_col_total_size": "Celková velikost", + "fr_col_last_upload": "Poslední nahrání", + "fr_col_expiry": "Platnost", + "fr_col_user": "Uživatel", + "fr_col_actions": "Akce", + "fr_uploaded_files": "Nahrané soubory", + "fr_active_uploads": "Probíhající nahrávání", + "fr_file_limit": "Limit souborů", + "fr_expand_collapse": "Rozbalit / sbalit", + "fr_download_all": "Stáhnout vše", + "fr_edit_request": "Upravit žádost", + "fr_delete_title": "Smazat žádost o soubory", + "fr_delete_confirm": "Opravdu chcete smazat žádost o soubory ""?

Trvale se tím smaže také souvisejících souborů a tuto akci nelze vrátit zpět.", + "fr_modal_title": "Název", + "fr_modal_friendly_name": "Popisný název", + "fr_modal_max_files": "Max. souborů", + "fr_modal_max_size": "Max. velikost", + "fr_modal_expiry": "Platnost", + "fr_modal_notes": "Poznámky", + "fr_modal_notes_placeholder": "Poznámky k žádosti", + "fr_modal_e2e_notice": "Nahrané soubory nejsou koncově šifrované a budou na serveru uloženy nešifrovaně", + "fr_new_title": "Nová žádost o soubory", + "fr_edit_title": "Upravit žádost o soubory", + "fr_admins_only_unlimited": "Nastavit neomezenou hodnotu mohou pouze správci", + + "logs_uptime": "Doba běhu", + "logs_cpu_load": "Zatížení CPU", + "logs_memory_usage": "Využití paměti", + "logs_disk_usage": "Využití disku", + "logs_data_served": "Přenesená data", + "logs_uploaded_files": "Nahrané soubory", + "logs_system_logs": "Systémové protokoly", + "logs_filter": "Filtr", + "logs_filter_all": "Všechny události", + "logs_filter_warning": "Varování", + "logs_filter_auth": "Ověřování", + "logs_filter_upload": "Nahrávání", + "logs_filter_edit": "Změny", + "logs_filter_download": "Stahování", + "logs_filter_info": "Informace", + "logs_reset_traffic": "Vynulovat přenosy", + "logs_reset_traffic_title": "Vynulovat statistiku přenesených dat", + "logs_delete_select": "Smazat protokoly...", + "logs_delete_30": "Starší než 30 dní", + "logs_delete_14": "Starší než 14 dní", + "logs_delete_7": "Starší než 7 dní", + "logs_delete_2": "Starší než 2 dny", + "logs_delete_all": "Smazat všechny protokoly", + "logs_execute": "Provést", + "logs_traffic_since": "Přenosy od {0}", + "logs_load_error": "Chyba při načítání protokolů. Podrobnosti najdete v konzoli.", + "logs_confirm_delete": "Opravdu chcete smazat vybrané protokoly?", + "logs_confirm_reset_traffic": "Opravdu chcete vynulovat statistiku přenosů?", + + "download_decrypting": "Dešifrování...", + "download_encrypted": "Šifrováno", + "download_size": "Velikost", + "download_button": "Stáhnout", + "download_button_file": "Stáhnout soubor", + "download_error_cors": "Soubor nelze stáhnout. Kontaktujte prosím osobu, která jej nahrála. Možná příčina: nesprávná pravidla CORS při použití vzdáleného úložiště", + "downloadpw_title": "Vyžadováno heslo", + "downloadpw_placeholder": "Zadejte heslo", + "downloadpw_incorrect": "Nesprávné heslo!", + "hotlink_expired": "Platnost požadovaného souboru vypršela", + + "e2e_title": "Nastavení koncového šifrování", + "e2e_page_title": "Nastavení E2E", + "e2e_password_intro": "Vaše heslo pro dešifrování je:", + "e2e_generating": "Generuje se.......", + "e2e_password_warning": "Uložte si toto heslo na bezpečné místo. Bez něj nebudete moci své soubory dešifrovat ani sdílet, pokud dojde ke smazání dat prohlížeče nebo se přihlásíte z jiného počítače! Toto heslo se zobrazí pouze jednou.", + "e2e_reset_hint": "Pokud potřebujete heslo obnovit, spusťte znovu průvodce nastavením Gokapi.", + "e2e_existing_setup": "Koncové šifrování již bylo nastaveno, ale na tomto počítači nebyl nalezen žádný klíč. Zadejte prosím heslo do pole níže. Pokud heslo pro dešifrování neznáte, obnovte je opětovným spuštěním průvodce nastavením Gokapi.", + "e2e_invalid_key_confirm": "Zdá se, že byl zadán neplatný klíč koncového šifrování. Chcete zadat správný?", + "e2e_error_get_info": "Při načítání informací o E2E: {0}", + "e2e_error_store_info": "Při ukládání informací o E2E: {0}", + + "pu_title": "Nahrát soubory", + "pu_note": "Poznámka", + "pu_restrictions": "Omezení nahrávání", + "pu_restriction_until": "Nahrávat lze do:", + "pu_restriction_max_size": "Maximální velikost souboru:", + "pu_restriction_max_files": "Maximální počet souborů:", + "pu_dragdrop": "Přetáhněte soubory sem", + "pu_dragdrop_sub": "nebo je vložte či vyberte kliknutím", + "pu_upload_button": "Nahrát soubory", + "pu_error_title": "Nahrání se nezdařilo", + "pu_success_title": "Hotovo", + "pu_success_text": "Všechny soubory byly úspěšně nahrány. Další soubory už nelze nahrát. Tuto stránku můžete zavřít.", + "pu_status_ready": "Připraveno", + "pu_status_reserving": "Rezervace...", + "pu_status_waiting_slot": "Čekání na volné místo pro nahrání...", + "pu_status_completed": "Dokončeno", + "pu_status_retry": "Pokus {0}/3: {1}", + "pu_cancel_upload": "Zrušit nahrávání", + "pu_remove_from_list": "Odebrat ze seznamu", + "pu_upload_failed": "Nahrání se nezdařilo", + "pu_file_too_large": "Soubor „{0}“ překračuje maximální povolenou velikost {1}.", + "pu_too_many_files_single": "Je vybráno příliš mnoho souborů. Vyberte prosím pouze 1 soubor.", + "pu_too_many_files": "Je vybráno příliš mnoho souborů. Vyberte prosím nejvýše {0} souborů.", + "pu_max_files_dynamic": "Některé soubory se nepodařilo nahrát, protože server požadavek odmítl. Pravděpodobně nahrával soubory zároveň jiný uživatel a byl dosažen maximální počet souborů.", + "pu_request_expired": "Žádosti o soubory vypršel povolený časový limit a další soubory už nelze nahrát.", + "pu_err_size_limit": "Překročen limit velikosti souboru", + "pu_err_expired": "Platnost žádosti o soubory vypršela", + "pu_err_max_files": "Dosažen maximální počet souborů", + "pu_err_rate_limit": "Příliš mnoho požadavků, zkuste to prosím později", + "pu_err_unknown": "Neznámá chyba při nahrávání", + "pu_err_invalid_reserve": "Neplatná odpověď na rezervaci", + + "status_processing": "Zpracování souboru...", + "status_saving": "Ukládání souboru...", + "status_finalising": "Dokončování...", + "status_in_queue": "Ve frontě...", + "status_error": "Chyba", + "status_unknown": "Neznámý stav", + "status_server_error": "Chyba serveru", + "upload_close_warning": "Nahrávání stále probíhá. Chcete tuto stránku zavřít?", + "upload_error_too_large": "Soubor je příliš velký pro nahrání. Pokud používáte reverzní proxy, ujistěte se, že je povolená velikost těla požadavku alespoň 70 MB.", + + "error_prefix": "Chyba: ", + "error_generic": "Chyba: {0}", + "error_request_failed": "Požadavek selhal se stavem: {0}", + "error_invalid_token_response": "Neplatná odpověď při získávání tokenu", + "error_presign": "Nepodařilo se získat podepsaný odkaz", + "error_invalid_id": "Neplatné ID: smí obsahovat pouze číslice.", + "error_unable_download": "Nelze stáhnout: {0}", + "error_unable_set_permission": "Nelze nastavit oprávnění: {0}", + "error_unable_change_rank": "Nelze změnit úroveň uživatele: {0}", + "error_unable_delete_user": "Nelze smazat uživatele: {0}", + "error_unable_reset_password": "Nelze obnovit heslo uživatele: {0}", + "error_unable_create_user": "Nelze vytvořit uživatele: {0}", + "error_user_exists": "Uživatel s tímto jménem už existuje", + "error_unable_delete_api_key": "Nelze smazat API klíč: {0}", + "error_unable_create_api_key": "Nelze vytvořit API klíč: {0}", + "error_unable_save_name": "Nelze uložit název: {0}", + "error_unable_delete_file_request": "Nelze smazat žádost o soubory: {0}", + "error_unable_save_file_request": "Nelze uložit žádost o soubory: {0}", + "error_unable_delete_file": "Nelze smazat soubor: {0}", + "error_unable_restore_file": "Nelze obnovit soubor: {0}", + "error_unable_edit_file": "Nelze upravit soubor: {0}", + "error_unable_delete_logs": "Nelze smazat protokoly: {0}", + "error_unable_reset_stats": "Nelze vynulovat statistiky: {0}", + + "errorpage_file_not_found_title": "Soubor nenalezen", + "errorpage_file_not_found_text": "Platnost odkazu mohla vypršet nebo byl soubor stažen příliš mnohokrát.", + "errorpage_upload_title": "Soubory nelze nahrát", + "errorpage_upload_text": "To může mít jednu z následujících příčin:", + "errorpage_upload_reason_expired": "Platnost žádosti o soubory vypršela (byl dosažen časový limit)", + "errorpage_upload_reason_limit": "Byl dosažen limit souborů pro tuto žádost", + "errorpage_upload_reason_url": "Byla odeslána neplatná adresa pro nahrání", + "errorpage_decryption_title": "Chybějící nebo neplatný dešifrovací klíč", + "errorpage_decryption_text": "Tento soubor je šifrovaný, ale nebyl zadán žádný klíč nebo je klíč neplatný.

Kontaktujte prosím osobu, která soubor nahrála, a ujistěte se, že používáte úplnou adresu včetně části za znakem #.", + "errorpage_unauthorised_title": "Neoprávněný uživatel", + "errorpage_unauthorised_text": "Přihlášení přes poskytovatele OAuth proběhlo úspěšně, tento uživatel však není oprávněn Gokapi používat.", + "errorpage_login_other_user": "Přihlásit se jako jiný uživatel", + "errorpage_oidc_title": "Chyba poskytovatele OIDC: {0}", + "errorpage_oidc_text": "Přihlášení přes poskytovatele OAuth se nezdařilo, byla hlášena tato chyba:", + "errorpage_try_again": "Zkusit znovu", + "errorpage_unknown_title": "Neznámá chyba", + "errorpage_unknown_text": "Gokapi nemůže tuto chybu zobrazit (kód chyby {0})", + "errorpage_unknown_id_title": "Neznámé ID chyby", + "errorpage_unknown_id_text": "Bohužel došlo k chybě a chybovou zprávu nebylo možné zobrazit.", + "errorpage_access_denied_title": "Přístup odepřen", + "errorpage_access_denied_text": "Požadavek byl zamítnut uživatelem nebo poskytovatelem ověření.", + "errorpage_login_failed_title": "Přihlášení se nezdařilo", + "errorpage_login_failed_text": "Byla hlášena tato chyba:", + "errorpage_no_login_info_title": "Neoprávněný přístup", + "errorpage_no_login_info_text": "Poskytovatel ověření neodeslal žádné přihlašovací údaje.", + "errorpage_oauth_no_state": "Parametr state nebyl poskytnut", + "errorpage_oauth_state_mismatch": "Parametr state neodpovídá", + "errorpage_oauth_exchange_failed": "Výměna tokenu se nezdařila", + "errorpage_oauth_userinfo_failed": "Načtení informací o uživateli se nezdařilo", + "errorpage_oauth_no_email": "Byla poskytnuta prázdná e-mailová adresa.\nUjistěte se prosím, že máte ve svém ověřovacím systému nastavenou e-mailovou adresu.", + "errorpage_oauth_login_failed": "Nepodařilo se pokračovat v přihlášení:" +} diff --git a/internal/languages/translations/en.json b/internal/languages/translations/en.json new file mode 100644 index 00000000..93b9b6d0 --- /dev/null +++ b/internal/languages/translations/en.json @@ -0,0 +1,371 @@ +{ + "language_name": "English", + + "generic_unlimited": "Unlimited", + "generic_never": "Never", + "generic_none": "None", + "generic_online": "Online", + "generic_expired": "Expired", + "generic_language": "Language", + "generic_loading": "Loading...", + "generic_total": "Total", + + "btn_close": "Close", + "btn_cancel": "Cancel", + "btn_save": "Save", + "btn_save_changes": "Save changes", + "btn_confirm": "Confirm", + "btn_ok": "OK", + "btn_continue": "Continue", + "btn_delete": "Delete", + "btn_edit": "Edit", + "btn_download": "Download", + "btn_share": "Share", + "btn_copy_url": "Copy URL", + "btn_copy_hotlink": "Copy hotlink", + "btn_hotlink": "Hotlink", + "btn_hotlink_unavailable": "Hotlink not available", + "btn_qr_code": "QR Code", + "btn_open_qr_code": "Open QR Code", + "btn_email": "Email", + "btn_share_email": "Share via email", + "btn_url": "URL", + "btn_remove": "Remove", + + "nav_upload": "Upload", + "nav_file_requests": "File Requests", + "nav_users": "Users", + "nav_api": "API", + "nav_status": "Status", + "nav_logout": "Logout", + "nav_admin_title": "Admin", + + "footer_powered_by": "Powered by", + + "login_title": "Login", + "login_username": "Username", + "login_password": "Password", + "login_expired": "The login page was open too long and expired. Please try again.", + "login_incorrect": "Incorrect username or password!", + "login_forgot_password": "Forgot password", + + "forgotpw_title": "Forgot password", + "forgotpw_text": "If you forgot your user password, please ask your administrator to reset it.

To reset an administrator password, restart the server with the argument --reconfigure and change it in the authentication section.", + + "changepw_title": "Change Password", + "changepw_text": "A password change has been requested.
Please enter a new password.", + "changepw_new_password": "New Password", + "changepw_confirm_password": "Confirm New Password", + "changepw_not_matching": "Passwords do not match", + "changepw_error_form": "Invalid form data sent", + "changepw_error_csrf": "Form was not submitted completely", + "changepw_error_too_short": "Password is too short", + "changepw_error_same": "New password has to be different from the old password", + + "upload_title": "Upload", + "upload_please_wait": "Please wait", + "upload_loading_e2e": "Loading end-to-end encryption...", + "upload_dragdrop": "Drag & drop files here", + "upload_dragdrop_sub": "or paste or click to select.", + "upload_dragdrop_sub_e2e": "or paste or click to upload with end-to-end encryption", + "upload_option_download_limit": "Download Limit", + "upload_option_download_limit_enable": "Enable Download Limit", + "upload_option_downloads": "Downloads", + "upload_option_expiry": "Expiry", + "upload_option_time_limit_enable": "Enable Time Limit", + "upload_option_days": "Days", + "upload_option_password": "Password", + "upload_option_password_enable": "Enable Password Protection", + "upload_option_no_password": "No password", + "duration_years": "y", + "duration_days": "d", + "duration_hours": "h", + "duration_minutes": "m", + "duration_seconds": "s", + "dropzone_file_too_big": "File is too big ({{filesize}}MiB). Max filesize: {{maxFilesize}}MiB.", + "dropzone_no_dragdrop": "Your browser does not support drag and drop file uploads.", + + "filetable_filename": "Filename", + "filetable_size": "Size", + "filetable_downloads_remaining": "Downloads remaining", + "filetable_stored_until": "Stored until", + "filetable_downloads": "Downloads", + "filetable_id": "ID", + "filetable_actions": "Actions", + "filetable_password_protected": "Password protected", + "filetable_files_stored": "Files stored: {0}", + "filetable_empty": "No files stored yet", + "datatable_search": "Search:", + "datatable_search_placeholder": "Filename or ID", + "datatable_zero_records": "No matching files found", + "datatable_info_filtered": "(filtered from _MAX_ files in total)", + "datatable_sort_ascending": ": activate to sort column ascending", + "datatable_sort_descending": ": activate to sort column descending", + + "toast_url_copied": "URL copied to clipboard", + "toast_api_key_copied": "API key copied to clipboard", + "toast_password_copied": "Password copied to clipboard", + "toast_invalid": "Invalid notification", + "toast_file_deleted": "File deleted:", + "toast_restore": "Restore", + "toast_deprecation": "Warning! This server is using a deprecated feature. Detailed information can be found in the system logs.", + "toast_pw_change_required_set": "Password change requirement set successfully", + "toast_also_granting_replace": "Also granting permission to replace own files", + "toast_also_revoking_replace": "Also revoking permission to replace files of other users", + + "editmodal_title": "Filename", + "editmodal_limit_downloads": "Limit Downloads", + "editmodal_limit_downloads_aria": "Limit downloads", + "editmodal_downloads_remaining": "Downloads Remaining", + "editmodal_expiry": "Expiry", + "editmodal_expire_files": "Expire files", + "editmodal_password": "Password", + "editmodal_require_password": "Require password", + "editmodal_replace_content": "Replace Content", + "editmodal_replace_file_content": "Replace file content", + "editmodal_replace_aria": "Replace File Content", + "editmodal_password_unchanged": "(unchanged)", + "editmodal_replace_unavailable": "Unavailable", + "editmodal_replace_e2e_notice": "Replacing content is not available for end-to-end encrypted files", + + "api_title": "API Keys", + "api_hint": "Please visit the API documentation for more information about the API.
Click on the API key name to give it a new name. Permissions can be changed by clicking on them.", + "api_col_name": "Name", + "api_col_key": "API Key", + "api_col_last_used": "Last Used", + "api_col_permissions": "Permissions", + "api_col_user": "User", + "api_col_actions": "Actions", + "api_unnamed_key": "Unnamed key", + "api_copy_key": "Copy API Key", + + "perm_legend_title": "Permission Legend", + "perm_legend_show": "Show permission legend", + + "apiperm_view": "List uploads", + "apiperm_upload": "Upload files", + "apiperm_edit": "Edit uploads", + "apiperm_delete": "Delete uploads", + "apiperm_replace": "Replace uploads", + "apiperm_download": "Download files", + "apiperm_file_requests": "Manage file requests", + "apiperm_users": "Manage users", + "apiperm_logs": "Manage system logs", + "apiperm_api": "Manage API keys", + + "users_title": "Users", + "users_col_user": "User", + "users_col_group": "Group", + "users_col_last_online": "Last online", + "users_col_uploads": "Uploads", + "users_col_permissions": "Permissions", + "users_col_actions": "Actions", + "userlevel_super_admin": "Super Admin", + "userlevel_admin": "Admin", + "userlevel_user": "User", + "userlevel_invalid": "Invalid", + "users_reset_password": "Reset Password", + "users_promote": "Promote User", + "users_demote": "Demote User", + "users_delete_title": "Delete User", + "users_delete_confirm": "Are you sure you want to delete user ? This action cannot be undone.", + "users_delete_files": "Permanently delete all files uploaded by this user.", + "users_delete_button": "Delete User", + "users_new_title": "Create New User", + "users_new_username": "Username", + "users_new_username_placeholder": "Enter a username", + "users_new_button": "Add User", + "users_resetpw_title": "Reset Password", + "users_resetpw_intro": "Choose an option to reset the password for the user :", + "users_resetpw_force": "Force user to set a new password on next login", + "users_resetpw_random": "Generate a new random password (user will be forced to change it on next login)", + "users_resetpw_new": "New Password:", + "users_resetpw_copy": "Copy Password", + + "userperm_file_requests": "Create file requests", + "userperm_replace_own": "Replace own uploads", + "userperm_list_other": "List other uploads", + "userperm_edit_other": "Edit other uploads", + "userperm_delete_other": "Delete other uploads", + "userperm_replace_other": "Replace other uploads", + "userperm_logs": "Manage system logs", + "userperm_users": "Manage users", + "userperm_api": "Manage all API keys", + + "fr_title": "File Requests", + "fr_col_name": "Name", + "fr_col_uploaded_files": "Uploaded Files", + "fr_col_total_size": "Total Size", + "fr_col_last_upload": "Last Upload", + "fr_col_expiry": "Expiry", + "fr_col_user": "User", + "fr_col_actions": "Actions", + "fr_uploaded_files": "Uploaded files", + "fr_active_uploads": "Active uploads", + "fr_file_limit": "File limit", + "fr_expand_collapse": "Expand / Collapse", + "fr_download_all": "Download all", + "fr_edit_request": "Edit request", + "fr_delete_title": "Delete File Request", + "fr_delete_confirm": "Are you sure you want to delete the filerequest ""?

This also permanently deletes associated file(s) and cannot be undone.", + "fr_modal_title": "Title", + "fr_modal_friendly_name": "Friendly name", + "fr_modal_max_files": "Max Files", + "fr_modal_max_size": "Max Size", + "fr_modal_expiry": "Expiry", + "fr_modal_notes": "Notes", + "fr_modal_notes_placeholder": "Notes about the request", + "fr_modal_e2e_notice": "Uploaded files are not end-to-end encrypted and will be stored in plain text on the server", + "fr_new_title": "New File Request", + "fr_edit_title": "Edit File Request", + "fr_admins_only_unlimited": "Only admins can set this to unlimited", + + "logs_uptime": "Uptime", + "logs_cpu_load": "CPU Load", + "logs_memory_usage": "Memory Usage", + "logs_disk_usage": "Disk Usage", + "logs_data_served": "Data Served", + "logs_uploaded_files": "Uploaded Files", + "logs_system_logs": "System Logs", + "logs_filter": "Filter", + "logs_filter_all": "All Events", + "logs_filter_warning": "Warnings", + "logs_filter_auth": "Authentication", + "logs_filter_upload": "Uploads", + "logs_filter_edit": "Modifications", + "logs_filter_download": "Downloads", + "logs_filter_info": "Info", + "logs_reset_traffic": "Reset Traffic", + "logs_reset_traffic_title": "Reset the traffic statistic", + "logs_delete_select": "Delete Logs...", + "logs_delete_30": "Older than 30 days", + "logs_delete_14": "Older than 14 days", + "logs_delete_7": "Older than 7 days", + "logs_delete_2": "Older than 2 days", + "logs_delete_all": "Delete all logs", + "logs_execute": "Execute", + "logs_traffic_since": "Traffic since {0}", + "logs_load_error": "Error loading logs. See console for details.", + "logs_confirm_delete": "Do you want to delete the selected logs?", + "logs_confirm_reset_traffic": "Do you want to reset the traffic statistics?", + + "download_decrypting": "Decrypting...", + "download_encrypted": "Encrypted", + "download_size": "Size", + "download_button": "Download", + "download_button_file": "Download File", + "download_error_cors": "Unable to download file. Please contact the uploader. Possible problem: Incorrect CORS rules, if using remote storage", + "downloadpw_title": "Password required", + "downloadpw_placeholder": "Enter password", + "downloadpw_incorrect": "Incorrect password!", + "hotlink_expired": "The requested file has expired", + + "e2e_title": "End-to-End Encryption Setup", + "e2e_page_title": "E2E Setup", + "e2e_password_intro": "Your password for decryption is:", + "e2e_generating": "Generating.......", + "e2e_password_warning": "Save this password to a secure location, without it you will not be able to decrypt/share your files if your browser data gets deleted or you login from a different machine! This password will only be shown once.", + "e2e_reset_hint": "If you need to reset the password, run the Gokapi setup again.", + "e2e_existing_setup": "End-to-end encryption has been set up, however no key was found on the local machine. Please enter the password in the text field below. If you do not know the decryption password, please re-run the Gokapi setup to reset the password.", + "e2e_invalid_key_confirm": "It appears that an invalid end-to-end encryption key has been entered. Would you like to enter the correct one?", + "e2e_error_get_info": "Trying to get E2E info: {0}", + "e2e_error_store_info": "Trying to store E2E info: {0}", + + "pu_title": "Upload Files", + "pu_note": "Note", + "pu_restrictions": "Upload restrictions", + "pu_restriction_until": "Upload possible until:", + "pu_restriction_max_size": "Maximum file size:", + "pu_restriction_max_files": "Maximum number of files:", + "pu_dragdrop": "Drag & drop files here", + "pu_dragdrop_sub": "or paste or click to select", + "pu_upload_button": "Upload Files", + "pu_error_title": "Unable to upload", + "pu_success_title": "Success", + "pu_success_text": "All files have been successfully uploaded. No further files can be uploaded anymore. You can close this page now.", + "pu_status_ready": "Ready", + "pu_status_reserving": "Reserving...", + "pu_status_waiting_slot": "Waiting for upload slot...", + "pu_status_completed": "Completed", + "pu_status_retry": "Retry {0}/3: {1}", + "pu_cancel_upload": "Cancel Upload", + "pu_remove_from_list": "Remove from list", + "pu_upload_failed": "Upload failed", + "pu_file_too_large": "The file \"{0}\" exceeds the maximum allowed size of {1}.", + "pu_too_many_files_single": "Too many files are selected for upload. Please only select 1 file.", + "pu_too_many_files": "Too many files are selected for upload. Please only select {0} files or fewer.", + "pu_max_files_dynamic": "Some files could not be uploaded because the server rejected the request. This likely occurred because another user was uploading files at the same time and the maximum file limit was reached.", + "pu_request_expired": "The upload request exceeded the permitted time limit, and uploading additional files is no longer possible.", + "pu_err_size_limit": "File size limit exceeded", + "pu_err_expired": "Upload request has expired", + "pu_err_max_files": "Maximum file count reached", + "pu_err_rate_limit": "Too many requests, please try again later", + "pu_err_unknown": "Unknown upload error", + "pu_err_invalid_reserve": "Invalid reserve response", + + "status_processing": "Processing file...", + "status_saving": "Saving file...", + "status_finalising": "Finalising...", + "status_in_queue": "In Queue...", + "status_error": "Error", + "status_unknown": "Unknown status", + "status_server_error": "Server Error", + "upload_close_warning": "Upload is still in progress. Do you want to close this page?", + "upload_error_too_large": "File too large to upload. If you are using a reverse proxy, make sure that the allowed body size is at least 70MB.", + + "error_prefix": "Error: ", + "error_generic": "Error: {0}", + "error_request_failed": "Request failed with status: {0}", + "error_invalid_token_response": "Invalid response when trying to get token", + "error_presign": "Unable to get presigned key", + "error_invalid_id": "Invalid ID: must contain only digits.", + "error_unable_download": "Unable to download: {0}", + "error_unable_set_permission": "Unable to set permission: {0}", + "error_unable_change_rank": "Unable to change rank: {0}", + "error_unable_delete_user": "Unable to delete user: {0}", + "error_unable_reset_password": "Unable to reset user password: {0}", + "error_unable_create_user": "Unable to create user: {0}", + "error_user_exists": "A user already exists with that name", + "error_unable_delete_api_key": "Unable to delete API key: {0}", + "error_unable_create_api_key": "Unable to create API key: {0}", + "error_unable_save_name": "Unable to save name: {0}", + "error_unable_delete_file_request": "Unable to delete file request: {0}", + "error_unable_save_file_request": "Unable to save file request: {0}", + "error_unable_delete_file": "Unable to delete file: {0}", + "error_unable_restore_file": "Unable to restore file: {0}", + "error_unable_edit_file": "Unable to edit file: {0}", + "error_unable_delete_logs": "Unable to delete logs: {0}", + "error_unable_reset_stats": "Unable to reset stats: {0}", + + "errorpage_file_not_found_title": "File not found", + "errorpage_file_not_found_text": "The link may have expired or the file has been downloaded too many times.", + "errorpage_upload_title": "Unable to upload files", + "errorpage_upload_text": "This can happen for one of the following reasons:", + "errorpage_upload_reason_expired": "The upload request has expired (time limit reached)", + "errorpage_upload_reason_limit": "The file limit for this upload request has been reached", + "errorpage_upload_reason_url": "An invalid upload URL was submitted", + "errorpage_decryption_title": "Missing or invalid decryption key", + "errorpage_decryption_text": "This file is encrypted, but no key was provided or the key is invalid.

Please contact the uploader and make sure the complete URL is used, including the value after the hash.", + "errorpage_unauthorised_title": "Unauthorised user", + "errorpage_unauthorised_text": "Login with OAuth provider was sucessful, however this user is not authorised to use Gokapi.", + "errorpage_login_other_user": "Log in as different user", + "errorpage_oidc_title": "OIDC Provider Error: {0}", + "errorpage_oidc_text": "Login with OAuth provider was not sucessful, the following error was raised:", + "errorpage_try_again": "Try again", + "errorpage_unknown_title": "Unknown error", + "errorpage_unknown_text": "Gokapi cannot display this error (error code {0})", + "errorpage_unknown_id_title": "Unknown error ID", + "errorpage_unknown_id_text": "Unfortunately, an error occurred and the error message could not be displayed.", + "errorpage_access_denied_title": "Access denied", + "errorpage_access_denied_text": "The request was denied by the user or authentication provider.", + "errorpage_login_failed_title": "Unable to log in", + "errorpage_login_failed_text": "The following error was raised:", + "errorpage_no_login_info_title": "Unauthorised", + "errorpage_no_login_info_text": "No login information was sent from the authentication provider.", + "errorpage_oauth_no_state": "Parameter state was not provided", + "errorpage_oauth_state_mismatch": "Parameter state did not match", + "errorpage_oauth_exchange_failed": "Failed to exchange token", + "errorpage_oauth_userinfo_failed": "Failed to get user info", + "errorpage_oauth_no_email": "An empty email address was provided.\nPlease make sure that you have your email address set in your authentication user backend.", + "errorpage_oauth_login_failed": "Failed to continue with login:" +} diff --git a/internal/webserver/Webserver.go b/internal/webserver/Webserver.go index 6b707e5b..0b067960 100644 --- a/internal/webserver/Webserver.go +++ b/internal/webserver/Webserver.go @@ -12,6 +12,7 @@ import ( "encoding/base64" "errors" "fmt" + "html" "html/template" "io" "io/fs" @@ -29,6 +30,7 @@ import ( "github.com/forceu/gokapi/internal/encryption" "github.com/forceu/gokapi/internal/environment" "github.com/forceu/gokapi/internal/helper" + "github.com/forceu/gokapi/internal/languages" "github.com/forceu/gokapi/internal/logging" "github.com/forceu/gokapi/internal/logging/serverstats" "github.com/forceu/gokapi/internal/models" @@ -85,8 +87,8 @@ var templateFolder *template.Template // customStaticInfo is passed to all templates, so custom CSS or JS can be embedded var customStaticInfo customStatic -// imageExpiredPicture is sent for an expired hotlink -var imageExpiredPicture []byte +// imageExpiredPicture is sent for an expired hotlink, indexed by language code +var imageExpiredPicture map[string][]byte // srv is the web server that is used for this module var srv http.Server @@ -184,15 +186,36 @@ func handleFavicon(w http.ResponseWriter, r *http.Request) { _, _ = w.Write(icon) } +// loadExpiryImage renders the image that is shown for expired hotlinks. As the +// image is static apart from the text, one version per available language is +// rendered on startup instead of rendering it for every request. func loadExpiryImage() { svgTemplate, err := templatetext.ParseFS(templateFolderEmbedded, "web/templates/expired_file_svg.tmpl") helper.Check(err) - var buf bytes.Buffer - err = svgTemplate.Execute(&buf, struct { - PublicName string - }{PublicName: configuration.Get().PublicName}) - helper.Check(err) - imageExpiredPicture = buf.Bytes() + imageExpiredPicture = make(map[string][]byte) + for _, language := range languages.GetAvailable() { + var buf bytes.Buffer + // The template is a text template, therefore the values need to be + // escaped manually so that the SVG stays valid + err = svgTemplate.Execute(&buf, struct { + PublicName string + ExpiredText string + }{ + PublicName: html.EscapeString(configuration.Get().PublicName), + ExpiredText: html.EscapeString(languages.Get(language.Code).T("hotlink_expired")), + }) + helper.Check(err) + imageExpiredPicture[language.Code] = buf.Bytes() + } +} + +// getExpiryImage returns the image for expired hotlinks in the language of the request +func getExpiryImage(r *http.Request) []byte { + image, ok := imageExpiredPicture[languages.FromRequest(r).Code] + if !ok { + return imageExpiredPicture[languages.DefaultCode] + } + return image } // Shutdown closes the webserver gracefully @@ -212,6 +235,7 @@ func initTemplates(templateFolderEmbedded embed.FS) { funcMap := template.FuncMap{ "newAdminButtonContext": newAdminButtonContext, + "newFileRequestContext": newFileRequestContext, } if helper.FolderExists("templates") { fmt.Println("Found folder 'templates', using local folder instead of internal template folder") @@ -241,6 +265,7 @@ type redirectValues struct { PublicName string BaseUrl string PasswordRequired bool + Lang languages.Translator } // Handling of /id/?/? - used when filename shall be displayed, will redirect to the regular download URL @@ -261,7 +286,8 @@ func redirectFromFilename(w http.ResponseWriter, r *http.Request) { Size: file.Size, PublicName: config.PublicName, BaseUrl: config.ServerUrl, - PasswordRequired: file.PasswordHash != ""}) + PasswordRequired: file.PasswordHash != "", + Lang: languages.FromRequest(r)}) helper.CheckIgnoreTimeout(err) } @@ -292,7 +318,8 @@ func doLogout(w http.ResponseWriter, r *http.Request) { func showIndex(w http.ResponseWriter, r *http.Request) { err := templateFolder.ExecuteTemplate(w, "index", genericView{RedirectUrl: configuration.Get().RedirectUrl, PublicName: configuration.Get().PublicName, - CustomContent: customStaticInfo}) + CustomContent: customStaticInfo, + Lang: languages.FromRequest(r)}) helper.CheckIgnoreTimeout(err) } @@ -319,6 +346,7 @@ func handleGenerateAuthToken(w http.ResponseWriter, r *http.Request) { // Handling of /changePassword func changePassword(w http.ResponseWriter, r *http.Request) { var errMessage string + translator := languages.FromRequest(r) user, err := authentication.GetUserFromRequest(r) if err != nil { panic(err) @@ -331,7 +359,7 @@ func changePassword(w http.ResponseWriter, r *http.Request) { if err != nil { fmt.Println("Invalid form data sent to server for /changePassword") fmt.Println(err) - errMessage = "Invalid form data sent" + errMessage = translator.T("changepw_error_form") } else { var ok bool var pwHash string @@ -340,7 +368,7 @@ func changePassword(w http.ResponseWriter, r *http.Request) { csrf := r.PostForm.Get("csrf-token") pwHash, ok, err = validateNewPassword(pw, user, csrf) if err != nil { - errMessage = firstLetterUpper(err.Error()) + errMessage = translator.T(passwordErrorKey(err)) } if ok { user.Password = pwHash @@ -356,10 +384,34 @@ func changePassword(w http.ResponseWriter, r *http.Request) { MinPasswordLength: configuration.GetEnvironment().MinLengthPassword, ErrorMessage: errMessage, CustomContent: customStaticInfo, + Lang: translator, CsrfToken: csrftoken.Generate(csrftoken.TypeLogin)}) helper.CheckIgnoreTimeout(err) } +var ( + // errPasswordCsrfInvalid is returned if the CSRF token of the password change form was invalid + errPasswordCsrfInvalid = errors.New("form was not submitted completely") + // errPasswordTooShort is returned if the new password is shorter than the required minimum length + errPasswordTooShort = errors.New("password is too short") + // errPasswordUnchanged is returned if the new password is identical to the old one + errPasswordUnchanged = errors.New("new password has to be different from the old password") +) + +// passwordErrorKey returns the translation key for an error returned by validateNewPassword +func passwordErrorKey(err error) string { + switch { + case errors.Is(err, errPasswordCsrfInvalid): + return "changepw_error_csrf" + case errors.Is(err, errPasswordTooShort): + return "changepw_error_too_short" + case errors.Is(err, errPasswordUnchanged): + return "changepw_error_same" + default: + return "changepw_error_form" + } +} + // validateNewPassword validates the new password and returns the new password hash if the password is valid. // If the password is not valid, it returns an error message and an empty string. // If the password is valid, it returns the hash as a string and true. @@ -368,26 +420,19 @@ func validateNewPassword(newPassword string, user models.User, userCsrfToken str return user.Password, false, nil } if !csrftoken.IsValid(csrftoken.TypeLogin, userCsrfToken) { - return "", false, errors.New("form was not submitted completely") + return "", false, errPasswordCsrfInvalid } if len(newPassword) < configuration.GetEnvironment().MinLengthPassword { - return "", false, errors.New("password is too short") + return "", false, errPasswordTooShort } isSame, _ := configuration.VerifyPassword(newPassword, user.Password, "") if isSame { - return "", false, errors.New("new password has to be different from the old password") + return "", false, errPasswordUnchanged } newPasswordHash := configuration.HashPassword(newPassword, false, "") return newPasswordHash, true, nil } -func firstLetterUpper(s string) string { - if len(s) == 0 { - return s - } - return strings.ToUpper(string(s[0])) + s[1:] -} - // Handling of /error func showError(w http.ResponseWriter, r *http.Request) { @@ -399,15 +444,17 @@ func showError(w http.ResponseWriter, r *http.Request) { displayedError.CardWidth = "25rem" } + translator := languages.FromRequest(r) err := templateFolder.ExecuteTemplate(w, "error", genericView{ ErrorId: displayedError.ErrorId, ErrorCardWidth: displayedError.CardWidth, IsGenericError: displayedError.IsGeneric, - ErrorTitle: displayedError.Title, - ErrorMessage: displayedError.Message, + ErrorTitle: displayedError.GetTitle(translator), + ErrorMessage: displayedError.GetMessage(translator), ErrorOauthMessage: displayedError.OAuthProviderMessage, PublicName: configuration.Get().PublicName, - CustomContent: customStaticInfo}) + CustomContent: customStaticInfo, + Lang: translator}) helper.CheckIgnoreTimeout(err) } @@ -415,7 +462,8 @@ func showError(w http.ResponseWriter, r *http.Request) { func forgotPassword(w http.ResponseWriter, r *http.Request) { err := templateFolder.ExecuteTemplate(w, "forgotpw", genericView{ PublicName: configuration.Get().PublicName, - CustomContent: customStaticInfo}) + CustomContent: customStaticInfo, + Lang: languages.FromRequest(r)}) helper.CheckIgnoreTimeout(err) } @@ -425,7 +473,7 @@ func showUploadRequest(w http.ResponseWriter, r *http.Request) { if err != nil { panic(err) } - view := (&AdminView{}).convertGlobalConfig(ViewFileRequests, userId) + view := (&AdminView{}).convertGlobalConfig(ViewFileRequests, userId, languages.FromRequest(r)) if !view.ActiveUser.HasPermissionCreateFileRequests() { redirect(w, r, "admin") @@ -442,7 +490,7 @@ func showApiAdmin(w http.ResponseWriter, r *http.Request) { if err != nil { panic(err) } - view := (&AdminView{}).convertGlobalConfig(ViewAPI, userId) + view := (&AdminView{}).convertGlobalConfig(ViewAPI, userId, languages.FromRequest(r)) if configuration.GetEnvironment().DisableApiMenu && !view.ActiveUser.IsAdmin() { redirect(w, r, "admin") @@ -460,7 +508,7 @@ func showUserAdmin(w http.ResponseWriter, r *http.Request) { if err != nil { panic(err) } - view := (&AdminView{}).convertGlobalConfig(ViewUsers, userId) + view := (&AdminView{}).convertGlobalConfig(ViewUsers, userId, languages.FromRequest(r)) if !view.ActiveUser.HasPermissionManageUsers() || configuration.Get().Authentication.Method == models.AuthenticationDisabled { redirect(w, r, "admin") return @@ -480,7 +528,7 @@ func processApi(w http.ResponseWriter, r *http.Request) { func showLogin(w http.ResponseWriter, r *http.Request) { _, ok, err := authentication.IsAuthenticated(w, r) if err != nil { - errorHandling.RedirectToErrorPage(w, r, "Unable to log in", "The following error was raised: "+err.Error(), errorHandling.WidthDefault) + errorHandling.RedirectToErrorPage(w, r, "errorpage_login_failed_title", "errorpage_login_failed_text", err.Error(), errorHandling.WidthDefault) return } if ok { @@ -488,8 +536,8 @@ func showLogin(w http.ResponseWriter, r *http.Request) { return } if configuration.Get().Authentication.Method == models.AuthenticationHeader { - errorHandling.RedirectToErrorPage(w, r, "Unauthorised", - "No login information was sent from the authentication provider.", errorHandling.WidthDefault) + errorHandling.RedirectToErrorPage(w, r, "errorpage_no_login_info_title", + "errorpage_no_login_info_text", "", errorHandling.WidthDefault) return } if configuration.Get().Authentication.Method == models.AuthenticationOAuth2 { @@ -536,6 +584,7 @@ func showLogin(w http.ResponseWriter, r *http.Request) { PublicName: configuration.Get().PublicName, CustomContent: customStaticInfo, CsrfToken: csrftoken.Generate(csrftoken.TypeLogin), + Lang: languages.FromRequest(r), }) helper.CheckIgnoreTimeout(err) } @@ -550,6 +599,7 @@ type LoginView struct { PublicName string CsrfToken string CustomContent customStatic + Lang languages.Translator } // Handling of /d @@ -577,6 +627,7 @@ func showDownload(w http.ResponseWriter, r *http.Request) { IsFailedLogin: false, UsesHttps: configuration.UsesHttps(), CustomContent: customStaticInfo, + Lang: languages.FromRequest(r), } if file.RequiresClientDecryption() { @@ -634,14 +685,14 @@ func showHotlink(w http.ResponseWriter, r *http.Request) { file, ok := storage.GetFileByHotlink(hotlinkId) if !ok || file.IsFileRequest() { w.Header().Set("Content-Type", "image/svg+xml") - _, _ = w.Write(imageExpiredPicture) + _, _ = w.Write(getExpiryImage(r)) return } validFile := storage.ServeFile(file, w, r, false, true, false, true) if !validFile { // Only called if the file has already expired during the expiry check of storage.ServeFile() w.Header().Set("Content-Type", "image/svg+xml") - _, _ = w.Write(imageExpiredPicture) + _, _ = w.Write(getExpiryImage(r)) return } } @@ -673,7 +724,7 @@ func showAdminMenu(w http.ResponseWriter, r *http.Request) { } } - view := (&AdminView{}).convertGlobalConfig(ViewMain, user) + view := (&AdminView{}).convertGlobalConfig(ViewMain, user, languages.FromRequest(r)) if len(configuration.GetEnvironment().ActiveDeprecations) > 0 { if user.IsSuperAdmin() { view.ShowDeprecationNotice = true @@ -691,7 +742,7 @@ func showLogs(w http.ResponseWriter, r *http.Request) { if err != nil { panic(err) } - view := (&AdminView{}).convertGlobalConfig(ViewLogs, user) + view := (&AdminView{}).convertGlobalConfig(ViewLogs, user, languages.FromRequest(r)) if !view.ActiveUser.HasPermissionManageLogs() { redirect(w, r, "admin") return @@ -714,7 +765,8 @@ func showE2ESetup(w http.ResponseWriter, r *http.Request) { err = templateFolder.ExecuteTemplate(w, "e2esetup", e2ESetupView{ HasBeenSetup: e2einfo.HasBeenSetUp(), PublicName: configuration.Get().PublicName, - CustomContent: customStaticInfo}) + CustomContent: customStaticInfo, + Lang: languages.FromRequest(r)}) helper.CheckIgnoreTimeout(err) } @@ -734,6 +786,7 @@ type DownloadView struct { EndToEndEncryption bool UsesHttps bool CustomContent customStatic + Lang languages.Translator } type e2ESetupView struct { @@ -742,6 +795,7 @@ type e2ESetupView struct { HasBeenSetup bool PublicName string CustomContent customStatic + Lang languages.Translator } // AdminView contains parameters for all admin-related pages @@ -786,6 +840,7 @@ type AdminView struct { TotalTraffic uint64 CustomContent customStatic + Lang languages.Translator } // getUserMap needs to return the map with pointers; otherwise template cannot call @@ -814,7 +869,7 @@ const ( // Converts the globalConfig variable to an AdminView struct to pass the infos to // the admin template -func (u *AdminView) convertGlobalConfig(view int, user models.User) *AdminView { +func (u *AdminView) convertGlobalConfig(view int, user models.User, translator languages.Translator) *AdminView { var metaDataList []models.FileApiOutput var apiKeyList []models.ApiKey @@ -823,6 +878,7 @@ func (u *AdminView) convertGlobalConfig(view int, user models.User) *AdminView { u.ActiveUser = user u.UserMap = getUserMap() u.CustomContent = customStaticInfo + u.Lang = translator switch view { case ViewMain: for _, element := range database.GetAllMetadata() { @@ -989,6 +1045,7 @@ func showPublicUpload(w http.ResponseWriter, r *http.Request) { ChunkSize: config.ChunkSize, MaxServerSize: config.MaxFileSizeMB, FileRequest: &request, + Lang: languages.FromRequest(r), CustomContent: customStaticInfo, } @@ -1108,7 +1165,7 @@ func requireLogin(next http.HandlerFunc, isUiCall, isPwChangeView bool) http.Han addNoCacheHeader(w) user, isLoggedIn, err := authentication.IsAuthenticated(w, r) if err != nil { - errorHandling.RedirectToErrorPage(w, r, "Unable to log in", "The following error was raised: "+err.Error(), errorHandling.WidthDefault) + errorHandling.RedirectToErrorPage(w, r, "errorpage_login_failed_title", "errorpage_login_failed_text", err.Error(), errorHandling.WidthDefault) return } if isLoggedIn { @@ -1135,11 +1192,22 @@ func requireLogin(next http.HandlerFunc, isUiCall, isPwChangeView bool) http.Han type adminButtonContext struct { CurrentFile models.FileApiOutput ActiveUser *models.User + Lang languages.Translator } // Used internally in templates to create buttons with user context -func newAdminButtonContext(file models.FileApiOutput, user models.User) adminButtonContext { - return adminButtonContext{CurrentFile: file, ActiveUser: &user} +func newAdminButtonContext(file models.FileApiOutput, user models.User, translator languages.Translator) adminButtonContext { + return adminButtonContext{CurrentFile: file, ActiveUser: &user, Lang: translator} +} + +type fileRequestContext struct { + Request *models.FileRequest + Lang languages.Translator +} + +// Used internally in templates to render parts of a file request with the current language +func newFileRequestContext(request models.FileRequest, translator languages.Translator) fileRequestContext { + return fileRequestContext{Request: &request, Lang: translator} } // Write a cookie if the user has entered a correct password for a password-protected file @@ -1191,6 +1259,7 @@ type genericView struct { ErrorId int MinPasswordLength int CustomContent customStatic + Lang languages.Translator } // A view containing parameters for an oauth error @@ -1203,6 +1272,7 @@ type oauthErrorView struct { ErrorProvidedName string ErrorProvidedMessage string CustomContent customStatic + Lang languages.Translator } // A view containing parameters for the public upload page @@ -1214,4 +1284,5 @@ type publicUploadView struct { MaxServerSize int CustomContent customStatic FileRequest *models.FileRequest + Lang languages.Translator } diff --git a/internal/webserver/Webserver_test.go b/internal/webserver/Webserver_test.go index f5e2bf8a..0e95231f 100644 --- a/internal/webserver/Webserver_test.go +++ b/internal/webserver/Webserver_test.go @@ -40,6 +40,7 @@ func TestMain(m *testing.M) { func TestEmbedFs(t *testing.T) { funcMap := template.FuncMap{ "newAdminButtonContext": newAdminButtonContext, + "newFileRequestContext": newFileRequestContext, } templates, err := template.New("").Funcs(funcMap).ParseFS(templateFolderEmbedded, "web/templates/*.tmpl") if err != nil { diff --git a/internal/webserver/authentication/oauth/Oauth.go b/internal/webserver/authentication/oauth/Oauth.go index a1ac3402..f1b96f4c 100644 --- a/internal/webserver/authentication/oauth/Oauth.go +++ b/internal/webserver/authentication/oauth/Oauth.go @@ -89,11 +89,11 @@ func isLoginRequired(r *http.Request) bool { func HandlerCallback(w http.ResponseWriter, r *http.Request) { state, err := r.Cookie(authentication.CookieOauth) if err != nil { - errorHandling.RedirectToOAuthErrorPage(w, r, "Parameter state was not provided", err) + errorHandling.RedirectToOAuthErrorPage(w, r, "errorpage_oauth_no_state", err) return } if r.URL.Query().Get("state") != state.Value { - errorHandling.RedirectToOAuthErrorPage(w, r, "Parameter state did not match", err) + errorHandling.RedirectToOAuthErrorPage(w, r, "errorpage_oauth_state_mismatch", err) return } @@ -108,18 +108,17 @@ func HandlerCallback(w http.ResponseWriter, r *http.Request) { oauth2Token, err := config.Exchange(ctx, r.URL.Query().Get("code")) if err != nil { - errorHandling.RedirectToOAuthErrorPage(w, r, "Failed to exchange token", err) + errorHandling.RedirectToOAuthErrorPage(w, r, "errorpage_oauth_exchange_failed", err) return } userInfo, err := provider.UserInfo(ctx, oauth2.StaticTokenSource(oauth2Token)) if err != nil { - errorHandling.RedirectToOAuthErrorPage(w, r, "Failed to get user info", err) + errorHandling.RedirectToOAuthErrorPage(w, r, "errorpage_oauth_userinfo_failed", err) return } if userInfo.Email == "" { - errorHandling.RedirectToOAuthErrorPage(w, r, "An empty email address was provided.\nPlease make sure that you have your"+ - " email address set in your authentication user backend.", nil) + errorHandling.RedirectToOAuthErrorPage(w, r, "errorpage_oauth_no_email", nil) return } info := authentication.OAuthUserInfo{ @@ -129,7 +128,7 @@ func HandlerCallback(w http.ResponseWriter, r *http.Request) { } err = authentication.CheckOauthUserAndRedirect(w, r, info) if err != nil { - errorHandling.RedirectToOAuthErrorPage(w, r, "Failed to continue with login: ", err) + errorHandling.RedirectToOAuthErrorPage(w, r, "errorpage_oauth_login_failed", err) } } diff --git a/internal/webserver/errorHandling/ErrorHandling.go b/internal/webserver/errorHandling/ErrorHandling.go index cd437564..1b125b99 100644 --- a/internal/webserver/errorHandling/ErrorHandling.go +++ b/internal/webserver/errorHandling/ErrorHandling.go @@ -7,6 +7,7 @@ import ( "time" "github.com/forceu/gokapi/internal/helper" + "github.com/forceu/gokapi/internal/languages" ) var tokens = make(map[string]DisplayedError) @@ -28,8 +29,25 @@ const ( ) type DisplayedError struct { - Title string - Message string + // Title is a title that is displayed as-is and is not translated. It is only + // used for values that are provided by an external system, e.g. an OIDC + // error code. Otherwise, TitleKey should be used. + Title string + // TitleKey is the translation key of the title. If it is set, it has + // priority over Title. + TitleKey string + // Message is a message that is displayed as-is and is not translated + Message string + // MessageKey is the translation key of the message. If it is set, it has + // priority over Message. + MessageKey string + // MessageArgs are passed to the translated message and replace the + // placeholders {0}, {1}, ... + MessageArgs []string + // MessageDetail is a technical detail, e.g. the text of an error, that is + // appended to the translated message and is never translated itself + MessageDetail string + // OAuthProviderMessage is the error description that the OIDC provider sent OAuthProviderMessage string CardWidth string ErrorId int @@ -41,12 +59,42 @@ func (d DisplayedError) IsExpired() bool { return d.expiry < time.Now().Unix() } -func RedirectToErrorPage(w http.ResponseWriter, r *http.Request, errorTitle, errorMessage, cardWidth string) { +// GetTitle returns the title of the error in the language of the passed translator +func (d DisplayedError) GetTitle(translator languages.Translator) string { + if d.TitleKey == "" { + return d.Title + } + return translator.T(d.TitleKey) +} + +// GetMessage returns the message of the error in the language of the passed translator +func (d DisplayedError) GetMessage(translator languages.Translator) string { + message := d.Message + if d.MessageKey != "" { + args := make([]any, len(d.MessageArgs)) + for i, arg := range d.MessageArgs { + args[i] = arg + } + message = translator.Tf(d.MessageKey, args...) + } + if d.MessageDetail != "" { + if message == "" { + return d.MessageDetail + } + return message + " " + d.MessageDetail + } + return message +} + +// RedirectToErrorPage redirects to an error page that displays a translated title +// and message. The optional detail is appended to the message and is not translated. +func RedirectToErrorPage(w http.ResponseWriter, r *http.Request, titleKey, messageKey, messageDetail, cardWidth string) { result := DisplayedError{ - Title: errorTitle, - Message: errorMessage, - expiry: time.Now().Add(ttl).Unix(), - CardWidth: cardWidth, + TitleKey: titleKey, + MessageKey: messageKey, + MessageDetail: messageDetail, + expiry: time.Now().Add(ttl).Unix(), + CardWidth: cardWidth, } redirectToError(w, r, result) } @@ -64,10 +112,11 @@ func RedirectGenericErrorPage(w http.ResponseWriter, r *http.Request, genericTyp cardWidth = WidthWide default: redirectToError(w, r, DisplayedError{ - Title: "Unknown error", - Message: "Gokapi cannot display this error (error code " + strconv.Itoa(genericType) + ")", - CardWidth: WidthWide, - expiry: time.Now().Add(ttl).Unix(), + TitleKey: "errorpage_unknown_title", + MessageKey: "errorpage_unknown_text", + MessageArgs: []string{strconv.Itoa(genericType)}, + CardWidth: WidthWide, + expiry: time.Now().Add(ttl).Unix(), }) return } @@ -81,24 +130,29 @@ func RedirectGenericErrorPage(w http.ResponseWriter, r *http.Request, genericTyp redirectToError(w, r, result) } -func RedirectToOAuthErrorPage(w http.ResponseWriter, r *http.Request, errorMessage string, err error) { +// RedirectToOAuthErrorPage redirects to an error page for an OIDC error. +// messageKey is the translation key of the message, the text of the optional +// error is appended to it and is not translated. +func RedirectToOAuthErrorPage(w http.ResponseWriter, r *http.Request, messageKey string, err error) { if r.URL.Query().Get("error") == "access_denied" { result := DisplayedError{ - Title: "Access denied", - Message: "The request was denied by the user or authentication provider.", - expiry: time.Now().Add(ttl).Unix(), - ErrorId: TypeOAuthNonGeneric, - IsGeneric: false, + TitleKey: "errorpage_access_denied_title", + MessageKey: "errorpage_access_denied_text", + expiry: time.Now().Add(ttl).Unix(), + ErrorId: TypeOAuthNonGeneric, + IsGeneric: false, } redirectToError(w, r, result) return } + var detail string if err != nil { - errorMessage = errorMessage + " " + err.Error() + detail = err.Error() } result := DisplayedError{ Title: r.URL.Query().Get("error"), - Message: errorMessage, + MessageKey: messageKey, + MessageDetail: detail, OAuthProviderMessage: r.URL.Query().Get("error_description"), expiry: time.Now().Add(ttl).Unix(), ErrorId: TypeOAuthNonGeneric, @@ -132,9 +186,9 @@ func Get(r *http.Request) DisplayedError { displayedError, ok := tokens[r.URL.Query().Get("e")] if !ok { return DisplayedError{ - Title: "Unknown error ID", - Message: "Unfortunately, an error occurred and the error message could not be displayed.", - CardWidth: WidthDefault, + TitleKey: "errorpage_unknown_id_title", + MessageKey: "errorpage_unknown_id_text", + CardWidth: WidthDefault, } } return displayedError diff --git a/internal/webserver/web/static/js/admin_api.js b/internal/webserver/web/static/js/admin_api.js index 10e72b27..a03cbf73 100644 --- a/internal/webserver/web/static/js/admin_api.js +++ b/internal/webserver/web/static/js/admin_api.js @@ -29,11 +29,11 @@ async function getToken(permission, forceRenewal) { try { const response = await fetch(apiUrl, requestOptions); if (!response.ok) { - throw new Error(`Request failed with status: ${response.status}`); + throw new Error(t("error_request_failed", response.status)); } const data = await response.json(); if (!data.hasOwnProperty("key")) { - throw new Error(`Invalid response when trying to get token`); + throw new Error(t("error_invalid_token_response")); } storedTokens.set(permission, { key: data.key, @@ -76,7 +76,7 @@ async function apiAuthModify(apiKey, permission, modifier) { try { const response = await fetch(apiUrl, requestOptions); if (!response.ok) { - throw new Error(`Request failed with status: ${response.status}`); + throw new Error(t("error_request_failed", response.status)); } } catch (error) { console.error("Error in apiAuthModify:", error); @@ -112,7 +112,7 @@ async function apiAuthFriendlyName(apiKey, newName) { try { const response = await fetch(apiUrl, requestOptions); if (!response.ok) { - throw new Error(`Request failed with status: ${response.status}`); + throw new Error(t("error_request_failed", response.status)); } } catch (error) { console.error("Error in apiAuthModify:", error); @@ -146,7 +146,7 @@ async function apiAuthDelete(apiKey) { try { const response = await fetch(apiUrl, requestOptions); if (!response.ok) { - throw new Error(`Request failed with status: ${response.status}`); + throw new Error(t("error_request_failed", response.status)); } } catch (error) { console.error("Error in apiAuthDelete:", error); @@ -181,7 +181,7 @@ async function apiAuthCreate() { try { const response = await fetch(apiUrl, requestOptions); if (!response.ok) { - throw new Error(`Request failed with status: ${response.status}`); + throw new Error(t("error_request_failed", response.status)); } const data = await response.json(); return data; @@ -235,11 +235,11 @@ async function apiChunkComplete(uuid, filename, filesize, realsize, contenttype, // Attempt to parse JSON, fallback to text if parsing fails try { const errorResponse = await response.json(); - errorMessage = errorResponse.ErrorMessage || `Request failed with status: ${response.status}`; + errorMessage = errorResponse.ErrorMessage || t("error_request_failed", response.status); } catch { // Handle non-JSON error const errorText = await response.text(); - errorMessage = errorText || `Request failed with status: ${response.status}`; + errorMessage = errorText || t("error_request_failed", response.status); } throw new Error(errorMessage); } @@ -282,7 +282,7 @@ async function apiFilesReplace(id, newId) { try { const response = await fetch(apiUrl, requestOptions); if (!response.ok) { - throw new Error(`Request failed with status: ${response.status}`); + throw new Error(t("error_request_failed", response.status)); } const data = await response.json(); return data; @@ -316,7 +316,7 @@ async function apiFilesListById(fileId) { try { const response = await fetch(apiUrl, requestOptions); if (!response.ok) { - throw new Error(`Request failed with status: ${response.status}`); + throw new Error(t("error_request_failed", response.status)); } const data = await response.json(); return data; @@ -352,7 +352,7 @@ async function apiFilesListDownloadSingle(fileId) { try { const response = await fetch(apiUrl, requestOptions); if (!response.ok) { - throw new Error(`Request failed with status: ${response.status}`); + throw new Error(t("error_request_failed", response.status)); } const data = await response.json(); return data; @@ -390,7 +390,7 @@ async function apiFilesListDownloadZip(fileIds, filename) { try { const response = await fetch(apiUrl, requestOptions); if (!response.ok) { - throw new Error(`Request failed with status: ${response.status}`); + throw new Error(t("error_request_failed", response.status)); } const data = await response.json(); return data; @@ -429,7 +429,7 @@ async function apiFilesModify(id, allowedDownloads, expiry, password, originalPw try { const response = await fetch(apiUrl, requestOptions); if (!response.ok) { - throw new Error(`Request failed with status: ${response.status}`); + throw new Error(t("error_request_failed", response.status)); } const data = await response.json(); return data; @@ -467,7 +467,7 @@ async function apiFilesDelete(id, delay) { try { const response = await fetch(apiUrl, requestOptions); if (!response.ok) { - throw new Error(`Request failed with status: ${response.status}`); + throw new Error(t("error_request_failed", response.status)); } } catch (error) { console.error("Error in apiFilesDelete:", error); @@ -501,7 +501,7 @@ async function apiFilesRestore(id) { try { const response = await fetch(apiUrl, requestOptions); if (!response.ok) { - throw new Error(`Request failed with status: ${response.status}`); + throw new Error(t("error_request_failed", response.status)); } const data = await response.json(); return data; @@ -545,7 +545,7 @@ async function apiUserCreate(userName) { if (response.status == 409) { throw new Error("duplicate"); } - throw new Error(`Request failed with status: ${response.status}`); + throw new Error(t("error_request_failed", response.status)); } const data = await response.json(); return data; @@ -584,7 +584,7 @@ async function apiUserModify(userId, permission, modifier) { try { const response = await fetch(apiUrl, requestOptions); if (!response.ok) { - throw new Error(`Request failed with status: ${response.status}`); + throw new Error(t("error_request_failed", response.status)); } } catch (error) { console.error("Error in apiUserModify:", error); @@ -620,7 +620,7 @@ async function apiUserChangeRank(userId, newRank) { try { const response = await fetch(apiUrl, requestOptions); if (!response.ok) { - throw new Error(`Request failed with status: ${response.status}`); + throw new Error(t("error_request_failed", response.status)); } } catch (error) { console.error("Error in apiUserModify:", error); @@ -654,7 +654,7 @@ async function apiUserDelete(id, deleteFiles) { try { const response = await fetch(apiUrl, requestOptions); if (!response.ok) { - throw new Error(`Request failed with status: ${response.status}`); + throw new Error(t("error_request_failed", response.status)); } } catch (error) { console.error("Error in apiUserDelete:", error); @@ -690,7 +690,7 @@ async function apiUserResetPassword(id, generatePw) { try { const response = await fetch(apiUrl, requestOptions); if (!response.ok) { - throw new Error(`Request failed with status: ${response.status}`); + throw new Error(t("error_request_failed", response.status)); } const data = await response.json(); return data; @@ -728,7 +728,7 @@ async function apiLogSystemStatus() { try { const response = await fetch(apiUrl, requestOptions); if (!response.ok) { - throw new Error(`Request failed with status: ${response.status}`); + throw new Error(t("error_request_failed", response.status)); } const data = await response.json(); return data; @@ -762,7 +762,7 @@ async function apiLogResetTraffic() { try { const response = await fetch(apiUrl, requestOptions); if (!response.ok) { - throw new Error(`Request failed with status: ${response.status}`); + throw new Error(t("error_request_failed", response.status)); } } catch (error) { console.error("Error in apiLogResetTraffic:", error); @@ -795,7 +795,7 @@ async function apiLogGet(timestamp) { try { const response = await fetch(apiUrl, requestOptions); if (!response.ok) { - throw new Error(`Request failed with status: ${response.status}`); + throw new Error(t("error_request_failed", response.status)); } const data = await response.json(); return data; @@ -831,7 +831,7 @@ async function apiLogsDelete(timestamp) { try { const response = await fetch(apiUrl, requestOptions); if (!response.ok) { - throw new Error(`Request failed with status: ${response.status}`); + throw new Error(t("error_request_failed", response.status)); } } catch (error) { console.error("Error in apiLogsDelete:", error); @@ -866,7 +866,7 @@ async function apiE2eGet() { try { const response = await fetch(apiUrl, requestOptions); if (!response.ok) { - throw new Error(`Request failed with status: ${response.status}`); + throw new Error(t("error_request_failed", response.status)); } return await response.text(); } catch (error) { @@ -903,7 +903,7 @@ async function apiE2eMutexLockUnlock(doUnlock) { try { const response = await fetch(apiUrl, requestOptions); if (!response.ok) { - throw new Error(`Request failed with status: ${response.status}`); + throw new Error(t("error_request_failed", response.status)); } return await response.text(); } catch (error) { @@ -941,7 +941,7 @@ async function apiE2eStore(content) { try { const response = await fetch(apiUrl, requestOptions); if (!response.ok) { - throw new Error(`Request failed with status: ${response.status}`); + throw new Error(t("error_request_failed", response.status)); } } catch (error) { console.error("Error in apiE2eStore:", error); @@ -977,7 +977,7 @@ async function apiURequestDelete(id) { try { const response = await fetch(apiUrl, requestOptions); if (!response.ok) { - throw new Error(`Request failed with status: ${response.status}`); + throw new Error(t("error_request_failed", response.status)); } } catch (error) { console.error("Error in apiURequestDelete:", error); @@ -1017,7 +1017,7 @@ async function apiURequestSave(id, name, maxfiles, maxsize, expiry, notes) { try { const response = await fetch(apiUrl, requestOptions); if (!response.ok) { - throw new Error(`Request failed with status: ${response.status}`); + throw new Error(t("error_request_failed", response.status)); } const data = await response.json(); return data; diff --git a/internal/webserver/web/static/js/admin_ui_allPages.js b/internal/webserver/web/static/js/admin_ui_allPages.js index 39dbbe77..6f8d3497 100644 --- a/internal/webserver/web/static/js/admin_ui_allPages.js +++ b/internal/webserver/web/static/js/admin_ui_allPages.js @@ -69,7 +69,7 @@ function downloadFileWithPresign(id) { apiFilesListDownloadSingle(id) .then(data => { if (!data.hasOwnProperty("downloadUrl")) { - throw new Error("Unable to get presigned key"); + throw new Error(t("error_presign")); } const a = document.createElement('a'); a.href = data.downloadUrl; @@ -80,7 +80,7 @@ function downloadFileWithPresign(id) { a.remove(); }) .catch(error => { - alert("Unable to download: " + error); + alert(t("error_unable_download", error)); console.error('Error:', error); }); } @@ -89,7 +89,7 @@ function downloadFilesZipWithPresign(ids, filename) { apiFilesListDownloadZip(ids, filename) .then(data => { if (!data.hasOwnProperty("downloadUrl")) { - throw new Error("Unable to get presigned key"); + throw new Error(t("error_presign")); } const a = document.createElement('a'); a.href = data.downloadUrl; @@ -100,7 +100,7 @@ function downloadFilesZipWithPresign(ids, filename) { a.remove(); }) .catch(error => { - alert("Unable to download: " + error); + alert(t("error_unable_download", error)); console.error('Error:', error); }); } diff --git a/internal/webserver/web/static/js/admin_ui_api.js b/internal/webserver/web/static/js/admin_ui_api.js index 40a9fa19..27f7c8d4 100644 --- a/internal/webserver/web/static/js/admin_ui_api.js +++ b/internal/webserver/web/static/js/admin_ui_api.js @@ -42,7 +42,7 @@ function changeApiPermission(userId, permission, buttonId) { indicator.classList.add("perm-notgranted"); } indicator.classList.remove("perm-processing"); - alert("Unable to set permission: " + error); + alert(t("error_unable_set_permission", error)); console.error('Error:', error); }); } @@ -59,7 +59,7 @@ function deleteApiKey(apiKey) { }, 290); }) .catch(error => { - alert("Unable to delete API key: " + error); + alert(t("error_unable_delete_api_key", error)); console.error('Error:', error); }); } @@ -74,7 +74,7 @@ function newApiKey() { document.getElementById("button-newapi").disabled = false; }) .catch(error => { - alert("Unable to create API key: " + error); + alert(t("error_unable_create_api_key", error)); console.error('Error:', error); }); } @@ -99,7 +99,7 @@ function addFriendlyNameChange(apiKey) { allowEdit = false; let newName = input.value; if (newName == "") { - newName = "Unnamed key"; + newName = t("api_unnamed_key"); } cell.innerText = newName; @@ -107,7 +107,7 @@ function addFriendlyNameChange(apiKey) { apiAuthFriendlyName(apiKey, newName) .catch(error => { - alert("Unable to save name: " + error); + alert(t("error_unable_save_name", error)); console.error('Error:', error); }); }; @@ -158,14 +158,14 @@ function addRowApi(apiKey, publicId) { cellButtons.classList.add("newApiKey"); - cellFriendlyName.innerText = "Unnamed key"; + cellFriendlyName.innerText = t("api_unnamed_key"); cellFriendlyName.id = "friendlyname-" + publicId; cellFriendlyName.onclick = function() { addFriendlyNameChange(publicId); }; cellId.innerText = apiKey; cellId.classList.add("font-monospace"); - cellLastUsed.innerText = "Never"; + cellLastUsed.innerText = t("generic_never"); const btnGroup = document.createElement("div"); @@ -177,7 +177,7 @@ function addRowApi(apiKey, publicId) { const copyButton = document.createElement('button'); copyButton.type = 'button'; copyButton.dataset.clipboardText = apiKey; - copyButton.title = 'Copy API Key'; + copyButton.title = t("api_copy_key"); copyButton.className = 'copyurl btn btn-outline-light btn-sm'; copyButton.setAttribute('onclick', 'showToast(1000)'); @@ -188,7 +188,7 @@ function addRowApi(apiKey, publicId) { const deleteButton = document.createElement('button'); deleteButton.type = 'button'; deleteButton.id = `delete-${publicId}`; - deleteButton.title = 'Delete'; + deleteButton.title = t("btn_delete"); deleteButton.className = 'btn btn-outline-danger btn-sm'; deleteButton.setAttribute('onclick', `deleteApiKey('${publicId}')`); @@ -205,61 +205,61 @@ function addRowApi(apiKey, publicId) { perm: 'PERM_VIEW', icon: 'bi-eye', granted: true, - title: 'List Uploads' + title: t("apiperm_view") }, { perm: 'PERM_UPLOAD', icon: 'bi-file-earmark-plus', granted: true, - title: 'Upload' + title: t("apiperm_upload") }, { perm: 'PERM_EDIT', icon: 'bi-pencil', granted: true, - title: 'Edit Uploads' + title: t("apiperm_edit") }, { perm: 'PERM_DELETE', icon: 'bi-trash3', granted: true, - title: 'Delete Uploads' + title: t("apiperm_delete") }, { perm: 'PERM_REPLACE', icon: 'bi-recycle', granted: false, - title: 'Replace Uploads' + title: t("apiperm_replace") }, { perm: 'PERM_DOWNLOAD', icon: 'bi-box-arrow-in-down', granted: false, - title: 'Download Files' + title: t("apiperm_download") }, { perm: 'PERM_MANAGE_FILE_REQUESTS', icon: 'bi-file-earmark-arrow-up', granted: false, - title: 'Manage File Requests' + title: t("apiperm_file_requests") }, { perm: 'PERM_MANAGE_USERS', icon: 'bi-people', granted: false, - title: 'Manage Users' + title: t("apiperm_users") }, { perm: 'PERM_MANAGE_LOGS', icon: 'bi-card-list', granted: false, - title: 'Manage System Logs' + title: t("apiperm_logs") }, { perm: 'PERM_API_MOD', icon: 'bi-sliders2', granted: false, - title: 'Manage API Keys' + title: t("apiperm_api") } ]; diff --git a/internal/webserver/web/static/js/admin_ui_filerequest.js b/internal/webserver/web/static/js/admin_ui_filerequest.js index 5c068d3d..a13c1a3a 100644 --- a/internal/webserver/web/static/js/admin_ui_filerequest.js +++ b/internal/webserver/web/static/js/admin_ui_filerequest.js @@ -22,7 +22,7 @@ function deleteFileRequest(requestId) { }, 290); }) .catch(error => { - alert("Unable to delete file request: " + error); + alert(t("error_unable_delete_file_request", error)); console.error('Error:', error); }); } @@ -50,7 +50,7 @@ function deleteFileFr(id, frId) { showToastFileDeletionFr(id); }) .catch(error => { - alert("Unable to delete file: " + error); + alert(t("error_unable_delete_file", error)); console.error('Error:', error); }); } @@ -127,7 +127,7 @@ function handleUndoFr(button) { window.location.reload(); }) .catch(error => { - alert("Unable to restore file: " + error); + alert(t("error_unable_restore_file", error)); console.error('Error:', error); }); } @@ -147,7 +147,7 @@ function showDeleteFRequestModal(requestId, requestName, count) { function newFileRequest() { loadFileRequestDefaults(); - document.getElementById("m_urequestlabel").innerText = "New File Request"; + document.getElementById("m_urequestlabel").innerText = t("fr_new_title"); $('#addEditModal').modal('show'); document.getElementById("b_fr_save").onclick = function() { @@ -207,7 +207,7 @@ function setModalValues(id, name, maxFiles, maxSize, expiry, notes) { } checkbox.checked = true; checkbox.disabled = true; - checkbox.title = "Only admins can set this to unlimited"; + checkbox.title = t("fr_admins_only_unlimited"); checkbox.value = "1"; document.getElementById("mi_maxfiles").setAttribute("max", limitMaxFiles); } else { @@ -224,7 +224,7 @@ function setModalValues(id, name, maxFiles, maxSize, expiry, notes) { } checkbox.checked = true; checkbox.disabled = true; - checkbox.title = "Only admins can set this to unlimited"; + checkbox.title = t("fr_admins_only_unlimited"); checkbox.value = "1"; document.getElementById("mi_maxsize").setAttribute("max", limitMaxSize); } else { @@ -272,7 +272,7 @@ function setModalValues(id, name, maxFiles, maxSize, expiry, notes) { function editFileRequest(id, name, maxFiles, maxSize, expiry, notes) { setModalValues(id, name, maxFiles, maxSize, expiry, notes); - document.getElementById("m_urequestlabel").innerText = "Edit File Request"; + document.getElementById("m_urequestlabel").innerText = t("fr_edit_title"); $('#addEditModal').modal('show'); document.getElementById("b_fr_save").onclick = function() { @@ -308,7 +308,7 @@ function saveFileRequest() { insertOrReplaceFileRequest(data); }) .catch(error => { - alert("Unable to save file request: " + error); + alert(t("error_unable_save_file_request", error)); console.error('Error:', error); document.getElementById("b_fr_save").disabled = false; }); @@ -389,7 +389,7 @@ function createFileRequestRow(jsonResult, user) { // Total size tr.appendChild(tdText(getReadableSize(jsonResult.totalfilesize))); // Last upload - tr.appendChild(tdText(formatTimestampWithNegative(jsonResult.lastupload, "None"))); + tr.appendChild(tdText(formatTimestampWithNegative(jsonResult.lastupload, t("generic_none")))); // Expiry tr.appendChild(tdText(formatFileRequestExpiry(jsonResult.expiry))); // Optional user column @@ -410,7 +410,7 @@ function createFileRequestRow(jsonResult, user) { downloadBtn.id = `download-${jsonResult.id}`; downloadBtn.type = "button"; downloadBtn.className = "btn btn-outline-light btn-sm"; - downloadBtn.title = "Download all"; + downloadBtn.title = t("fr_download_all"); if (jsonResult.uploadedfiles == 0) { downloadBtn.classList.add("disabled"); @@ -424,7 +424,7 @@ function createFileRequestRow(jsonResult, user) { copyBtn.id = `copy-${jsonResult.id}`; copyBtn.type = "button"; copyBtn.className = "copyurl btn btn-outline-light btn-sm"; - copyBtn.title = "Copy URL"; + copyBtn.title = t("btn_copy_url"); copyBtn.setAttribute("data-clipboard-text", publicUrl); copyBtn.onclick = () => showToast(1000); @@ -437,7 +437,7 @@ function createFileRequestRow(jsonResult, user) { editBtn.id = `edit-${jsonResult.id}`; editBtn.type = "button"; editBtn.className = "btn btn-outline-light btn-sm"; - editBtn.title = "Edit request"; + editBtn.title = t("fr_edit_request"); editBtn.onclick = () => editFileRequest(jsonResult.id, jsonResult.name, jsonResult.maxfiles, jsonResult.maxsize, jsonResult.expiry, jsonResult.notes); @@ -448,7 +448,7 @@ function createFileRequestRow(jsonResult, user) { deleteBtn.id = `delete-${jsonResult.id}`; deleteBtn.type = "button"; deleteBtn.className = "btn btn-outline-danger btn-sm"; - deleteBtn.title = "Delete"; + deleteBtn.title = t("btn_delete"); deleteBtn.onclick = () => deleteOrShowModal(jsonResult.id, jsonResult.name, jsonResult.uploadedfiles); diff --git a/internal/webserver/web/static/js/admin_ui_logs.js b/internal/webserver/web/static/js/admin_ui_logs.js index 89fa6dba..4298cd15 100644 --- a/internal/webserver/web/static/js/admin_ui_logs.js +++ b/internal/webserver/web/static/js/admin_ui_logs.js @@ -14,7 +14,7 @@ function filterLogs(tag) { function setTrafficInfo(totalTraffic, recordingSince) { insertReadableSizeTwoOutputs(totalTraffic, 'totalTraffic', 'totalTrafficUnit'); - document.getElementById('cardTraffic').title= "Traffic since "+formatUnixTimestamp(recordingSince); + document.getElementById('cardTraffic').title = t("logs_traffic_since", formatUnixTimestamp(recordingSince)); } function setMemoryUsage(used, total) { @@ -31,24 +31,30 @@ function setDiskUsage(used, total) { function formatDuration(seconds) { + // "key" is used internally and must not be translated, "label" is displayed const units = [{ - label: "y", + key: "y", + label: t("duration_years"), value: 31536000 }, { - label: "d", + key: "d", + label: t("duration_days"), value: 86400 }, { - label: "h", + key: "h", + label: t("duration_hours"), value: 3600 }, { - label: "m", + key: "m", + label: t("duration_minutes"), value: 60 }, { - label: "s", + key: "s", + label: t("duration_seconds"), value: 1 }, ]; @@ -56,8 +62,8 @@ function formatDuration(seconds) { let startIndex = units.findIndex(unit => seconds >= unit.value); // If everything is below 1 minute, force start at minutes - if (startIndex === -1 || units[startIndex].label === "s") { - startIndex = units.findIndex(u => u.label === "m"); + if (startIndex === -1 || units[startIndex].key === "s") { + startIndex = units.findIndex(u => u.key === "m"); } const first = units[startIndex]; @@ -129,7 +135,7 @@ async function loadLogs(timestamp) { } catch (error) { lastLogUpdate = 0; console.error("Failed to load logs:", error); - textarea.value = "Error loading logs. See console for details."; + textarea.value = t("logs_load_error"); } } @@ -180,7 +186,7 @@ function deleteLogs() { if (cutoff == "none" || cutoff == "") { return; } - if (!confirm("Do you want to delete the selected logs?")) { + if (!confirm(t("logs_confirm_delete"))) { document.getElementById('deleteLogs').selectedIndex = 0; return; } @@ -209,14 +215,14 @@ function deleteLogs() { location.reload(); }) .catch(error => { - alert("Unable to delete logs: " + error); + alert(t("error_unable_delete_logs", error)); console.error('Error:', error); }); } function resetTrafficStat() { - if (!confirm("Do you want to reset the traffic statistics?")) { + if (!confirm(t("logs_confirm_reset_traffic"))) { return; } @@ -225,7 +231,7 @@ function resetTrafficStat() { location.reload(); }) .catch(error => { - alert("Unable to reset stats: " + error); + alert(t("error_unable_reset_stats", error)); console.error('Error:', error); }); } diff --git a/internal/webserver/web/static/js/admin_ui_upload.js b/internal/webserver/web/static/js/admin_ui_upload.js index 4232afc3..1aa67865 100644 --- a/internal/webserver/web/static/js/admin_ui_upload.js +++ b/internal/webserver/web/static/js/admin_ui_upload.js @@ -39,18 +39,18 @@ function initDropzone() { console.log(errorMessage); if (xhr) { if (xhr.status === 413) { - showError(file, "File too large to upload. If you are using a reverse proxy, make sure that the allowed body size is at least 70MB."); + showError(file, t("upload_error_too_large")); return; } try { console.log(xhr); errInfo = JSON.parse(xhr.responseText); - showError(file, "Error: " + errInfo.ErrorMessage); + showError(file, t("error_generic", errInfo.ErrorMessage)); } catch (ignored) { - showError(file, "Error: " + xhr.responseText); + showError(file, t("error_generic", xhr.responseText)); } } else { - showError(file, "Error: " + errorMessage); + showError(file, t("error_generic", errorMessage)); } }); this.on("uploadprogress", function(file, progress, bytesSent) { @@ -103,7 +103,7 @@ function initDropzone() { window.addEventListener('beforeunload', (event) => { if (isUploading) { - event.returnValue = 'Upload is still in progress. Do you want to close this page?'; + event.returnValue = t("upload_close_warning"); } }); } @@ -227,7 +227,7 @@ function sendChunkComplete(file, done) { done(); let progressText = document.getElementById(`us-progress-info-${file.upload.uuid}`); if (progressText != null) - progressText.innerText = "In Queue..."; + progressText.innerText = t("status_in_queue"); }) .catch(error => { console.error('Error:', error); @@ -312,26 +312,26 @@ function parseProgressStatus(eventData) { let text; switch (eventData.upload_status) { case 0: - text = "Processing file..."; + text = t("status_processing"); break; case 1: - text = "Saving file..."; + text = t("status_saving"); break; case 2: - text = "Finalising..."; + text = t("status_finalising"); requestFileInfo(eventData.file_id, eventData.chunk_id); break; case 3: - text = "Error"; + text = t("status_error"); let file = dropzoneGetFile(eventData.chunk_id); if (eventData.error_message == "") - eventData.error_message = "Server Error"; + eventData.error_message = t("status_server_error"); if (file != null) { dropzoneUploadError(file, eventData.error_message); } return; default: - text = "Unknown status"; + text = t("status_unknown"); break; } document.getElementById(`us-progress-info-${eventData.chunk_id}`).innerText = text; @@ -354,7 +354,7 @@ function editFile() { let allowedDownloads = document.getElementById('mi_edit_down').value; let expiryTimestamp = document.getElementById('mi_edit_expiry').value; let password = document.getElementById('mi_edit_pw').value; - let originalPassword = (password === '(unchanged)'); + let originalPassword = (password === t("editmodal_password_unchanged")); if (!document.getElementById('mc_download').checked) { allowedDownloads = 0; @@ -385,13 +385,13 @@ function editFile() { location.reload(); }) .catch(error => { - alert("Unable to edit file: " + error); + alert(t("error_unable_edit_file", error)); console.error('Error:', error); button.disabled = false; }); }) .catch(error => { - alert("Unable to edit file: " + error); + alert(t("error_unable_edit_file", error)); console.error('Error:', error); button.disabled = false; }); @@ -434,7 +434,7 @@ function showEditModal(filename, id, downloads, expiry, password, unlimitedown, } if (password) { - document.getElementById("mi_edit_pw").value = "(unchanged)"; + document.getElementById("mi_edit_pw").value = t("editmodal_password_unchanged"); document.getElementById("mi_edit_pw").disabled = false; document.getElementById("mc_password").checked = true; } else { @@ -455,9 +455,9 @@ function showEditModal(filename, id, downloads, expiry, password, unlimitedown, } } else { document.getElementById("mc_replace").disabled = true; - document.getElementById("mc_replace").title = "Replacing content is not available for end-to-end encrypted files"; - selectReplace.add(new Option("Unavailable", 0)); - selectReplace.title = "Replacing content is not available for end-to-end encrypted files"; + document.getElementById("mc_replace").title = t("editmodal_replace_e2e_notice"); + selectReplace.add(new Option(t("editmodal_replace_unavailable"), 0)); + selectReplace.title = t("editmodal_replace_e2e_notice"); selectReplace.value = "0"; } } else { @@ -470,7 +470,7 @@ function showEditModal(filename, id, downloads, expiry, password, unlimitedown, } function selectTextForPw(input) { - if (input.value === "(unchanged)") { + if (input.value === t("editmodal_password_unchanged")) { input.setSelectionRange(0, input.value.length); } } @@ -508,7 +508,7 @@ function deleteFile(id) { notifyWorker({ type: "fileDeleted", id: id }); }) .catch(error => { - alert("Unable to delete file: " + error); + alert(t("error_unable_delete_file", error)); console.error('Error:', error); }); } @@ -755,13 +755,13 @@ function addRow(item) { cellDownloadCount.id = "cell-downloads-" + item.Id; cellFileSize.innerText = item.Size; if (item.UnlimitedDownloads) { - cellRemainingDownloads.innerText = "Unlimited"; + cellRemainingDownloads.innerText = t("generic_unlimited"); } else { cellRemainingDownloads.innerText = item.DownloadsRemaining; cellRemainingDownloads.id = "cell-downloadsRemaining-" + item.Id; } if (item.UnlimitedTime) { - cellStoredUntil.innerText = "Unlimited"; + cellStoredUntil.innerText = t("generic_unlimited"); } else { cellStoredUntil.innerText = formatUnixTimestamp(item.ExpireAt); } @@ -779,7 +779,7 @@ function addRow(item) { if (item.IsPasswordProtected === true) { const icon = document.createElement('i'); icon.className = 'bi bi-key'; - icon.title = 'Password protected'; + icon.title = t("filetable_password_protected"); cellUrl.appendChild(document.createTextNode(' ')); cellUrl.appendChild(icon); } @@ -815,12 +815,12 @@ function createButtonGroup(item) { copyUrlBtn.className = 'copyurl btn btn-outline-light btn-sm'; copyUrlBtn.dataset.clipboardText = item.UrlDownload; copyUrlBtn.id = 'url-button-' + item.Id; - copyUrlBtn.title = 'Copy URL'; + copyUrlBtn.title = t("btn_copy_url"); const copyIcon = document.createElement('i'); copyIcon.className = 'bi bi-copy'; copyUrlBtn.appendChild(copyIcon); - copyUrlBtn.appendChild(document.createTextNode(' URL')); + copyUrlBtn.appendChild(document.createTextNode(' ' + t("btn_url"))); copyUrlBtn.addEventListener('click', () => { showToast(1000); @@ -844,14 +844,14 @@ function createButtonGroup(item) { const aDr1 = document.createElement("a"); if (item.UrlHotlink !== "") { aDr1.className = "dropdown-item copyurl"; - aDr1.title = "Copy hotlink"; + aDr1.title = t("btn_copy_hotlink"); aDr1.style.cursor = "pointer"; aDr1.setAttribute("data-clipboard-text", item.UrlHotlink); aDr1.onclick = () => showToast(1000); - aDr1.innerHTML = ` Hotlink`; + aDr1.innerHTML = ' ' + t("btn_hotlink"); } else { aDr1.className = "dropdown-item"; - aDr1.innerText = "Hotlink not available"; + aDr1.innerText = t("btn_hotlink_unavailable"); } liDr1.appendChild(aDr1); dropdown1.appendChild(liDr1); @@ -861,7 +861,7 @@ function createButtonGroup(item) { const btnShare = document.createElement("button"); btnShare.type = "button"; btnShare.className = "btn btn-outline-light btn-sm"; - btnShare.title = "Share"; + btnShare.title = t("btn_share"); btnShare.onclick = () => shareUrl(event, item.Id); // For some reason bi-share does not always show up, using the svg fixes it btnShare.innerHTML = ` @@ -889,20 +889,20 @@ function createButtonGroup(item) { qrA.className = "dropdown-item"; qrA.id = `qrcode-${item.Id}`; qrA.style.cursor = "pointer"; - qrA.title = "Open QR Code"; + qrA.title = t("btn_open_qr_code"); qrA.onclick = () => showQrCode(item.UrlDownload); - qrA.innerHTML = ` QR Code`; + qrA.innerHTML = ' ' + t("btn_qr_code"); qrLi.appendChild(qrA); dropdown2.appendChild(qrLi); const emailLi = document.createElement("li"); const emailA = document.createElement("a"); emailA.className = "dropdown-item"; - emailA.title = "Share via email"; + emailA.title = t("btn_share_email"); emailA.id = `email-${item.Id}`; emailA.target = "_blank"; emailA.href = `mailto:?body=${encodeURIComponent(item.UrlDownload)}`; - emailA.innerHTML = ` Email`; + emailA.innerHTML = ' ' + t("btn_email"); emailLi.appendChild(emailA); dropdown2.appendChild(emailLi); group1.appendChild(dropdown2); @@ -917,7 +917,7 @@ function createButtonGroup(item) { const btnDownload = document.createElement('button'); btnDownload.type = 'button'; btnDownload.className = 'btn btn-outline-light btn-sm'; - btnDownload.title = 'Download'; + btnDownload.title = t("btn_download"); if (item.RequiresClientSideDecryption) { btnDownload.classList.add("disabled"); } @@ -936,7 +936,7 @@ function createButtonGroup(item) { const btnEdit = document.createElement('button'); btnEdit.type = 'button'; btnEdit.className = 'btn btn-outline-light btn-sm'; - btnEdit.title = 'Edit'; + btnEdit.title = t("btn_edit"); const editIcon = document.createElement('i'); editIcon.className = 'bi bi-pencil'; @@ -962,7 +962,7 @@ function createButtonGroup(item) { const btnDelete = document.createElement('button'); btnDelete.type = 'button'; btnDelete.className = 'btn btn-outline-danger btn-sm'; - btnDelete.title = 'Delete'; + btnDelete.title = t("btn_delete"); btnDelete.id = 'button-delete-' + item.Id; const deleteIcon = document.createElement('i'); @@ -1003,9 +1003,9 @@ function changeRowCount(add, row) { let infoEmpty = document.getElementsByClassName("dataTables_empty")[0]; if (typeof infoEmpty !== "undefined") { - infoEmpty.innerText = "Files stored: " + rowCount; + infoEmpty.innerText = t("filetable_files_stored", rowCount); } else { - document.getElementsByClassName("dataTables_info")[0].innerText = "Files stored: " + rowCount; + document.getElementsByClassName("dataTables_info")[0].innerText = t("filetable_files_stored", rowCount); } } @@ -1063,7 +1063,7 @@ function handleUndo(button) { } }) .catch(error => { - alert("Unable to restore file: " + error); + alert(t("error_unable_restore_file", error)); console.error('Error:', error); }); } diff --git a/internal/webserver/web/static/js/admin_ui_users.js b/internal/webserver/web/static/js/admin_ui_users.js index bda3c39a..8a4bcdc4 100644 --- a/internal/webserver/web/static/js/admin_ui_users.js +++ b/internal/webserver/web/static/js/admin_ui_users.js @@ -22,14 +22,14 @@ function changeUserPermission(userId, permission, buttonId) { if (permission == "PERM_REPLACE_OTHER" && !wasGranted) { hasNotPermissionReplace = document.getElementById("perm_replace_" + userId).classList.contains("perm-notgranted"); if (hasNotPermissionReplace) { - showToast(2000, "Also granting permission to replace own files"); + showToast(2000, t("toast_also_granting_replace")); changeUserPermission(userId, "PERM_REPLACE", "perm_replace_" + userId); } } if (permission == "PERM_REPLACE" && wasGranted) { hasPermissionReplaceOthers = document.getElementById("perm_replace_other_" + userId).classList.contains("perm-granted"); if (hasPermissionReplaceOthers) { - showToast(2000, "Also revoking permission to replace files of other users"); + showToast(2000, t("toast_also_revoking_replace")); changeUserPermission(userId, "PERM_REPLACE_OTHER", "perm_replace_other_" + userId); } } @@ -51,7 +51,7 @@ function changeUserPermission(userId, permission, buttonId) { indicator.classList.add("perm-notgranted"); } indicator.classList.remove("perm-processing"); - alert("Unable to set permission: " + error); + alert(t("error_unable_set_permission", error)); console.error('Error:', error); }); } @@ -72,7 +72,7 @@ function changeRank(userId, newRank, buttonId) { }) .catch(error => { indicator.disabled = false; - alert("Unable to change rank: " + error); + alert(t("error_unable_change_rank", error)); console.error('Error:', error); }); } @@ -95,7 +95,7 @@ function showDeleteUserModal(userId, userEmail) { }, 290); }) .catch(error => { - alert("Unable to delete user: " + error); + alert(t("error_unable_delete_user", error)); console.error('Error:', error); }); }; @@ -138,7 +138,7 @@ function resetPw(userid, newPw) { .then(data => { if (!newPw) { $('#resetPasswordModal').modal('hide'); - showToast(1000, 'Password change requirement set successfully') + showToast(1000, t("toast_pw_change_required_set")) return; } button.style.display = 'none'; @@ -150,11 +150,11 @@ function resetPw(userid, newPw) { document.getElementById("copypwclip").onclick = function() { // For some reason ClipboardJs is not working on the user PW reset modal, even when initilising again. Manually writing to clipboard navigator.clipboard.writeText(data.password); - showToast(1000, "Password copied to clipboard"); + showToast(1000, t("toast_password_copied")); } }) .catch(error => { - alert("Unable to reset user password: " + error); + alert(t("error_unable_reset_password", error)); console.error('Error:', error); button.disabled = false; }); @@ -178,10 +178,10 @@ function addNewUser() { }) .catch(error => { if (error.message == "duplicate") { - alert("A user already exists with that name"); + alert(t("error_user_exists")); button.disabled = false; } else { - alert("Unable to create user: " + error); + alert(t("error_unable_create_user", error)); console.error('Error:', error); button.disabled = false; } @@ -196,7 +196,7 @@ const PermissionDefinitions = [ key: "UserPermGuestUploads", bit: 1 << 8, icon: "bi bi-box-arrow-in-down", - title: "Create file requests", + title: t("userperm_file_requests"), htmlId: userid => `perm_guest_upload_${userid}`, apiName: "PERM_GUEST_UPLOAD" }, @@ -204,7 +204,7 @@ const PermissionDefinitions = [ key: "UserPermReplaceUploads", bit: 1 << 0, icon: "bi bi-recycle", - title: "Replace own uploads", + title: t("userperm_replace_own"), htmlId: userid => `perm_replace_${userid}`, apiName: "PERM_REPLACE" }, @@ -212,7 +212,7 @@ const PermissionDefinitions = [ key: "UserPermListOtherUploads", bit: 1 << 1, icon: "bi bi-eye", - title: "List other uploads", + title: t("userperm_list_other"), htmlId: userid => `perm_list_${userid}`, apiName: "PERM_LIST" }, @@ -220,7 +220,7 @@ const PermissionDefinitions = [ key: "UserPermEditOtherUploads", bit: 1 << 2, icon: "bi bi-pencil", - title: "Edit other uploads", + title: t("userperm_edit_other"), htmlId: userid => `perm_edit_${userid}`, apiName: "PERM_EDIT" }, @@ -228,7 +228,7 @@ const PermissionDefinitions = [ key: "UserPermDeleteOtherUploads", bit: 1 << 4, icon: "bi bi-trash3", - title: "Delete other uploads", + title: t("userperm_delete_other"), htmlId: userid => `perm_delete_${userid}`, apiName: "PERM_DELETE" }, @@ -236,7 +236,7 @@ const PermissionDefinitions = [ key: "UserPermReplaceOtherUploads", bit: 1 << 3, icon: "bi bi-arrow-left-right", - title: "Replace other uploads", + title: t("userperm_replace_other"), htmlId: userid => `perm_replace_other_${userid}`, apiName: "PERM_REPLACE_OTHER" }, @@ -244,7 +244,7 @@ const PermissionDefinitions = [ key: "UserPermManageLogs", bit: 1 << 5, icon: "bi bi-card-list", - title: "Manage system logs", + title: t("userperm_logs"), htmlId: userid => `perm_logs_${userid}`, apiName: "PERM_LOGS" }, @@ -252,7 +252,7 @@ const PermissionDefinitions = [ key: "UserPermManageUsers", bit: 1 << 7, icon: "bi bi-people", - title: "Manage users", + title: t("userperm_users"), htmlId: userid => `perm_users_${userid}`, apiName: "PERM_USERS" }, @@ -260,7 +260,7 @@ const PermissionDefinitions = [ key: "UserPermManageApiKeys", bit: 1 << 6, icon: "bi bi-sliders2", - title: "Manage all API keys", + title: t("userperm_api"), htmlId: userid => `perm_api_${userid}`, apiName: "PERM_API" } @@ -294,8 +294,8 @@ function addRowUser(userid, name, permissions) { cellName.innerText = name; - cellGroup.innerText = "User"; - cellLastOnline.innerText = "Never"; + cellGroup.innerText = t("userlevel_user"); + cellLastOnline.innerText = t("generic_never"); cellUploads.innerText = "0"; // Create one button group @@ -309,7 +309,7 @@ function addRowUser(userid, name, permissions) { btnResetPw.id = `pwchange-${userid}`; btnResetPw.type = "button"; btnResetPw.className = "btn btn-outline-light btn-sm"; - btnResetPw.title = "Reset Password"; + btnResetPw.title = t("users_reset_password"); btnResetPw.onclick = () => showResetPwModal(userid, name); btnResetPw.innerHTML = ``; btnGroup.appendChild(btnResetPw); @@ -320,7 +320,7 @@ function addRowUser(userid, name, permissions) { btnPromote.id = `changeRank_${userid}`; btnPromote.type = "button"; btnPromote.className = "btn btn-outline-light btn-sm"; - btnPromote.title = "Promote User"; + btnPromote.title = t("users_promote"); if (isAdmin) { btnPromote.onclick = () => changeRank(userid, 'ADMIN', `changeRank_${userid}`); } else { @@ -334,7 +334,7 @@ function addRowUser(userid, name, permissions) { btnDelete.id = `delete-${userid}`; btnDelete.type = "button"; btnDelete.className = "btn btn-outline-danger btn-sm"; - btnDelete.title = "Delete"; + btnDelete.title = t("btn_delete"); btnDelete.onclick = () => showDeleteUserModal(userid, name); btnDelete.innerHTML = ``; btnGroup.appendChild(btnDelete); @@ -379,7 +379,7 @@ function addRowUser(userid, name, permissions) { function sanitizeUserId(id) { const numericId = id.toString().trim(); if (!/^\d+$/.test(numericId)) { - throw new Error("Invalid ID: must contain only digits."); + throw new Error(t("error_invalid_id")); } return numericId; } diff --git a/internal/webserver/web/static/js/all_public.js b/internal/webserver/web/static/js/all_public.js index 74d5480a..48e4e904 100644 --- a/internal/webserver/web/static/js/all_public.js +++ b/internal/webserver/web/static/js/all_public.js @@ -1,3 +1,41 @@ +/** + * t returns the translated string for the given key. + * + * The translations are provided by the server as the global variable + * gokapiTranslations, which is defined in the header template. Any additional + * arguments replace the placeholders {0}, {1}, ... in the string. If the key is + * unknown, the key itself is returned, so that a missing translation is easy to + * spot. + * + * @param {string} key + * @param {...*} args + * @returns {string} + */ +function t(key, ...args) { + let result = key; + if (typeof gokapiTranslations !== "undefined" && gokapiTranslations !== null && + Object.prototype.hasOwnProperty.call(gokapiTranslations, key)) { + result = gokapiTranslations[key]; + } + for (let i = 0; i < args.length; i++) { + result = result.split("{" + i + "}").join(args[i]); + } + return result; +} + +/** + * setLanguage stores the language selected by the user in a cookie and reloads + * the page, so that the server renders it in the new language. No path is set, + * which means that the cookie is scoped to the directory Gokapi is hosted in. + * + * @param {string} code ISO 639-1 language code + */ +function setLanguage(code) { + document.cookie = "gokapi_language=" + encodeURIComponent(code) + + ";max-age=31536000;samesite=lax"; + window.location.reload(); +} + function getUuid() { // Native UUID, not available in insecure environment if (typeof crypto !== "undefined" && crypto.randomUUID) { @@ -53,7 +91,7 @@ function formatUnixTimestamp(unixTimestamp) { function formatTimestampWithNegative(unixTimestamp, negative) { if (negative === undefined) { - negative = "Never"; + negative = t("generic_never"); } if (unixTimestamp == 0) { return negative; @@ -71,7 +109,7 @@ function insertDateWithNegative(unixTimestamp, id, negative) { function insertLastOnlineDate(unixTimestamp, id) { if ((Date.now() / 1000) - 120 < unixTimestamp) { - document.getElementById(id).innerText = "Online"; + document.getElementById(id).innerText = t("generic_online"); return; } insertDateWithNegative(unixTimestamp, id); @@ -79,10 +117,10 @@ function insertLastOnlineDate(unixTimestamp, id) { function formatFileRequestExpiry(unixTimestamp) { if (unixTimestamp == 0) { - return "Never"; + return t("generic_never"); } if ((Date.now() / 1000) > unixTimestamp) { - return "Expired"; + return t("generic_expired"); } return formatUnixTimestamp(unixTimestamp); } diff --git a/internal/webserver/web/static/js/end2end_admin.js b/internal/webserver/web/static/js/end2end_admin.js index 3040fc02..0b4bcb7d 100644 --- a/internal/webserver/web/static/js/end2end_admin.js +++ b/internal/webserver/web/static/js/end2end_admin.js @@ -4,7 +4,7 @@ function displayError(err) { errorMessageEl.innerText = ""; const bold = document.createElement("b"); - bold.innerText = "Error: "; + bold.innerText = t("error_prefix"); const message = document.createTextNode(err.toString().replace(/^Error:/gi, "")); errorMessageEl.appendChild(bold); @@ -26,8 +26,8 @@ function checkIfE2EKeyIsSet() { } getE2EInfo(); dropzoneObject.enable(); - document.getElementById("uploadBoxTitle").innerText = "Drag & drop files here"; - document.getElementById("uploadBoxSubtitle").innerText = "or paste or click to upload with end-to-end encryption"; + document.getElementById("uploadBoxTitle").innerText = t("upload_dragdrop"); + document.getElementById("uploadBoxSubtitle").innerText = t("upload_dragdrop_sub_e2e"); }); } } @@ -44,12 +44,12 @@ function getE2EInfo() { if (err.message === "cipher: message authentication failed") { invalidCipherRedirectConfim(); } else { - displayError("Trying to get E2E info: " + data); + displayError(t("e2e_error_get_info", data)); } } }) .catch(error => { - displayError("Trying to get E2E info: " + error); + displayError(t("e2e_error_get_info", error)); }) .finally(() => { apiE2eMutexLockUnlock(true).catch(error => { @@ -62,13 +62,13 @@ function getE2EInfo() { function storeE2EInfo(data) { apiE2eStore(data) .catch(error => { - displayError("Trying to store E2E info: " + error); + displayError(t("e2e_error_store_info", error)); }); } function invalidCipherRedirectConfim() { - if (confirm('It appears that an invalid end-to-end encryption key has been entered. Would you like to enter the correct one?')) { + if (confirm(t("e2e_invalid_key_confirm"))) { window.location = './e2eSetup'; } } diff --git a/internal/webserver/web/static/js/min/admin.min.ccb62a8d5b.js b/internal/webserver/web/static/js/min/admin.min.ccb62a8d5b.js index 9f4f99c8..6e4f1967 100644 --- a/internal/webserver/web/static/js/min/admin.min.ccb62a8d5b.js +++ b/internal/webserver/web/static/js/min/admin.min.ccb62a8d5b.js @@ -1,10 +1,10 @@ -const storedTokens=new Map;async function getToken(e,t){const n="./auth/token";if(!t){if(!storedTokens.has(e))return getToken(e,!0);let t=storedTokens.get(e);return t.expiry-Date.now()/1e3<60?getToken(e,!0):t.key}const s={method:"POST",headers:{"Content-Type":"application/json",permission:e}};try{const o=await fetch(n,s);if(!o.ok)throw new Error(`Request failed with status: ${o.status}`);const t=await o.json();if(!t.hasOwnProperty("key"))throw new Error(`Invalid response when trying to get token`);return storedTokens.set(e,{key:t.key,expiry:t.expiry}),t.key}catch(e){throw console.error("Error in getToken:",e),e}}async function apiAuthModify(e,t,n){const o="./api/auth/modify",i="PERM_API_MOD";let s;try{s=await getToken(i,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const a={method:"POST",headers:{"Content-Type":"application/json",apikey:s,targetKey:e,permission:t,permissionModifier:n}};try{const e=await fetch(o,a);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`)}catch(e){throw console.error("Error in apiAuthModify:",e),e}}async function apiAuthFriendlyName(e,t){const s="./api/auth/friendlyname",o="PERM_API_MOD";let n;try{n=await getToken(o,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const i={method:"PUT",headers:{"Content-Type":"application/json",apikey:n,targetKey:e,friendlyName:t}};try{const e=await fetch(s,i);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`)}catch(e){throw console.error("Error in apiAuthModify:",e),e}}async function apiAuthDelete(e){const n="./api/auth/delete",s="PERM_API_MOD";let t;try{t=await getToken(s,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const o={method:"POST",headers:{"Content-Type":"application/json",apikey:t,targetKey:e}};try{const e=await fetch(n,o);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`)}catch(e){throw console.error("Error in apiAuthDelete:",e),e}}async function apiAuthCreate(){const t="./api/auth/create",n="PERM_API_MOD";let e;try{e=await getToken(n,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const s={method:"POST",headers:{"Content-Type":"application/json",apikey:e,basicPermissions:"true"}};try{const e=await fetch(t,s);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`);const n=await e.json();return n}catch(e){throw console.error("Error in apiAuthCreate:",e),e}}async function apiChunkComplete(e,t,n,s,o,i,a,r,c,l){const u="./api/chunk/complete",h="PERM_UPLOAD";let d;try{d=await getToken(h,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const m={method:"POST",headers:{"Content-Type":"application/json",apikey:d,uuid:e,filename:"base64:"+Base64.encode(t),filesize:n,realsize:s,contenttype:o,allowedDownloads:i,expiryDays:a,password:r,isE2E:c,nonblocking:l}};try{const e=await fetch(u,m);if(!e.ok){let t;try{const n=await e.json();t=n.ErrorMessage||`Request failed with status: ${e.status}`}catch{const n=await e.text();t=n||`Request failed with status: ${e.status}`}throw new Error(t)}const t=await e.json();return t}catch(e){throw console.error("Error in apiChunkComplete:",e),e}}async function apiFilesReplace(e,t){const s="./api/files/replace",o="PERM_REPLACE";let n;try{n=await getToken(o,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const i={method:"PUT",headers:{"Content-Type":"application/json",id:e,apikey:n,idNewContent:t,deleteNewFile:!1}};try{const e=await fetch(s,i);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`);const t=await e.json();return t}catch(e){throw console.error("Error in apiFilesReplace:",e),e}}async function apiFilesListById(e){const n="./api/files/list/"+e,s="PERM_VIEW";let t;try{t=await getToken(s,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const o={method:"GET",headers:{"Content-Type":"application/json",apikey:t}};try{const e=await fetch(n,o);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`);const t=await e.json();return t}catch(e){throw console.error("Error in apiFilesListById:",e),e}}async function apiFilesListDownloadSingle(e){const n="./api/files/download/"+e,s="PERM_DOWNLOAD";let t;try{t=await getToken(s,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const o={method:"GET",headers:{"Content-Type":"application/json",apikey:t,presignUrl:!0}};try{const e=await fetch(n,o);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`);const t=await e.json();return t}catch(e){throw console.error("Error in apiFilesListDownloadSingle:",e),e}}async function apiFilesListDownloadZip(e,t){const s="./api/files/downloadzip",o="PERM_DOWNLOAD";let n;try{n=await getToken(o,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const i={method:"GET",headers:{"Content-Type":"application/json",apikey:n,ids:e,filename:"base64:"+Base64.encode(t),presignUrl:!0}};try{const e=await fetch(s,i);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`);const t=await e.json();return t}catch(e){throw console.error("Error in apiFilesListDownloadZip:",e),e}}async function apiFilesModify(e,t,n,s,o){const a="./api/files/modify",r="PERM_EDIT";let i;try{i=await getToken(r,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const c={method:"PUT",headers:{"Content-Type":"application/json",id:e,apikey:i,allowedDownloads:t,expiryTimestamp:n,password:s,originalPassword:o}};try{const e=await fetch(a,c);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`);const t=await e.json();return t}catch(e){throw console.error("Error in apiFilesModify:",e),e}}async function apiFilesDelete(e,t){const s="./api/files/delete",o="PERM_DELETE";let n;try{n=await getToken(o,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const i={method:"POST",headers:{"Content-Type":"application/json",apikey:n,id:e,delay:t}};try{const e=await fetch(s,i);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`)}catch(e){throw console.error("Error in apiFilesDelete:",e),e}}async function apiFilesRestore(e){const n="./api/files/restore",s="PERM_DELETE";let t;try{t=await getToken(s,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const o={method:"POST",headers:{"Content-Type":"application/json",apikey:t,id:e}};try{const e=await fetch(n,o);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`);const t=await e.json();return t}catch(e){throw console.error("Error in apiFilesRestore:",e),e}}async function apiUserCreate(e){const n="./api/user/create",s="PERM_MANAGE_USERS";let t;try{t=await getToken(s,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const o={method:"POST",headers:{"Content-Type":"application/json",apikey:t,username:e}};try{const e=await fetch(n,o);if(!e.ok)throw e.status==409?new Error("duplicate"):new Error(`Request failed with status: ${e.status}`);const t=await e.json();return t}catch(e){throw console.error("Error in apiUserModify:",e),e}}async function apiUserModify(e,t,n){const o="./api/user/modify",i="PERM_MANAGE_USERS";let s;try{s=await getToken(i,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const a={method:"POST",headers:{"Content-Type":"application/json",apikey:s,userid:e,userpermission:t,permissionModifier:n}};try{const e=await fetch(o,a);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`)}catch(e){throw console.error("Error in apiUserModify:",e),e}}async function apiUserChangeRank(e,t){const s="./api/user/changeRank",o="PERM_MANAGE_USERS";let n;try{n=await getToken(o,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const i={method:"POST",headers:{"Content-Type":"application/json",apikey:n,userid:e,newRank:t}};try{const e=await fetch(s,i);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`)}catch(e){throw console.error("Error in apiUserModify:",e),e}}async function apiUserDelete(e,t){const s="./api/user/delete",o="PERM_MANAGE_USERS";let n;try{n=await getToken(o,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const i={method:"POST",headers:{"Content-Type":"application/json",apikey:n,userid:e,deleteFiles:t}};try{const e=await fetch(s,i);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`)}catch(e){throw console.error("Error in apiUserDelete:",e),e}}async function apiUserResetPassword(e,t){const s="./api/user/resetPassword",o="PERM_MANAGE_USERS";let n;try{n=await getToken(o,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const i={method:"POST",headers:{"Content-Type":"application/json",apikey:n,userid:e,generateNewPassword:t}};try{const e=await fetch(s,i);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`);const t=await e.json();return t}catch(e){throw console.error("Error in apiUserResetPassword:",e),e}}async function apiLogSystemStatus(){const t="./api/logs/systemStatus",n="PERM_MANAGE_LOGS";let e;try{e=await getToken(n,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const s={method:"GET",headers:{"Content-Type":"application/json",apikey:e}};try{const e=await fetch(t,s);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`);const n=await e.json();return n}catch(e){throw console.error("Error in apiLogSystemStatus:",e),e}}async function apiLogResetTraffic(){const t="./api/logs/resetTraffic",n="PERM_MANAGE_LOGS";let e;try{e=await getToken(n,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const s={method:"GET",headers:{"Content-Type":"application/json",apikey:e}};try{const e=await fetch(t,s);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`)}catch(e){throw console.error("Error in apiLogResetTraffic:",e),e}}async function apiLogGet(e){const n="./api/logs/get",s="PERM_MANAGE_LOGS";let t;try{t=await getToken(s,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const o={method:"GET",headers:{"Content-Type":"application/json",apikey:t,timestamp:e}};try{const e=await fetch(n,o);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`);const t=await e.json();return t}catch(e){throw console.error("Error in apiLogGet:",e),e}}async function apiLogsDelete(e){const n="./api/logs/delete",s="PERM_MANAGE_LOGS";let t;try{t=await getToken(s,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const o={method:"POST",headers:{"Content-Type":"application/json",apikey:t,timestamp:e}};try{const e=await fetch(n,o);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`)}catch(e){throw console.error("Error in apiLogsDelete:",e),e}}async function apiE2eGet(){const t="./api/e2e/get",n="PERM_UPLOAD";let e;try{e=await getToken(n,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const s={method:"POST",headers:{"Content-Type":"application/json",apikey:e}};try{const e=await fetch(t,s);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`);return await e.text()}catch(e){throw console.error("Error in apiE2eGet:",e),e}}async function apiE2eMutexLockUnlock(e){let t="./api/e2e/mutex/lock";e&&(t="./api/e2e/mutex/unlock");const s="PERM_UPLOAD";let n;try{n=await getToken(s,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const o={method:"GET",headers:{"Content-Type":"application/json",apikey:n}};try{const e=await fetch(t,o);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`);return await e.text()}catch(e){throw console.error("Error in apiE2eMutexLock:",e),e}}async function apiE2eStore(e){const n="./api/e2e/set",s="PERM_UPLOAD";let t;try{t=await getToken(s,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const o={method:"POST",headers:{"Content-Type":"application/json",apikey:t},body:JSON.stringify({content:e})};try{const e=await fetch(n,o);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`)}catch(e){throw console.error("Error in apiE2eStore:",e),e}}async function apiURequestDelete(e){const n="./api/uploadrequest/delete",s="PERM_MANAGE_FILE_REQUESTS";let t;try{t=await getToken(s,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const o={method:"DELETE",headers:{"Content-Type":"application/json",apikey:t,id:e}};try{const e=await fetch(n,o);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`)}catch(e){throw console.error("Error in apiURequestDelete:",e),e}}async function apiURequestSave(e,t,n,s,o,i){const r="./api/uploadrequest/save",c="PERM_MANAGE_FILE_REQUESTS";let a;try{a=await getToken(c,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const l={method:"POST",headers:{"Content-Type":"application/json",apikey:a,id:e,name:"base64:"+Base64.encode(t),expiry:o,maxfiles:n,maxsize:s,notes:"base64:"+Base64.encode(i)}};try{const e=await fetch(r,l);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`);const t=await e.json();return t}catch(e){throw console.error("Error in apiURequestDelete:",e),e}}try{var toastId,calendarInstance,dropzoneObject,isE2EEnabled,isUploading,rowCount,sseWorkerPort,statusItemCount,clipboard=new ClipboardJS(".copyurl")}catch{}function showToast(e,t){let n=document.getElementById("toastnotification");typeof t!="undefined"?n.innerText=t:n.innerText=n.dataset.default,n.classList.add("show"),clearTimeout(toastId),toastId=setTimeout(()=>{hideToast()},e)}function hideToast(){document.getElementById("toastnotification").classList.remove("show")}calendarInstance=null;function createCalendar(e,t){const n=new Date(t*1e3);calendarInstance=flatpickr(document.getElementById(e),{enableTime:!0,dateFormat:"U",altInput:!0,altFormat:"Y-m-d H:i",allowInput:!0,time_24hr:!0,defaultDate:n,minDate:"today"})}function handleEditCheckboxChange(e){var t=document.getElementById(e.getAttribute("data-toggle-target")),n=e.getAttribute("data-timestamp");e.checked?(t.classList.remove("disabled"),t.removeAttribute("disabled"),n!=null&&(calendarInstance._input.disabled=!1)):(n!=null&&(calendarInstance._input.disabled=!0),t.classList.add("disabled"),t.setAttribute("disabled",!0))}function downloadFileWithPresign(e){apiFilesListDownloadSingle(e).then(e=>{if(!e.hasOwnProperty("downloadUrl"))throw new Error("Unable to get presigned key");const t=document.createElement("a");t.href=e.downloadUrl,t.style.display="none",document.body.appendChild(t),t.click(),t.remove()}).catch(e=>{alert("Unable to download: "+e),console.error("Error:",e)})}function downloadFilesZipWithPresign(e,t){apiFilesListDownloadZip(e,t).then(e=>{if(!e.hasOwnProperty("downloadUrl"))throw new Error("Unable to get presigned key");const t=document.createElement("a");t.href=e.downloadUrl,t.style.display="none",document.body.appendChild(t),t.click(),t.remove()}).catch(e=>{alert("Unable to download: "+e),console.error("Error:",e)})}function doLogout(){typeof sseWorkerPort!="undefined"&&sseWorkerPort!==null&&sseWorkerPort.postMessage({type:"shutdown"}),window.location.href="./logout"}function changeApiPermission(e,t,n){var o,i,s=document.getElementById(n);if(s.classList.contains("perm-processing")||s.classList.contains("perm-nochange"))return;o=s.classList.contains("perm-granted"),s.classList.add("perm-processing"),s.classList.remove("perm-granted"),s.classList.remove("perm-notgranted"),i="GRANT",o&&(i="REVOKE"),apiAuthModify(e,t,i).then(e=>{o?(s.classList.add("perm-notgranted"),s.classList.add("perm-nownotgranted")):(s.classList.add("perm-granted"),s.classList.add("perm-nowgranted")),s.classList.remove("perm-processing"),setTimeout(()=>{s.classList.remove("perm-nowgranted"),s.classList.remove("perm-nownotgranted")},1e3)}).catch(e=>{o?s.classList.add("perm-granted"):s.classList.add("perm-notgranted"),s.classList.remove("perm-processing"),alert("Unable to set permission: "+e),console.error("Error:",e)})}function deleteApiKey(e){document.getElementById("delete-"+e).disabled=!0,apiAuthDelete(e).then(t=>{document.getElementById("row-"+e).classList.add("rowDeleting"),setTimeout(()=>{document.getElementById("row-"+e).remove()},290)}).catch(e=>{alert("Unable to delete API key: "+e),console.error("Error:",e)})}function newApiKey(){document.getElementById("button-newapi").disabled=!0,apiAuthCreate().then(e=>{addRowApi(e.Id,e.PublicId),document.getElementById("button-newapi").disabled=!1}).catch(e=>{alert("Unable to create API key: "+e),console.error("Error:",e)})}function addFriendlyNameChange(e){let t=document.getElementById("friendlyname-"+e);if(t.classList.contains("isBeingEdited"))return;t.classList.add("isBeingEdited");let i=t.innerText,n=document.createElement("input");n.size=5,n.value=i;let s=!0,o=function(){if(!s)return;s=!1;let o=n.value;o==""&&(o="Unnamed key"),t.innerText=o,t.classList.remove("isBeingEdited"),apiAuthFriendlyName(e,o).catch(e=>{alert("Unable to save name: "+e),console.error("Error:",e)})};n.onblur=o,n.addEventListener("keyup",function(e){e.keyCode===13&&(e.preventDefault(),o())}),t.innerText="",t.appendChild(n),n.focus()}function addRowApi(e,t){let p=document.getElementById("apitable"),s=p.insertRow(0);s.id="row-"+t;let i=0,c=s.insertCell(i++),l=s.insertCell(i++),d=s.insertCell(i++),a=s.insertCell(i++),u;canViewOtherApiKeys&&(u=s.insertCell(i++));let h=s.insertCell(i++);canViewOtherApiKeys&&(u.classList.add("newApiKey"),u.innerText=userName),c.classList.add("newApiKey"),l.classList.add("newApiKey"),d.classList.add("newApiKey"),a.classList.add("newApiKey"),a.classList.add("prevent-select"),h.classList.add("newApiKey"),c.innerText="Unnamed key",c.id="friendlyname-"+t,c.onclick=function(){addFriendlyNameChange(t)},l.innerText=e,l.classList.add("font-monospace"),d.innerText="Never";const r=document.createElement("div");r.className="btn-group",r.setAttribute("role","group");const n=document.createElement("button");n.type="button",n.dataset.clipboardText=e,n.title="Copy API Key",n.className="copyurl btn btn-outline-light btn-sm",n.setAttribute("onclick","showToast(1000)");const m=document.createElement("i");m.className="bi bi-copy",n.appendChild(m);const o=document.createElement("button");o.type="button",o.id=`delete-${t}`,o.title="Delete",o.className="btn btn-outline-danger btn-sm",o.setAttribute("onclick",`deleteApiKey('${t}')`);const f=document.createElement("i");f.className="bi bi-trash3",o.appendChild(f),r.appendChild(n),r.appendChild(o),h.appendChild(r);const g=[{perm:"PERM_VIEW",icon:"bi-eye",granted:!0,title:"List Uploads"},{perm:"PERM_UPLOAD",icon:"bi-file-earmark-plus",granted:!0,title:"Upload"},{perm:"PERM_EDIT",icon:"bi-pencil",granted:!0,title:"Edit Uploads"},{perm:"PERM_DELETE",icon:"bi-trash3",granted:!0,title:"Delete Uploads"},{perm:"PERM_REPLACE",icon:"bi-recycle",granted:!1,title:"Replace Uploads"},{perm:"PERM_DOWNLOAD",icon:"bi-box-arrow-in-down",granted:!1,title:"Download Files"},{perm:"PERM_MANAGE_FILE_REQUESTS",icon:"bi-file-earmark-arrow-up",granted:!1,title:"Manage File Requests"},{perm:"PERM_MANAGE_USERS",icon:"bi-people",granted:!1,title:"Manage Users"},{perm:"PERM_MANAGE_LOGS",icon:"bi-card-list",granted:!1,title:"Manage System Logs"},{perm:"PERM_API_MOD",icon:"bi-sliders2",granted:!1,title:"Manage API Keys"}];if(g.forEach(({perm:e,icon:n,granted:s,title:o})=>{const i=document.createElement("i"),r=`${e.toLowerCase()}_${t}`;i.id=r,i.className=`bi ${n} ${s?"perm-granted":"perm-notgranted"}`,i.title=o,i.setAttribute("onclick",`changeApiPermission("${t}","${e}", "${r}");`),a.appendChild(i),a.appendChild(document.createTextNode(" "))}),!canReplaceFiles){let e=document.getElementById("perm_replace_"+t);e.classList.add("perm-unavailable"),e.classList.add("perm-nochange")}if(!canManageUsers){let e=document.getElementById("perm_manage_users_"+t);e.classList.add("perm-unavailable"),e.classList.add("perm-nochange")}if(!canViewSystemLog){let e=document.getElementById("perm_manage_logs_"+t);e.classList.add("perm-unavailable"),e.classList.add("perm-nochange")}if(!canCreateFileRequest){let e=document.getElementById("perm_manage_file_requests_"+t);e.classList.add("perm-unavailable"),e.classList.add("perm-nochange")}setTimeout(()=>{c.classList.remove("newApiKey"),l.classList.remove("newApiKey"),d.classList.remove("newApiKey"),a.classList.remove("newApiKey"),h.classList.remove("newApiKey")},700)}function deleteFileRequest(e){document.getElementById("delete-"+e).disabled=!0,apiURequestDelete(e).then(t=>{const s=document.getElementById("row-"+e),n=document.getElementById("filelist-"+e);s.classList.add("rowDeleting"),n!==null&&n.classList.add("rowDeleting"),setTimeout(()=>{s.remove(),n!==null&&n.remove()},290)}).catch(e=>{alert("Unable to delete file request: "+e),console.error("Error:",e)})}function deleteOrShowModal(e,t,n){n===0?deleteFileRequest(e):showDeleteFRequestModal(e,t,n)}function deleteFileFr(e,t){document.getElementById("button-delete-"+e).disabled=!0;let n=document.getElementById("cell-listupload-"+e);apiFilesDelete(e,10).then(s=>{changeFileCountFr(t,-1),removeDownloadFileReference(e,t),n.classList.add("rowDeleting"),setTimeout(()=>{n.remove()},290),showToastFileDeletionFr(e)}).catch(e=>{alert("Unable to delete file: "+e),console.error("Error:",e)})}function changeFileCountFr(e,t){let n=document.getElementById("totalFiles-fr-"+e),s=Number(n.innerText)||0,o=s+t;n.innerText=o}function removeDownloadFileReference(e,t){const n=document.getElementById(`download-${t}`);if(!n)return;const a=n.getAttribute("onclick")||"",o=a.match(/downloadFilesZipWithPresign\('([^']*)',\s*'([^']*)'\)/),r=a.match(/downloadFileWithPresign\('([^']*)'\)/);let s=[],i="";o?(s=o[1].split(",").filter(e=>e!==""),i=o[2]):r&&(s=[r[1]],i=n.dataset.recordName||""),s=s.filter(t=>t!==e),s.length===0?(n.classList.add("disabled"),n.removeAttribute("onclick")):s.length===1?(n.classList.remove("disabled"),n.setAttribute("onclick",`downloadFileWithPresign('${s[0]}');`)):(n.classList.remove("disabled"),n.setAttribute("onclick",`downloadFilesZipWithPresign('${s.join(",")}', '${i}');`))}function showToastFileDeletionFr(e){let t=document.getElementById("toastnotificationUndo"),n=document.getElementById("cell-name-"+e).innerText,s=document.getElementById("toastFilename"),o=document.getElementById("toastUndoButton");s.innerText=n,o.dataset.fileid=e,hideToast(),t.classList.add("show"),clearTimeout(toastId),toastId=setTimeout(()=>{hideFileToast()},5e3)}function handleUndoFr(e){hideFileToast(),apiFilesRestore(e.dataset.fileid).then(e=>{window.location.reload()}).catch(e=>{alert("Unable to restore file: "+e),console.error("Error:",e)})}function showDeleteFRequestModal(e,t,n){document.getElementById("deleteModalBodyName").innerText=t,document.getElementById("deleteModalBodyCount").innerText=n,$("#deleteModal").modal("show"),document.getElementById("buttonDelete").onclick=function(){$("#deleteModal").modal("hide"),deleteFileRequest(e)}}function newFileRequest(){loadFileRequestDefaults(),document.getElementById("m_urequestlabel").innerText="New File Request",$("#addEditModal").modal("show"),document.getElementById("b_fr_save").onclick=function(){saveFileRequestDefaults(),saveFileRequest(),$("#addEditModal").modal("hide")}}function saveFileRequestDefaults(){if(document.getElementById("mc_maxfiles").checked?localStorage.setItem("fr_maxfiles",document.getElementById("mi_maxfiles").value):localStorage.setItem("fr_maxfiles",0),document.getElementById("mc_maxsize").checked?localStorage.setItem("fr_maxsize",document.getElementById("mi_maxsize").value):localStorage.setItem("fr_maxsize",0),document.getElementById("mc_expiry").checked){let e=document.getElementById("mi_expiry").value-Math.round(Date.now()/1e3);localStorage.setItem("fr_expiry",e)}else localStorage.setItem("fr_expiry",0)}function loadFileRequestDefaults(){const t=localStorage.getItem("fr_maxfiles"),n=localStorage.getItem("fr_maxsize");let e=localStorage.getItem("fr_expiry");if(e!=="0"&&e!==null){let t=new Date(Date.now()+Number(e*1e3));t.setHours(12,0,0,0),e=Math.floor(t.getTime()/1e3)}setModalValues("","",t,n,e,"")}function setModalValues(e,t,n,s,o,i){if(document.getElementById("freqId").value=e,t===null?document.getElementById("mFriendlyName").value="":document.getElementById("mFriendlyName").value=t,limitMaxFiles!=0){let e=document.getElementById("mc_maxfiles");(n===null||n==0)&&(n=limitMaxFiles),e.checked=!0,e.disabled=!0,e.title="Only admins can set this to unlimited",e.value="1",document.getElementById("mi_maxfiles").setAttribute("max",limitMaxFiles)}else{let e=document.getElementById("mc_maxfiles");e.disabled=!1,e.title="",document.getElementById("mi_maxfiles").setAttribute("max","")}if(limitMaxSize!=0){let e=document.getElementById("mc_maxsize");(s===null||s==0)&&(s=limitMaxSize),e.checked=!0,e.disabled=!0,e.title="Only admins can set this to unlimited",e.value="1",document.getElementById("mi_maxsize").setAttribute("max",limitMaxSize)}else{let e=document.getElementById("mc_maxsize");e.disabled=!1,e.title="",document.getElementById("mi_maxsize").setAttribute("max","")}if(n===null||n==0?(document.getElementById("mi_maxfiles").value="1",document.getElementById("mi_maxfiles").disabled=!0,document.getElementById("mc_maxfiles").checked=!1):(document.getElementById("mi_maxfiles").value=n,document.getElementById("mi_maxfiles").disabled=!1,document.getElementById("mc_maxfiles").checked=!0),s===null||s==0?(document.getElementById("mi_maxsize").value="10",document.getElementById("mi_maxsize").disabled=!0,document.getElementById("mc_maxsize").checked=!1):(document.getElementById("mi_maxsize").value=s,document.getElementById("mi_maxsize").disabled=!1,document.getElementById("mc_maxsize").checked=!0),o===null||o==0){const e=Math.floor(new Date(Date.now()+14*24*60*60*1e3).getTime()/1e3);document.getElementById("mi_expiry").disabled=!0,document.getElementById("mc_expiry").checked=!1,document.getElementById("mi_expiry").value=e,createCalendar("mi_expiry",e)}else document.getElementById("mi_expiry").value=o,document.getElementById("mi_expiry").disabled=!1,document.getElementById("mc_expiry").checked=!0,createCalendar("mi_expiry",o);document.getElementById("mNotes").value=i}function editFileRequest(e,t,n,s,o,i){setModalValues(e,t,n,s,o,i),document.getElementById("m_urequestlabel").innerText="Edit File Request",$("#addEditModal").modal("show"),document.getElementById("b_fr_save").onclick=function(){saveFileRequest(),$("#addEditModal").modal("hide")}}function saveFileRequest(){const s=document.getElementById("b_fr_save"),o=document.getElementById("freqId").value,i=document.getElementById("mFriendlyName").value,a=document.getElementById("mNotes").value;let e=0,t=0,n=0;document.getElementById("mc_maxfiles").checked&&(e=document.getElementById("mi_maxfiles").value),document.getElementById("mc_maxsize").checked&&(t=document.getElementById("mi_maxsize").value),document.getElementById("mc_expiry").checked&&(n=document.getElementById("mi_expiry").value),s.disabled=!0,apiURequestSave(o,i,e,t,n,a).then(e=>{document.getElementById("b_fr_save").disabled=!1,insertOrReplaceFileRequest(e)}).catch(e=>{alert("Unable to save file request: "+e),console.error("Error:",e),document.getElementById("b_fr_save").disabled=!1})}function checkMaxNumber(e){if(e.value==""){e.value="1";return}let t=e.getAttribute("max");if(t=="")return;e.value>t&&(e.value=t)}function insertOrReplaceFileRequest(e){const n=document.getElementById("filerequesttable");let t=document.getElementById(`row-${e.id}`);if(t){const n=document.getElementById(`cell-username-${e.id}`).innerText;t.replaceWith(createFileRequestRow(e,n))}else{let t=createFileRequestRow(e,userName);t.querySelectorAll("td").forEach(e=>{e.classList.add("newFileRequest"),setTimeout(()=>{e.classList.remove("newFileRequest")},700)}),n.prepend(t)}}function createFileRequestRow(e,t){function r(e){const t=document.createElement("td");return t.textContent=e,t}function h(e,t){const s=document.createElement("td"),n=document.createElement("a");return n.textContent=e,n.href=t,n.target="_blank",s.appendChild(n),s}function c(e){const t=document.createElement("i");return t.className=`bi ${e}`,t}const d=`${baseUrl}publicUpload?id=${e.id}&key=${e.apikey}`,n=document.createElement("tr");if(n.id=`row-${e.id}`,n.className="filerequest-item",n.appendChild(h(e.name,d)),e.maxfiles==0?n.appendChild(r(e.uploadedfiles)):n.appendChild(r(`${e.uploadedfiles} / ${e.maxfiles}`)),n.appendChild(r(getReadableSize(e.totalfilesize))),n.appendChild(r(formatTimestampWithNegative(e.lastupload,"None"))),n.appendChild(r(formatFileRequestExpiry(e.expiry))),canViewOtherRequests){let s=r(t);s.id=`cell-username-${e.id}`,n.appendChild(s)}const u=document.createElement("td"),l=document.createElement("div");l.className="btn-group",l.role="group";const o=document.createElement("button");o.id=`download-${e.id}`,o.type="button",o.className="btn btn-outline-light btn-sm",o.title="Download all",e.uploadedfiles==0&&o.classList.add("disabled"),o.appendChild(c("bi-download"));const s=document.createElement("button");s.id=`copy-${e.id}`,s.type="button",s.className="copyurl btn btn-outline-light btn-sm",s.title="Copy URL",s.setAttribute("data-clipboard-text",d),s.onclick=()=>showToast(1e3),s.appendChild(c("bi-copy"));const i=document.createElement("button");i.id=`edit-${e.id}`,i.type="button",i.className="btn btn-outline-light btn-sm",i.title="Edit request",i.onclick=()=>editFileRequest(e.id,e.name,e.maxfiles,e.maxsize,e.expiry,e.notes),i.appendChild(c("bi-pencil"));const a=document.createElement("button");return a.id=`delete-${e.id}`,a.type="button",a.className="btn btn-outline-danger btn-sm",a.title="Delete",a.onclick=()=>deleteOrShowModal(e.id,e.name,e.uploadedfiles),a.appendChild(c("bi-trash3")),l.append(o,s,i,a),u.appendChild(l),n.appendChild(u),n}function filterLogs(e){const t=document.getElementById("logviewer");e=="all"?t.value=logContent:t.value=logContent.split(` +const storedTokens=new Map;async function getToken(e,n){const s="./auth/token";if(!n){if(!storedTokens.has(e))return getToken(e,!0);let t=storedTokens.get(e);return t.expiry-Date.now()/1e3<60?getToken(e,!0):t.key}const o={method:"POST",headers:{"Content-Type":"application/json",permission:e}};try{const i=await fetch(s,o);if(!i.ok)throw new Error(t("error_request_failed",i.status));const n=await i.json();if(!n.hasOwnProperty("key"))throw new Error(t("error_invalid_token_response"));return storedTokens.set(e,{key:n.key,expiry:n.expiry}),n.key}catch(e){throw console.error("Error in getToken:",e),e}}async function apiAuthModify(e,n,s){const i="./api/auth/modify",a="PERM_API_MOD";let o;try{o=await getToken(a,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const r={method:"POST",headers:{"Content-Type":"application/json",apikey:o,targetKey:e,permission:n,permissionModifier:s}};try{const e=await fetch(i,r);if(!e.ok)throw new Error(t("error_request_failed",e.status))}catch(e){throw console.error("Error in apiAuthModify:",e),e}}async function apiAuthFriendlyName(e,n){const o="./api/auth/friendlyname",i="PERM_API_MOD";let s;try{s=await getToken(i,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const a={method:"PUT",headers:{"Content-Type":"application/json",apikey:s,targetKey:e,friendlyName:n}};try{const e=await fetch(o,a);if(!e.ok)throw new Error(t("error_request_failed",e.status))}catch(e){throw console.error("Error in apiAuthModify:",e),e}}async function apiAuthDelete(e){const s="./api/auth/delete",o="PERM_API_MOD";let n;try{n=await getToken(o,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const i={method:"POST",headers:{"Content-Type":"application/json",apikey:n,targetKey:e}};try{const e=await fetch(s,i);if(!e.ok)throw new Error(t("error_request_failed",e.status))}catch(e){throw console.error("Error in apiAuthDelete:",e),e}}async function apiAuthCreate(){const n="./api/auth/create",s="PERM_API_MOD";let e;try{e=await getToken(s,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const o={method:"POST",headers:{"Content-Type":"application/json",apikey:e,basicPermissions:"true"}};try{const e=await fetch(n,o);if(!e.ok)throw new Error(t("error_request_failed",e.status));const s=await e.json();return s}catch(e){throw console.error("Error in apiAuthCreate:",e),e}}async function apiChunkComplete(e,n,s,o,i,a,r,c,l,d){const h="./api/chunk/complete",m="PERM_UPLOAD";let u;try{u=await getToken(m,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const f={method:"POST",headers:{"Content-Type":"application/json",apikey:u,uuid:e,filename:"base64:"+Base64.encode(n),filesize:s,realsize:o,contenttype:i,allowedDownloads:a,expiryDays:r,password:c,isE2E:l,nonblocking:d}};try{const e=await fetch(h,f);if(!e.ok){let n;try{const s=await e.json();n=s.ErrorMessage||t("error_request_failed",e.status)}catch{const s=await e.text();n=s||t("error_request_failed",e.status)}throw new Error(n)}const n=await e.json();return n}catch(e){throw console.error("Error in apiChunkComplete:",e),e}}async function apiFilesReplace(e,n){const o="./api/files/replace",i="PERM_REPLACE";let s;try{s=await getToken(i,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const a={method:"PUT",headers:{"Content-Type":"application/json",id:e,apikey:s,idNewContent:n,deleteNewFile:!1}};try{const e=await fetch(o,a);if(!e.ok)throw new Error(t("error_request_failed",e.status));const n=await e.json();return n}catch(e){throw console.error("Error in apiFilesReplace:",e),e}}async function apiFilesListById(e){const s="./api/files/list/"+e,o="PERM_VIEW";let n;try{n=await getToken(o,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const i={method:"GET",headers:{"Content-Type":"application/json",apikey:n}};try{const e=await fetch(s,i);if(!e.ok)throw new Error(t("error_request_failed",e.status));const n=await e.json();return n}catch(e){throw console.error("Error in apiFilesListById:",e),e}}async function apiFilesListDownloadSingle(e){const s="./api/files/download/"+e,o="PERM_DOWNLOAD";let n;try{n=await getToken(o,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const i={method:"GET",headers:{"Content-Type":"application/json",apikey:n,presignUrl:!0}};try{const e=await fetch(s,i);if(!e.ok)throw new Error(t("error_request_failed",e.status));const n=await e.json();return n}catch(e){throw console.error("Error in apiFilesListDownloadSingle:",e),e}}async function apiFilesListDownloadZip(e,n){const o="./api/files/downloadzip",i="PERM_DOWNLOAD";let s;try{s=await getToken(i,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const a={method:"GET",headers:{"Content-Type":"application/json",apikey:s,ids:e,filename:"base64:"+Base64.encode(n),presignUrl:!0}};try{const e=await fetch(o,a);if(!e.ok)throw new Error(t("error_request_failed",e.status));const n=await e.json();return n}catch(e){throw console.error("Error in apiFilesListDownloadZip:",e),e}}async function apiFilesModify(e,n,s,o,i){const r="./api/files/modify",c="PERM_EDIT";let a;try{a=await getToken(c,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const l={method:"PUT",headers:{"Content-Type":"application/json",id:e,apikey:a,allowedDownloads:n,expiryTimestamp:s,password:o,originalPassword:i}};try{const e=await fetch(r,l);if(!e.ok)throw new Error(t("error_request_failed",e.status));const n=await e.json();return n}catch(e){throw console.error("Error in apiFilesModify:",e),e}}async function apiFilesDelete(e,n){const o="./api/files/delete",i="PERM_DELETE";let s;try{s=await getToken(i,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const a={method:"POST",headers:{"Content-Type":"application/json",apikey:s,id:e,delay:n}};try{const e=await fetch(o,a);if(!e.ok)throw new Error(t("error_request_failed",e.status))}catch(e){throw console.error("Error in apiFilesDelete:",e),e}}async function apiFilesRestore(e){const s="./api/files/restore",o="PERM_DELETE";let n;try{n=await getToken(o,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const i={method:"POST",headers:{"Content-Type":"application/json",apikey:n,id:e}};try{const e=await fetch(s,i);if(!e.ok)throw new Error(t("error_request_failed",e.status));const n=await e.json();return n}catch(e){throw console.error("Error in apiFilesRestore:",e),e}}async function apiUserCreate(e){const s="./api/user/create",o="PERM_MANAGE_USERS";let n;try{n=await getToken(o,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const i={method:"POST",headers:{"Content-Type":"application/json",apikey:n,username:e}};try{const e=await fetch(s,i);if(!e.ok)throw e.status==409?new Error("duplicate"):new Error(t("error_request_failed",e.status));const n=await e.json();return n}catch(e){throw console.error("Error in apiUserModify:",e),e}}async function apiUserModify(e,n,s){const i="./api/user/modify",a="PERM_MANAGE_USERS";let o;try{o=await getToken(a,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const r={method:"POST",headers:{"Content-Type":"application/json",apikey:o,userid:e,userpermission:n,permissionModifier:s}};try{const e=await fetch(i,r);if(!e.ok)throw new Error(t("error_request_failed",e.status))}catch(e){throw console.error("Error in apiUserModify:",e),e}}async function apiUserChangeRank(e,n){const o="./api/user/changeRank",i="PERM_MANAGE_USERS";let s;try{s=await getToken(i,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const a={method:"POST",headers:{"Content-Type":"application/json",apikey:s,userid:e,newRank:n}};try{const e=await fetch(o,a);if(!e.ok)throw new Error(t("error_request_failed",e.status))}catch(e){throw console.error("Error in apiUserModify:",e),e}}async function apiUserDelete(e,n){const o="./api/user/delete",i="PERM_MANAGE_USERS";let s;try{s=await getToken(i,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const a={method:"POST",headers:{"Content-Type":"application/json",apikey:s,userid:e,deleteFiles:n}};try{const e=await fetch(o,a);if(!e.ok)throw new Error(t("error_request_failed",e.status))}catch(e){throw console.error("Error in apiUserDelete:",e),e}}async function apiUserResetPassword(e,n){const o="./api/user/resetPassword",i="PERM_MANAGE_USERS";let s;try{s=await getToken(i,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const a={method:"POST",headers:{"Content-Type":"application/json",apikey:s,userid:e,generateNewPassword:n}};try{const e=await fetch(o,a);if(!e.ok)throw new Error(t("error_request_failed",e.status));const n=await e.json();return n}catch(e){throw console.error("Error in apiUserResetPassword:",e),e}}async function apiLogSystemStatus(){const n="./api/logs/systemStatus",s="PERM_MANAGE_LOGS";let e;try{e=await getToken(s,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const o={method:"GET",headers:{"Content-Type":"application/json",apikey:e}};try{const e=await fetch(n,o);if(!e.ok)throw new Error(t("error_request_failed",e.status));const s=await e.json();return s}catch(e){throw console.error("Error in apiLogSystemStatus:",e),e}}async function apiLogResetTraffic(){const n="./api/logs/resetTraffic",s="PERM_MANAGE_LOGS";let e;try{e=await getToken(s,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const o={method:"GET",headers:{"Content-Type":"application/json",apikey:e}};try{const e=await fetch(n,o);if(!e.ok)throw new Error(t("error_request_failed",e.status))}catch(e){throw console.error("Error in apiLogResetTraffic:",e),e}}async function apiLogGet(e){const s="./api/logs/get",o="PERM_MANAGE_LOGS";let n;try{n=await getToken(o,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const i={method:"GET",headers:{"Content-Type":"application/json",apikey:n,timestamp:e}};try{const e=await fetch(s,i);if(!e.ok)throw new Error(t("error_request_failed",e.status));const n=await e.json();return n}catch(e){throw console.error("Error in apiLogGet:",e),e}}async function apiLogsDelete(e){const s="./api/logs/delete",o="PERM_MANAGE_LOGS";let n;try{n=await getToken(o,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const i={method:"POST",headers:{"Content-Type":"application/json",apikey:n,timestamp:e}};try{const e=await fetch(s,i);if(!e.ok)throw new Error(t("error_request_failed",e.status))}catch(e){throw console.error("Error in apiLogsDelete:",e),e}}async function apiE2eGet(){const n="./api/e2e/get",s="PERM_UPLOAD";let e;try{e=await getToken(s,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const o={method:"POST",headers:{"Content-Type":"application/json",apikey:e}};try{const e=await fetch(n,o);if(!e.ok)throw new Error(t("error_request_failed",e.status));return await e.text()}catch(e){throw console.error("Error in apiE2eGet:",e),e}}async function apiE2eMutexLockUnlock(e){let n="./api/e2e/mutex/lock";e&&(n="./api/e2e/mutex/unlock");const o="PERM_UPLOAD";let s;try{s=await getToken(o,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const i={method:"GET",headers:{"Content-Type":"application/json",apikey:s}};try{const e=await fetch(n,i);if(!e.ok)throw new Error(t("error_request_failed",e.status));return await e.text()}catch(e){throw console.error("Error in apiE2eMutexLock:",e),e}}async function apiE2eStore(e){const s="./api/e2e/set",o="PERM_UPLOAD";let n;try{n=await getToken(o,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const i={method:"POST",headers:{"Content-Type":"application/json",apikey:n},body:JSON.stringify({content:e})};try{const e=await fetch(s,i);if(!e.ok)throw new Error(t("error_request_failed",e.status))}catch(e){throw console.error("Error in apiE2eStore:",e),e}}async function apiURequestDelete(e){const s="./api/uploadrequest/delete",o="PERM_MANAGE_FILE_REQUESTS";let n;try{n=await getToken(o,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const i={method:"DELETE",headers:{"Content-Type":"application/json",apikey:n,id:e}};try{const e=await fetch(s,i);if(!e.ok)throw new Error(t("error_request_failed",e.status))}catch(e){throw console.error("Error in apiURequestDelete:",e),e}}async function apiURequestSave(e,n,s,o,i,a){const c="./api/uploadrequest/save",l="PERM_MANAGE_FILE_REQUESTS";let r;try{r=await getToken(l,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const d={method:"POST",headers:{"Content-Type":"application/json",apikey:r,id:e,name:"base64:"+Base64.encode(n),expiry:i,maxfiles:s,maxsize:o,notes:"base64:"+Base64.encode(a)}};try{const e=await fetch(c,d);if(!e.ok)throw new Error(t("error_request_failed",e.status));const n=await e.json();return n}catch(e){throw console.error("Error in apiURequestDelete:",e),e}}try{var toastId,calendarInstance,dropzoneObject,isE2EEnabled,isUploading,rowCount,sseWorkerPort,statusItemCount,clipboard=new ClipboardJS(".copyurl")}catch{}function showToast(e,t){let n=document.getElementById("toastnotification");typeof t!="undefined"?n.innerText=t:n.innerText=n.dataset.default,n.classList.add("show"),clearTimeout(toastId),toastId=setTimeout(()=>{hideToast()},e)}function hideToast(){document.getElementById("toastnotification").classList.remove("show")}calendarInstance=null;function createCalendar(e,t){const n=new Date(t*1e3);calendarInstance=flatpickr(document.getElementById(e),{enableTime:!0,dateFormat:"U",altInput:!0,altFormat:"Y-m-d H:i",allowInput:!0,time_24hr:!0,defaultDate:n,minDate:"today"})}function handleEditCheckboxChange(e){var t=document.getElementById(e.getAttribute("data-toggle-target")),n=e.getAttribute("data-timestamp");e.checked?(t.classList.remove("disabled"),t.removeAttribute("disabled"),n!=null&&(calendarInstance._input.disabled=!1)):(n!=null&&(calendarInstance._input.disabled=!0),t.classList.add("disabled"),t.setAttribute("disabled",!0))}function downloadFileWithPresign(e){apiFilesListDownloadSingle(e).then(e=>{if(!e.hasOwnProperty("downloadUrl"))throw new Error(t("error_presign"));const n=document.createElement("a");n.href=e.downloadUrl,n.style.display="none",document.body.appendChild(n),n.click(),n.remove()}).catch(e=>{alert(t("error_unable_download",e)),console.error("Error:",e)})}function downloadFilesZipWithPresign(e,n){apiFilesListDownloadZip(e,n).then(e=>{if(!e.hasOwnProperty("downloadUrl"))throw new Error(t("error_presign"));const n=document.createElement("a");n.href=e.downloadUrl,n.style.display="none",document.body.appendChild(n),n.click(),n.remove()}).catch(e=>{alert(t("error_unable_download",e)),console.error("Error:",e)})}function doLogout(){typeof sseWorkerPort!="undefined"&&sseWorkerPort!==null&&sseWorkerPort.postMessage({type:"shutdown"}),window.location.href="./logout"}function changeApiPermission(e,n,s){var i,a,o=document.getElementById(s);if(o.classList.contains("perm-processing")||o.classList.contains("perm-nochange"))return;i=o.classList.contains("perm-granted"),o.classList.add("perm-processing"),o.classList.remove("perm-granted"),o.classList.remove("perm-notgranted"),a="GRANT",i&&(a="REVOKE"),apiAuthModify(e,n,a).then(e=>{i?(o.classList.add("perm-notgranted"),o.classList.add("perm-nownotgranted")):(o.classList.add("perm-granted"),o.classList.add("perm-nowgranted")),o.classList.remove("perm-processing"),setTimeout(()=>{o.classList.remove("perm-nowgranted"),o.classList.remove("perm-nownotgranted")},1e3)}).catch(e=>{i?o.classList.add("perm-granted"):o.classList.add("perm-notgranted"),o.classList.remove("perm-processing"),alert(t("error_unable_set_permission",e)),console.error("Error:",e)})}function deleteApiKey(e){document.getElementById("delete-"+e).disabled=!0,apiAuthDelete(e).then(t=>{document.getElementById("row-"+e).classList.add("rowDeleting"),setTimeout(()=>{document.getElementById("row-"+e).remove()},290)}).catch(e=>{alert(t("error_unable_delete_api_key",e)),console.error("Error:",e)})}function newApiKey(){document.getElementById("button-newapi").disabled=!0,apiAuthCreate().then(e=>{addRowApi(e.Id,e.PublicId),document.getElementById("button-newapi").disabled=!1}).catch(e=>{alert(t("error_unable_create_api_key",e)),console.error("Error:",e)})}function addFriendlyNameChange(e){let n=document.getElementById("friendlyname-"+e);if(n.classList.contains("isBeingEdited"))return;n.classList.add("isBeingEdited");let a=n.innerText,s=document.createElement("input");s.size=5,s.value=a;let o=!0,i=function(){if(!o)return;o=!1;let i=s.value;i==""&&(i=t("api_unnamed_key")),n.innerText=i,n.classList.remove("isBeingEdited"),apiAuthFriendlyName(e,i).catch(e=>{alert(t("error_unable_save_name",e)),console.error("Error:",e)})};s.onblur=i,s.addEventListener("keyup",function(e){e.keyCode===13&&(e.preventDefault(),i())}),n.innerText="",n.appendChild(s),s.focus()}function addRowApi(e,n){let g=document.getElementById("apitable"),o=g.insertRow(0);o.id="row-"+n;let a=0,l=o.insertCell(a++),d=o.insertCell(a++),u=o.insertCell(a++),r=o.insertCell(a++),h;canViewOtherApiKeys&&(h=o.insertCell(a++));let m=o.insertCell(a++);canViewOtherApiKeys&&(h.classList.add("newApiKey"),h.innerText=userName),l.classList.add("newApiKey"),d.classList.add("newApiKey"),u.classList.add("newApiKey"),r.classList.add("newApiKey"),r.classList.add("prevent-select"),m.classList.add("newApiKey"),l.innerText=t("api_unnamed_key"),l.id="friendlyname-"+n,l.onclick=function(){addFriendlyNameChange(n)},d.innerText=e,d.classList.add("font-monospace"),u.innerText=t("generic_never");const c=document.createElement("div");c.className="btn-group",c.setAttribute("role","group");const s=document.createElement("button");s.type="button",s.dataset.clipboardText=e,s.title=t("api_copy_key"),s.className="copyurl btn btn-outline-light btn-sm",s.setAttribute("onclick","showToast(1000)");const f=document.createElement("i");f.className="bi bi-copy",s.appendChild(f);const i=document.createElement("button");i.type="button",i.id=`delete-${n}`,i.title=t("btn_delete"),i.className="btn btn-outline-danger btn-sm",i.setAttribute("onclick",`deleteApiKey('${n}')`);const p=document.createElement("i");p.className="bi bi-trash3",i.appendChild(p),c.appendChild(s),c.appendChild(i),m.appendChild(c);const v=[{perm:"PERM_VIEW",icon:"bi-eye",granted:!0,title:t("apiperm_view")},{perm:"PERM_UPLOAD",icon:"bi-file-earmark-plus",granted:!0,title:t("apiperm_upload")},{perm:"PERM_EDIT",icon:"bi-pencil",granted:!0,title:t("apiperm_edit")},{perm:"PERM_DELETE",icon:"bi-trash3",granted:!0,title:t("apiperm_delete")},{perm:"PERM_REPLACE",icon:"bi-recycle",granted:!1,title:t("apiperm_replace")},{perm:"PERM_DOWNLOAD",icon:"bi-box-arrow-in-down",granted:!1,title:t("apiperm_download")},{perm:"PERM_MANAGE_FILE_REQUESTS",icon:"bi-file-earmark-arrow-up",granted:!1,title:t("apiperm_file_requests")},{perm:"PERM_MANAGE_USERS",icon:"bi-people",granted:!1,title:t("apiperm_users")},{perm:"PERM_MANAGE_LOGS",icon:"bi-card-list",granted:!1,title:t("apiperm_logs")},{perm:"PERM_API_MOD",icon:"bi-sliders2",granted:!1,title:t("apiperm_api")}];if(v.forEach(({perm:e,icon:t,granted:s,title:o})=>{const i=document.createElement("i"),a=`${e.toLowerCase()}_${n}`;i.id=a,i.className=`bi ${t} ${s?"perm-granted":"perm-notgranted"}`,i.title=o,i.setAttribute("onclick",`changeApiPermission("${n}","${e}", "${a}");`),r.appendChild(i),r.appendChild(document.createTextNode(" "))}),!canReplaceFiles){let e=document.getElementById("perm_replace_"+n);e.classList.add("perm-unavailable"),e.classList.add("perm-nochange")}if(!canManageUsers){let e=document.getElementById("perm_manage_users_"+n);e.classList.add("perm-unavailable"),e.classList.add("perm-nochange")}if(!canViewSystemLog){let e=document.getElementById("perm_manage_logs_"+n);e.classList.add("perm-unavailable"),e.classList.add("perm-nochange")}if(!canCreateFileRequest){let e=document.getElementById("perm_manage_file_requests_"+n);e.classList.add("perm-unavailable"),e.classList.add("perm-nochange")}setTimeout(()=>{l.classList.remove("newApiKey"),d.classList.remove("newApiKey"),u.classList.remove("newApiKey"),r.classList.remove("newApiKey"),m.classList.remove("newApiKey")},700)}function deleteFileRequest(e){document.getElementById("delete-"+e).disabled=!0,apiURequestDelete(e).then(t=>{const s=document.getElementById("row-"+e),n=document.getElementById("filelist-"+e);s.classList.add("rowDeleting"),n!==null&&n.classList.add("rowDeleting"),setTimeout(()=>{s.remove(),n!==null&&n.remove()},290)}).catch(e=>{alert(t("error_unable_delete_file_request",e)),console.error("Error:",e)})}function deleteOrShowModal(e,t,n){n===0?deleteFileRequest(e):showDeleteFRequestModal(e,t,n)}function deleteFileFr(e,n){document.getElementById("button-delete-"+e).disabled=!0;let s=document.getElementById("cell-listupload-"+e);apiFilesDelete(e,10).then(t=>{changeFileCountFr(n,-1),removeDownloadFileReference(e,n),s.classList.add("rowDeleting"),setTimeout(()=>{s.remove()},290),showToastFileDeletionFr(e)}).catch(e=>{alert(t("error_unable_delete_file",e)),console.error("Error:",e)})}function changeFileCountFr(e,t){let n=document.getElementById("totalFiles-fr-"+e),s=Number(n.innerText)||0,o=s+t;n.innerText=o}function removeDownloadFileReference(e,t){const n=document.getElementById(`download-${t}`);if(!n)return;const a=n.getAttribute("onclick")||"",o=a.match(/downloadFilesZipWithPresign\('([^']*)',\s*'([^']*)'\)/),r=a.match(/downloadFileWithPresign\('([^']*)'\)/);let s=[],i="";o?(s=o[1].split(",").filter(e=>e!==""),i=o[2]):r&&(s=[r[1]],i=n.dataset.recordName||""),s=s.filter(t=>t!==e),s.length===0?(n.classList.add("disabled"),n.removeAttribute("onclick")):s.length===1?(n.classList.remove("disabled"),n.setAttribute("onclick",`downloadFileWithPresign('${s[0]}');`)):(n.classList.remove("disabled"),n.setAttribute("onclick",`downloadFilesZipWithPresign('${s.join(",")}', '${i}');`))}function showToastFileDeletionFr(e){let t=document.getElementById("toastnotificationUndo"),n=document.getElementById("cell-name-"+e).innerText,s=document.getElementById("toastFilename"),o=document.getElementById("toastUndoButton");s.innerText=n,o.dataset.fileid=e,hideToast(),t.classList.add("show"),clearTimeout(toastId),toastId=setTimeout(()=>{hideFileToast()},5e3)}function handleUndoFr(e){hideFileToast(),apiFilesRestore(e.dataset.fileid).then(e=>{window.location.reload()}).catch(e=>{alert(t("error_unable_restore_file",e)),console.error("Error:",e)})}function showDeleteFRequestModal(e,t,n){document.getElementById("deleteModalBodyName").innerText=t,document.getElementById("deleteModalBodyCount").innerText=n,$("#deleteModal").modal("show"),document.getElementById("buttonDelete").onclick=function(){$("#deleteModal").modal("hide"),deleteFileRequest(e)}}function newFileRequest(){loadFileRequestDefaults(),document.getElementById("m_urequestlabel").innerText=t("fr_new_title"),$("#addEditModal").modal("show"),document.getElementById("b_fr_save").onclick=function(){saveFileRequestDefaults(),saveFileRequest(),$("#addEditModal").modal("hide")}}function saveFileRequestDefaults(){if(document.getElementById("mc_maxfiles").checked?localStorage.setItem("fr_maxfiles",document.getElementById("mi_maxfiles").value):localStorage.setItem("fr_maxfiles",0),document.getElementById("mc_maxsize").checked?localStorage.setItem("fr_maxsize",document.getElementById("mi_maxsize").value):localStorage.setItem("fr_maxsize",0),document.getElementById("mc_expiry").checked){let e=document.getElementById("mi_expiry").value-Math.round(Date.now()/1e3);localStorage.setItem("fr_expiry",e)}else localStorage.setItem("fr_expiry",0)}function loadFileRequestDefaults(){const t=localStorage.getItem("fr_maxfiles"),n=localStorage.getItem("fr_maxsize");let e=localStorage.getItem("fr_expiry");if(e!=="0"&&e!==null){let t=new Date(Date.now()+Number(e*1e3));t.setHours(12,0,0,0),e=Math.floor(t.getTime()/1e3)}setModalValues("","",t,n,e,"")}function setModalValues(e,n,s,o,i,a){if(document.getElementById("freqId").value=e,n===null?document.getElementById("mFriendlyName").value="":document.getElementById("mFriendlyName").value=n,limitMaxFiles!=0){let e=document.getElementById("mc_maxfiles");(s===null||s==0)&&(s=limitMaxFiles),e.checked=!0,e.disabled=!0,e.title=t("fr_admins_only_unlimited"),e.value="1",document.getElementById("mi_maxfiles").setAttribute("max",limitMaxFiles)}else{let e=document.getElementById("mc_maxfiles");e.disabled=!1,e.title="",document.getElementById("mi_maxfiles").setAttribute("max","")}if(limitMaxSize!=0){let e=document.getElementById("mc_maxsize");(o===null||o==0)&&(o=limitMaxSize),e.checked=!0,e.disabled=!0,e.title=t("fr_admins_only_unlimited"),e.value="1",document.getElementById("mi_maxsize").setAttribute("max",limitMaxSize)}else{let e=document.getElementById("mc_maxsize");e.disabled=!1,e.title="",document.getElementById("mi_maxsize").setAttribute("max","")}if(s===null||s==0?(document.getElementById("mi_maxfiles").value="1",document.getElementById("mi_maxfiles").disabled=!0,document.getElementById("mc_maxfiles").checked=!1):(document.getElementById("mi_maxfiles").value=s,document.getElementById("mi_maxfiles").disabled=!1,document.getElementById("mc_maxfiles").checked=!0),o===null||o==0?(document.getElementById("mi_maxsize").value="10",document.getElementById("mi_maxsize").disabled=!0,document.getElementById("mc_maxsize").checked=!1):(document.getElementById("mi_maxsize").value=o,document.getElementById("mi_maxsize").disabled=!1,document.getElementById("mc_maxsize").checked=!0),i===null||i==0){const e=Math.floor(new Date(Date.now()+14*24*60*60*1e3).getTime()/1e3);document.getElementById("mi_expiry").disabled=!0,document.getElementById("mc_expiry").checked=!1,document.getElementById("mi_expiry").value=e,createCalendar("mi_expiry",e)}else document.getElementById("mi_expiry").value=i,document.getElementById("mi_expiry").disabled=!1,document.getElementById("mc_expiry").checked=!0,createCalendar("mi_expiry",i);document.getElementById("mNotes").value=a}function editFileRequest(e,n,s,o,i,a){setModalValues(e,n,s,o,i,a),document.getElementById("m_urequestlabel").innerText=t("fr_edit_title"),$("#addEditModal").modal("show"),document.getElementById("b_fr_save").onclick=function(){saveFileRequest(),$("#addEditModal").modal("hide")}}function saveFileRequest(){const o=document.getElementById("b_fr_save"),i=document.getElementById("freqId").value,a=document.getElementById("mFriendlyName").value,r=document.getElementById("mNotes").value;let e=0,n=0,s=0;document.getElementById("mc_maxfiles").checked&&(e=document.getElementById("mi_maxfiles").value),document.getElementById("mc_maxsize").checked&&(n=document.getElementById("mi_maxsize").value),document.getElementById("mc_expiry").checked&&(s=document.getElementById("mi_expiry").value),o.disabled=!0,apiURequestSave(i,a,e,n,s,r).then(e=>{document.getElementById("b_fr_save").disabled=!1,insertOrReplaceFileRequest(e)}).catch(e=>{alert(t("error_unable_save_file_request",e)),console.error("Error:",e),document.getElementById("b_fr_save").disabled=!1})}function checkMaxNumber(e){if(e.value==""){e.value="1";return}let t=e.getAttribute("max");if(t=="")return;e.value>t&&(e.value=t)}function insertOrReplaceFileRequest(e){const n=document.getElementById("filerequesttable");let t=document.getElementById(`row-${e.id}`);if(t){const n=document.getElementById(`cell-username-${e.id}`).innerText;t.replaceWith(createFileRequestRow(e,n))}else{let t=createFileRequestRow(e,userName);t.querySelectorAll("td").forEach(e=>{e.classList.add("newFileRequest"),setTimeout(()=>{e.classList.remove("newFileRequest")},700)}),n.prepend(t)}}function createFileRequestRow(e,n){function c(e){const t=document.createElement("td");return t.textContent=e,t}function m(e,t){const s=document.createElement("td"),n=document.createElement("a");return n.textContent=e,n.href=t,n.target="_blank",s.appendChild(n),s}function l(e){const t=document.createElement("i");return t.className=`bi ${e}`,t}const u=`${baseUrl}publicUpload?id=${e.id}&key=${e.apikey}`,s=document.createElement("tr");if(s.id=`row-${e.id}`,s.className="filerequest-item",s.appendChild(m(e.name,u)),e.maxfiles==0?s.appendChild(c(e.uploadedfiles)):s.appendChild(c(`${e.uploadedfiles} / ${e.maxfiles}`)),s.appendChild(c(getReadableSize(e.totalfilesize))),s.appendChild(c(formatTimestampWithNegative(e.lastupload,t("generic_none")))),s.appendChild(c(formatFileRequestExpiry(e.expiry))),canViewOtherRequests){let t=c(n);t.id=`cell-username-${e.id}`,s.appendChild(t)}const h=document.createElement("td"),d=document.createElement("div");d.className="btn-group",d.role="group";const i=document.createElement("button");i.id=`download-${e.id}`,i.type="button",i.className="btn btn-outline-light btn-sm",i.title=t("fr_download_all"),e.uploadedfiles==0&&i.classList.add("disabled"),i.appendChild(l("bi-download"));const o=document.createElement("button");o.id=`copy-${e.id}`,o.type="button",o.className="copyurl btn btn-outline-light btn-sm",o.title=t("btn_copy_url"),o.setAttribute("data-clipboard-text",u),o.onclick=()=>showToast(1e3),o.appendChild(l("bi-copy"));const a=document.createElement("button");a.id=`edit-${e.id}`,a.type="button",a.className="btn btn-outline-light btn-sm",a.title=t("fr_edit_request"),a.onclick=()=>editFileRequest(e.id,e.name,e.maxfiles,e.maxsize,e.expiry,e.notes),a.appendChild(l("bi-pencil"));const r=document.createElement("button");return r.id=`delete-${e.id}`,r.type="button",r.className="btn btn-outline-danger btn-sm",r.title=t("btn_delete"),r.onclick=()=>deleteOrShowModal(e.id,e.name,e.uploadedfiles),r.appendChild(l("bi-trash3")),d.append(i,o,a,r),h.appendChild(d),s.appendChild(h),s}function filterLogs(e){const t=document.getElementById("logviewer");e=="all"?t.value=logContent:t.value=logContent.split(` `).filter(t=>t.includes("["+e+"]")).join(` -`),t.scrollTop=t.scrollHeight}function setTrafficInfo(e,t){insertReadableSizeTwoOutputs(e,"totalTraffic","totalTrafficUnit"),document.getElementById("cardTraffic").title="Traffic since "+formatUnixTimestamp(t)}function setMemoryUsage(e,t){insertReadableSizeTwoOutputs(t,"totalMemory","memoryUnit");let n=document.getElementById("memoryUnit").innerText;insertReadableSizeForcedUnit(e,"usedMemory",n)}function setDiskUsage(e,t){insertReadableSizeTwoOutputs(t,"totalDisk","diskUnit");let n=document.getElementById("diskUnit").innerText;insertReadableSizeForcedUnit(e,"usedDisk",n)}function formatDuration(e){const t=[{label:"y",value:31536e3},{label:"d",value:86400},{label:"h",value:3600},{label:"m",value:60},{label:"s",value:1}];let n=t.findIndex(t=>e>=t.value);(n===-1||t[n].label==="s")&&(n=t.findIndex(e=>e.label==="m"));const s=t[n],o=t[n+1],i=Math.floor(e/s.value),a=e%s.value,r=Math.floor(a/o.value);return`${i}${s.label} ${r}${o.label}`}function addUptime(){if(currentUptime>3600)return;setTimeout(()=>{++currentUptime,document.getElementById("uptime").innerText=formatDuration(currentUptime),addUptime()},1e3)}function setPercentageBar(e,t,n){let o=t;n!==0[0]&&(o=t/n*100);const s=document.getElementById(e);s.classList.remove("bg-success"),s.classList.remove("bg-warning"),s.classList.remove("bg-danger"),o<70&&s.classList.add("bg-success"),o>=70&&o<90&&s.classList.add("bg-warning"),o>=90&&s.classList.add("bg-danger"),s.style.width=o+"%"}async function loadLogs(e){const t=document.getElementById("logviewer");try{const n=await apiLogGet(e);lastLogUpdate=n.timestamp;let s=!0;if(e!=0){if(n.logEntries=="")return;s=allowScroll(),logContent=logContent+n.logEntries}else logContent=n.logEntries;filterLogs(document.getElementById("logFilter").value),s&&(t.scrollTop=t.scrollHeight)}catch(e){lastLogUpdate=0,console.error("Failed to load logs:",e),t.value="Error loading logs. See console for details."}}async function loadStatus(){try{const e=await apiLogSystemStatus();currentUptime=e.uptime,document.getElementById("labelCpu").innerText=e.cpuLoad+"%",document.getElementById("labelActiveFiles").innerText=e.activeFiles,setPercentageBar("barCpu",e.cpuLoad),setPercentageBar("barDisk",e.diskUsagePercentage),setPercentageBar("barMemory",e.memoryUsagePercentage),setMemoryUsage(e.memoryUsed,e.memoryTotal),setDiskUsage(e.diskUsed,e.diskTotal),setTrafficInfo(e.dataServed,e.trafficRecordingSince)}catch(e){console.error("Failed to server status:",e)}}async function pollInfo(){for(firstStart=!0;!0;)await loadLogs(lastLogUpdate),firstStart?firstStart=!1:await loadStatus(),await new Promise(e=>setTimeout(e,POLL_INTERVAL_S*1e3))}function allowScroll(){const e=document.getElementById("logviewer");return e.scrollTop+e.clientHeight>=e.scrollHeight-5}function deleteLogs(){const n=document.getElementById("deleteLogsSel");if(!n)return;const t=n.value;if(t=="none"||t=="")return;if(!confirm("Do you want to delete the selected logs?")){document.getElementById("deleteLogs").selectedIndex=0;return}let e=Math.floor(Date.now()/1e3);switch(t){case"all":e=0;break;case"2":e=e-2*24*60*60;break;case"7":e=e-7*24*60*60;break;case"14":e=e-14*24*60*60;break;case"30":e=e-30*24*60*60;break;default:return}apiLogsDelete(e).then(e=>{location.reload()}).catch(e=>{alert("Unable to delete logs: "+e),console.error("Error:",e)})}function resetTrafficStat(){if(!confirm("Do you want to reset the traffic statistics?"))return;apiLogResetTraffic().then(e=>{location.reload()}).catch(e=>{alert("Unable to reset stats: "+e),console.error("Error:",e)})}isE2EEnabled=!1,isUploading=!1,rowCount=-1;function initDropzone(){Dropzone.options.uploaddropzone={paramName:"file",dictDefaultMessage:"",createImageThumbnails:!1,chunksUploaded:function(e,t){sendChunkComplete(e,t)},init:function(){dropzoneObject=this,this.on("addedfile",e=>{e.upload.uuid=getUuid(),saveUploadDefaults(),addFileProgress(e)}),this.on("queuecomplete",function(){isUploading=!1}),this.on("sending",function(){isUploading=!0}),this.on("error",function(e,t,n){if(console.log(t),n){if(n.status===413){showError(e,"File too large to upload. If you are using a reverse proxy, make sure that the allowed body size is at least 70MB.");return}try{console.log(n),errInfo=JSON.parse(n.responseText),showError(e,"Error: "+errInfo.ErrorMessage)}catch{showError(e,"Error: "+n.responseText)}}else showError(e,"Error: "+t)}),this.on("uploadprogress",function(e,t,n){updateProgressbar(e,t,n)}),isE2EEnabled&&(dropzoneObject.disable(),setE2eUpload())}},document.onpaste=function(e){if(dropzoneObject.disabled)return;const n=document.activeElement;if(n&&(n.hasAttribute("data-allow-regular-paste")||n.hasAttribute("placeholder")))return;var t,s=(e.clipboardData||e.originalEvent.clipboardData).items;for(let e in s)t=s[e],t.kind==="file"&&dropzoneObject.addFile(t.getAsFile()),t.kind==="string"&&t.getAsString(function(e){const t=//gi;if(t.test(e)===!1){let t=new Blob([e],{type:"text/plain"}),n=new File([t],"Pasted Text.txt",{type:"text/plain",lastModified:new Date(0)});dropzoneObject.addFile(n)}})},window.addEventListener("beforeunload",e=>{isUploading&&(e.returnValue="Upload is still in progress. Do you want to close this page?")})}function updateProgressbar(e,t,n){let o=e.upload.uuid,i=document.getElementById(`us-container-${o}`);if(i==null||i.getAttribute("data-complete")==="true")return;let s=Math.round(t);s<0&&(s=0),s>100&&(s=100);let r=Date.now()-i.getAttribute("data-starttime"),c=n/(r/1e3)/1024/1024;document.getElementById(`us-progressbar-${o}`).style.width=s+"%";let a=Math.round(c*10)/10;Number.isNaN(a)||(document.getElementById(`us-progress-info-${o}`).innerText=s+"% - "+a+"MB/s")}function addFileProgress(e){addFileStatus(e.upload.uuid,e.upload.filename)}function setUploadDefaults(){let s=getLocalStorageWithDefault("defaultDownloads",1),o=getLocalStorageWithDefault("defaultExpiry",14),e=getLocalStorageWithDefault("defaultPassword",""),t=getLocalStorageWithDefault("defaultUnlimitedDownloads",!1)==="true",n=getLocalStorageWithDefault("defaultUnlimitedTime",!1)==="true";document.getElementById("allowedDownloads").value=s,document.getElementById("expiryDays").value=o,document.getElementById("password").value=e,document.getElementById("enableDownloadLimit").checked=!t,document.getElementById("enableTimeLimit").checked=!n,e===""?(document.getElementById("enablePassword").checked=!1,document.getElementById("password").disabled=!0):(document.getElementById("enablePassword").checked=!0,document.getElementById("password").disabled=!1),t&&(document.getElementById("allowedDownloads").disabled=!0),n&&(document.getElementById("expiryDays").disabled=!0)}function saveUploadDefaults(){localStorage.setItem("defaultDownloads",document.getElementById("allowedDownloads").value),localStorage.setItem("defaultExpiry",document.getElementById("expiryDays").value),localStorage.setItem("defaultPassword",document.getElementById("password").value),localStorage.setItem("defaultUnlimitedDownloads",!document.getElementById("enableDownloadLimit").checked),localStorage.setItem("defaultUnlimitedTime",!document.getElementById("enableTimeLimit").checked)}function getLocalStorageWithDefault(e,t){var n=localStorage.getItem(e);return n===null?t:n}function urlencodeFormData(e){let t="";function s(e){return encodeURIComponent(e).replace(/%20/g,"+")}for(var n of e.entries())typeof n[1]=="string"&&(t+=(t?"&":"")+s(n[0])+"="+s(n[1]));return t}function sendChunkComplete(e,t){let c=e.upload.uuid,n=e.name,s=e.size,l=e.size,o=e.type,i=document.getElementById("allowedDownloads").value,a=document.getElementById("expiryDays").value,d=document.getElementById("password").value,r=e.isEndToEndEncrypted===!0,u=!0;document.getElementById("enableDownloadLimit").checked||(i=0),document.getElementById("enableTimeLimit").checked||(a=0),r&&(s=e.sizeEncrypted,n="Encrypted File",o=""),apiChunkComplete(c,n,s,l,o,i,a,d,r,u).then(n=>{t();let s=document.getElementById(`us-progress-info-${e.upload.uuid}`);s!=null&&(s.innerText="In Queue...")}).catch(t=>{console.error("Error:",t),dropzoneUploadError(e,t)})}function dropzoneUploadError(e,t){e.accepted=!1,dropzoneObject._errorProcessing([e],t),showError(e,t)}function dropzoneGetFile(e){for(let t=0;t{addRow(n),notifyWorker({type:"fileAdded",item:n});let s=dropzoneGetFile(t);if(s==null)return;s.isEndToEndEncrypted===!0?apiE2eMutexLockUnlock(!1).then(()=>apiE2eGet()).then(n=>{let i=GokapiE2EInfoParse(n);if(i instanceof Error)throw i;let a=GokapiE2EAddFile(t,e,s.name);if(a instanceof Error)throw a;let o=GokapiE2EInfoEncrypt();if(o instanceof Error)throw o;return apiE2eStore(o)}).then(()=>{GokapiE2EDecryptMenu(),removeFileStatus(t)}).catch(e=>{s.accepted=!1,dropzoneObject._errorProcessing([s],e),console.error("Error:",e)}).finally(()=>{apiE2eMutexLockUnlock(!0).catch(e=>{console.error("Failed to release E2E mutex after write: "+e)})}):removeFileStatus(t)}).catch(e=>{let n=dropzoneGetFile(t);n!=null&&dropzoneUploadError(n,e),console.error("Error:",e)})}function parseProgressStatus(e){let n=document.getElementById(`us-container-${e.chunk_id}`);if(n==null)return;n.setAttribute("data-complete","true");let t;switch(e.upload_status){case 0:t="Processing file...";break;case 1:t="Saving file...";break;case 2:t="Finalising...",requestFileInfo(e.file_id,e.chunk_id);break;case 3:t="Error";let n=dropzoneGetFile(e.chunk_id);e.error_message==""&&(e.error_message="Server Error"),n!=null&&dropzoneUploadError(n,e.error_message);return;default:t="Unknown status";break}document.getElementById(`us-progress-info-${e.chunk_id}`).innerText=t}function showError(e,t){let n=e.upload.uuid;document.getElementById(`us-progressbar-${n}`).style.width="100%",document.getElementById(`us-progressbar-${n}`).style.backgroundColor="red",document.getElementById(`us-progress-info-${n}`).innerText=t,document.getElementById(`us-progress-info-${n}`).classList.add("uploaderror")}function editFile(){const e=document.getElementById("mb_save");e.disabled=!0;let s=e.getAttribute("data-fileid"),o=document.getElementById("mi_edit_down").value,i=document.getElementById("mi_edit_expiry").value,t=document.getElementById("mi_edit_pw").value,a=t==="(unchanged)";document.getElementById("mc_download").checked||(o=0),document.getElementById("mc_expiry").checked||(i=0),document.getElementById("mc_password").checked||(a=!1,t="");let r=!1,n="";document.getElementById("mc_replace").checked&&(n=document.getElementById("mi_edit_replace").value,r=n!=""),apiFilesModify(s,o,i,t,a).then(t=>{if(!r){location.reload();return}apiFilesReplace(s,n).then(e=>{location.reload()}).catch(t=>{alert("Unable to edit file: "+t),console.error("Error:",t),e.disabled=!1})}).catch(t=>{alert("Unable to edit file: "+t),console.error("Error:",t),e.disabled=!1})}function showEditModal(e,t,n,s,o,i,a,r,c){let d=$("#modaledit").clone();$("#modaledit").on("hide.bs.modal",function(){$("#modaledit").remove();let e=d.clone();$("body").append(e)}),document.getElementById("m_filenamelabel").innerText=e,document.getElementById("mc_expiry").setAttribute("data-timestamp",s),document.getElementById("mb_save").setAttribute("data-fileid",t),createCalendar("mi_edit_expiry",s),i?(document.getElementById("mi_edit_down").value="1",document.getElementById("mi_edit_down").disabled=!0,document.getElementById("mc_download").checked=!1):(document.getElementById("mi_edit_down").value=n,document.getElementById("mi_edit_down").disabled=!1,document.getElementById("mc_download").checked=!0),a?(document.getElementById("mi_edit_expiry").value=add14DaysIfBeforeCurrentTime(s),document.getElementById("mi_edit_expiry").disabled=!0,document.getElementById("mc_expiry").checked=!1,calendarInstance._input.disabled=!0):(document.getElementById("mi_edit_expiry").value=s,document.getElementById("mi_edit_expiry").disabled=!1,document.getElementById("mc_expiry").checked=!0,calendarInstance._input.disabled=!1),o?(document.getElementById("mi_edit_pw").value="(unchanged)",document.getElementById("mi_edit_pw").disabled=!1,document.getElementById("mc_password").checked=!0):(document.getElementById("mi_edit_pw").value="",document.getElementById("mi_edit_pw").disabled=!0,document.getElementById("mc_password").checked=!1);let l=document.getElementById("mi_edit_replace");if(c)if(document.getElementById("replaceGroup").style.display="flex",r)document.getElementById("mc_replace").disabled=!0,document.getElementById("mc_replace").title="Replacing content is not available for end-to-end encrypted files",l.add(new Option("Unavailable",0)),l.title="Replacing content is not available for end-to-end encrypted files",l.value="0";else{let e=getAllAvailableFiles();for(let n=0;n{changeRowCount(!1,document.getElementById("row-"+e)),showToastFileDeletion(e),notifyWorker({type:"fileDeleted",id:e})}).catch(e=>{alert("Unable to delete file: "+e),console.error("Error:",e)})}function checkBoxChanged(e,t){let n=!e.checked;n?document.getElementById(t).setAttribute("disabled",""):document.getElementById(t).removeAttribute("disabled"),t==="password"&&n&&(document.getElementById("password").value="")}function parseSseData(e){let t;try{t=JSON.parse(e)}catch(e){console.error("Failed to parse event data:",e);return}switch(t.event){case"download":setNewDownloadCount(t.file_id,t.download_count,t.downloads_remaining);return;case"uploadStatus":parseProgressStatus(t);return;default:console.error("Unknown event",t)}}function setNewDownloadCount(e,t,n){let s=document.getElementById("cell-downloads-"+e);if(s!=null&&(s.innerText=t,s.classList.add("updatedDownloadCount"),setTimeout(()=>s.classList.remove("updatedDownloadCount"),500)),n!=-1){let t=document.getElementById("cell-downloadsRemaining-"+e);t!=null&&(t.innerText=n,t.classList.add("updatedDownloadCount"),setTimeout(()=>t.classList.remove("updatedDownloadCount"),500))}}sseWorkerPort=null;function notifyWorker(e){sseWorkerPort!==null&&sseWorkerPort.postMessage(e)}function registerChangeHandler(){if(typeof SharedWorker!="undefined")try{const e=new SharedWorker("./js/sse-worker.js");e.port.onmessage=e=>{if(e.data.type==="message")parseSseData(e.data.data);else if(e.data.type==="error")console.error("SSE worker connection error:",e.data.detail);else if(e.data.type==="shutdown")setTimeout(function(){window.location.href="./login"},1e3);else if(e.data.type==="fileAdded")document.getElementById("row-"+sanitizeId(e.data.item.Id))==null&&addRow(e.data.item);else if(e.data.type==="fileDeleted"){let t=document.getElementById("row-"+sanitizeId(e.data.id));t!=null&&changeRowCount(!1,t)}else if(e.data.type==="log"){const{level:t,message:n,detail:s}=e.data;s?console[t](n,s):console[t](n)}},e.onerror=e=>{console.warn("SharedWorker failed, falling back to direct SSE:",e),sseWorkerPort=null,_registerDirectSSE()},e.port.start(),sseWorkerPort=e.port;return}catch(e){console.warn("SharedWorker unavailable, falling back to direct SSE:",e)}_registerDirectSSE()}function _registerDirectSSE(){const e=new EventSource("./uploadStatus");e.onmessage=e=>{parseSseData(e.data)},e.onerror=t=>{t.target.readyState!==EventSource.CLOSED&&e.close(),console.log("Reconnecting to SSE (direct)..."),setTimeout(_registerDirectSSE,5e3)}}statusItemCount=0;function addFileStatus(e,t){const n=document.createElement("div");n.setAttribute("id",`us-container-${e}`),n.classList.add("us-container");const a=document.createElement("div");a.classList.add("filename"),a.textContent=t,n.appendChild(a);const s=document.createElement("div");s.classList.add("upload-progress-container"),s.setAttribute("id",`us-progress-container-${e}`);const r=document.createElement("div");r.classList.add("upload-progress-bar");const o=document.createElement("div");o.setAttribute("id",`us-progressbar-${e}`),o.classList.add("upload-progress-bar-progress"),o.style.width="0%",r.appendChild(o);const i=document.createElement("div");i.setAttribute("id",`us-progress-info-${e}`),i.classList.add("upload-progress-info"),i.textContent="0%",s.appendChild(r),s.appendChild(i),n.appendChild(s),n.setAttribute("data-starttime",Date.now()),n.setAttribute("data-complete","false");const c=document.getElementById("uploadstatus");c.appendChild(n),c.style.visibility="visible",statusItemCount++}function removeFileStatus(e){const t=document.getElementById(`us-container-${e}`);if(t==null)return;t.remove(),statusItemCount--,statusItemCount<1&&(document.getElementById("uploadstatus").style.visibility="hidden")}function addRow(e){let d=document.getElementById("downloadtable"),t=d.insertRow(0);e.Id=sanitizeId(e.Id),t.id="row-"+e.Id;let i=t.insertCell(0),a=t.insertCell(1),s=t.insertCell(2),r=t.insertCell(3),c=t.insertCell(4),o=t.insertCell(5),l=t.insertCell(6);i.innerText=e.Name,i.id="cell-name-"+e.Id,c.id="cell-downloads-"+e.Id,a.innerText=e.Size,e.UnlimitedDownloads?s.innerText="Unlimited":(s.innerText=e.DownloadsRemaining,s.id="cell-downloadsRemaining-"+e.Id),e.UnlimitedTime?r.innerText="Unlimited":r.innerText=formatUnixTimestamp(e.ExpireAt),c.innerText=e.DownloadCount;const n=document.createElement("a");if(n.href=e.UrlDownload,n.target="_blank",n.style.color="inherit",n.id="url-href-"+e.Id,n.textContent=e.Id,o.appendChild(n),e.IsPasswordProtected===!0){const e=document.createElement("i");e.className="bi bi-key",e.title="Password protected",o.appendChild(document.createTextNode(" ")),o.appendChild(e)}return l.appendChild(createButtonGroup(e)),i.classList.add("newItem"),a.classList.add("newItem"),s.classList.add("newItem"),r.classList.add("newItem"),c.classList.add("newItem"),o.classList.add("newItem"),l.classList.add("newItem"),a.setAttribute("data-order",e.SizeBytes),changeRowCount(!0,t),e.Id}function createButtonGroup(e){const m=document.createElement("div");m.className="btn-toolbar justify-content-end",m.setAttribute("role","toolbar");const t=document.createElement("div");t.className="btn-group me-2",t.setAttribute("role","group");const n=document.createElement("button");n.type="button",n.className="copyurl btn btn-outline-light btn-sm",n.dataset.clipboardText=e.UrlDownload,n.id="url-button-"+e.Id,n.title="Copy URL";const _=document.createElement("i");_.className="bi bi-copy",n.appendChild(_),n.appendChild(document.createTextNode(" URL")),n.addEventListener("click",()=>{showToast(1e3)}),t.appendChild(n);const f=document.createElement("button");f.type="button",f.className="btn btn-outline-light btn-sm dropdown-toggle dropdown-toggle-split",f.setAttribute("data-bs-toggle","dropdown"),f.setAttribute("aria-expanded","false"),t.appendChild(f);const p=document.createElement("ul");p.className="dropdown-menu dropdown-menu-end",p.setAttribute("data-bs-theme","dark");const b=document.createElement("li"),s=document.createElement("a");e.UrlHotlink!==""?(s.className="dropdown-item copyurl",s.title="Copy hotlink",s.style.cursor="pointer",s.setAttribute("data-clipboard-text",e.UrlHotlink),s.onclick=()=>showToast(1e3),s.innerHTML=` Hotlink`):(s.className="dropdown-item",s.innerText="Hotlink not available"),b.appendChild(s),p.appendChild(b),t.appendChild(p);const d=document.createElement("button");d.type="button",d.className="btn btn-outline-light btn-sm",d.title="Share",d.onclick=()=>shareUrl(event,e.Id),d.innerHTML=` +`),t.scrollTop=t.scrollHeight}function setTrafficInfo(e,n){insertReadableSizeTwoOutputs(e,"totalTraffic","totalTrafficUnit"),document.getElementById("cardTraffic").title=t("logs_traffic_since",formatUnixTimestamp(n))}function setMemoryUsage(e,t){insertReadableSizeTwoOutputs(t,"totalMemory","memoryUnit");let n=document.getElementById("memoryUnit").innerText;insertReadableSizeForcedUnit(e,"usedMemory",n)}function setDiskUsage(e,t){insertReadableSizeTwoOutputs(t,"totalDisk","diskUnit");let n=document.getElementById("diskUnit").innerText;insertReadableSizeForcedUnit(e,"usedDisk",n)}function formatDuration(e){const t=[{label:"y",value:31536e3},{label:"d",value:86400},{label:"h",value:3600},{label:"m",value:60},{label:"s",value:1}];let n=t.findIndex(t=>e>=t.value);(n===-1||t[n].label==="s")&&(n=t.findIndex(e=>e.label==="m"));const s=t[n],o=t[n+1],i=Math.floor(e/s.value),a=e%s.value,r=Math.floor(a/o.value);return`${i}${s.label} ${r}${o.label}`}function addUptime(){if(currentUptime>3600)return;setTimeout(()=>{++currentUptime,document.getElementById("uptime").innerText=formatDuration(currentUptime),addUptime()},1e3)}function setPercentageBar(e,t,n){let o=t;n!==0[0]&&(o=t/n*100);const s=document.getElementById(e);s.classList.remove("bg-success"),s.classList.remove("bg-warning"),s.classList.remove("bg-danger"),o<70&&s.classList.add("bg-success"),o>=70&&o<90&&s.classList.add("bg-warning"),o>=90&&s.classList.add("bg-danger"),s.style.width=o+"%"}async function loadLogs(e){const n=document.getElementById("logviewer");try{const t=await apiLogGet(e);lastLogUpdate=t.timestamp;let s=!0;if(e!=0){if(t.logEntries=="")return;s=allowScroll(),logContent=logContent+t.logEntries}else logContent=t.logEntries;filterLogs(document.getElementById("logFilter").value),s&&(n.scrollTop=n.scrollHeight)}catch(e){lastLogUpdate=0,console.error("Failed to load logs:",e),n.value=t("logs_load_error")}}async function loadStatus(){try{const e=await apiLogSystemStatus();currentUptime=e.uptime,document.getElementById("labelCpu").innerText=e.cpuLoad+"%",document.getElementById("labelActiveFiles").innerText=e.activeFiles,setPercentageBar("barCpu",e.cpuLoad),setPercentageBar("barDisk",e.diskUsagePercentage),setPercentageBar("barMemory",e.memoryUsagePercentage),setMemoryUsage(e.memoryUsed,e.memoryTotal),setDiskUsage(e.diskUsed,e.diskTotal),setTrafficInfo(e.dataServed,e.trafficRecordingSince)}catch(e){console.error("Failed to server status:",e)}}async function pollInfo(){for(firstStart=!0;!0;)await loadLogs(lastLogUpdate),firstStart?firstStart=!1:await loadStatus(),await new Promise(e=>setTimeout(e,POLL_INTERVAL_S*1e3))}function allowScroll(){const e=document.getElementById("logviewer");return e.scrollTop+e.clientHeight>=e.scrollHeight-5}function deleteLogs(){const s=document.getElementById("deleteLogsSel");if(!s)return;const n=s.value;if(n=="none"||n=="")return;if(!confirm(t("logs_confirm_delete"))){document.getElementById("deleteLogs").selectedIndex=0;return}let e=Math.floor(Date.now()/1e3);switch(n){case"all":e=0;break;case"2":e=e-2*24*60*60;break;case"7":e=e-7*24*60*60;break;case"14":e=e-14*24*60*60;break;case"30":e=e-30*24*60*60;break;default:return}apiLogsDelete(e).then(e=>{location.reload()}).catch(e=>{alert(t("error_unable_delete_logs",e)),console.error("Error:",e)})}function resetTrafficStat(){if(!confirm(t("logs_confirm_reset_traffic")))return;apiLogResetTraffic().then(e=>{location.reload()}).catch(e=>{alert(t("error_unable_reset_stats",e)),console.error("Error:",e)})}isE2EEnabled=!1,isUploading=!1,rowCount=-1;function initDropzone(){Dropzone.options.uploaddropzone={paramName:"file",dictDefaultMessage:"",createImageThumbnails:!1,chunksUploaded:function(e,t){sendChunkComplete(e,t)},init:function(){dropzoneObject=this,this.on("addedfile",e=>{e.upload.uuid=getUuid(),saveUploadDefaults(),addFileProgress(e)}),this.on("queuecomplete",function(){isUploading=!1}),this.on("sending",function(){isUploading=!0}),this.on("error",function(e,n,s){if(console.log(n),s){if(s.status===413){showError(e,t("upload_error_too_large"));return}try{console.log(s),errInfo=JSON.parse(s.responseText),showError(e,t("error_generic",errInfo.ErrorMessage))}catch{showError(e,t("error_generic",s.responseText))}}else showError(e,t("error_generic",n))}),this.on("uploadprogress",function(e,t,n){updateProgressbar(e,t,n)}),isE2EEnabled&&(dropzoneObject.disable(),setE2eUpload())}},document.onpaste=function(e){if(dropzoneObject.disabled)return;const n=document.activeElement;if(n&&(n.hasAttribute("data-allow-regular-paste")||n.hasAttribute("placeholder")))return;var t,s=(e.clipboardData||e.originalEvent.clipboardData).items;for(let e in s)t=s[e],t.kind==="file"&&dropzoneObject.addFile(t.getAsFile()),t.kind==="string"&&t.getAsString(function(e){const t=//gi;if(t.test(e)===!1){let t=new Blob([e],{type:"text/plain"}),n=new File([t],"Pasted Text.txt",{type:"text/plain",lastModified:new Date(0)});dropzoneObject.addFile(n)}})},window.addEventListener("beforeunload",e=>{isUploading&&(e.returnValue=t("upload_close_warning"))})}function updateProgressbar(e,t,n){let o=e.upload.uuid,i=document.getElementById(`us-container-${o}`);if(i==null||i.getAttribute("data-complete")==="true")return;let s=Math.round(t);s<0&&(s=0),s>100&&(s=100);let r=Date.now()-i.getAttribute("data-starttime"),c=n/(r/1e3)/1024/1024;document.getElementById(`us-progressbar-${o}`).style.width=s+"%";let a=Math.round(c*10)/10;Number.isNaN(a)||(document.getElementById(`us-progress-info-${o}`).innerText=s+"% - "+a+"MB/s")}function addFileProgress(e){addFileStatus(e.upload.uuid,e.upload.filename)}function setUploadDefaults(){let s=getLocalStorageWithDefault("defaultDownloads",1),o=getLocalStorageWithDefault("defaultExpiry",14),e=getLocalStorageWithDefault("defaultPassword",""),t=getLocalStorageWithDefault("defaultUnlimitedDownloads",!1)==="true",n=getLocalStorageWithDefault("defaultUnlimitedTime",!1)==="true";document.getElementById("allowedDownloads").value=s,document.getElementById("expiryDays").value=o,document.getElementById("password").value=e,document.getElementById("enableDownloadLimit").checked=!t,document.getElementById("enableTimeLimit").checked=!n,e===""?(document.getElementById("enablePassword").checked=!1,document.getElementById("password").disabled=!0):(document.getElementById("enablePassword").checked=!0,document.getElementById("password").disabled=!1),t&&(document.getElementById("allowedDownloads").disabled=!0),n&&(document.getElementById("expiryDays").disabled=!0)}function saveUploadDefaults(){localStorage.setItem("defaultDownloads",document.getElementById("allowedDownloads").value),localStorage.setItem("defaultExpiry",document.getElementById("expiryDays").value),localStorage.setItem("defaultPassword",document.getElementById("password").value),localStorage.setItem("defaultUnlimitedDownloads",!document.getElementById("enableDownloadLimit").checked),localStorage.setItem("defaultUnlimitedTime",!document.getElementById("enableTimeLimit").checked)}function getLocalStorageWithDefault(e,t){var n=localStorage.getItem(e);return n===null?t:n}function urlencodeFormData(e){let t="";function s(e){return encodeURIComponent(e).replace(/%20/g,"+")}for(var n of e.entries())typeof n[1]=="string"&&(t+=(t?"&":"")+s(n[0])+"="+s(n[1]));return t}function sendChunkComplete(e,n){let l=e.upload.uuid,s=e.name,o=e.size,d=e.size,i=e.type,a=document.getElementById("allowedDownloads").value,r=document.getElementById("expiryDays").value,u=document.getElementById("password").value,c=e.isEndToEndEncrypted===!0,h=!0;document.getElementById("enableDownloadLimit").checked||(a=0),document.getElementById("enableTimeLimit").checked||(r=0),c&&(o=e.sizeEncrypted,s="Encrypted File",i=""),apiChunkComplete(l,s,o,d,i,a,r,u,c,h).then(s=>{n();let o=document.getElementById(`us-progress-info-${e.upload.uuid}`);o!=null&&(o.innerText=t("status_in_queue"))}).catch(t=>{console.error("Error:",t),dropzoneUploadError(e,t)})}function dropzoneUploadError(e,t){e.accepted=!1,dropzoneObject._errorProcessing([e],t),showError(e,t)}function dropzoneGetFile(e){for(let t=0;t{addRow(n),notifyWorker({type:"fileAdded",item:n});let s=dropzoneGetFile(t);if(s==null)return;s.isEndToEndEncrypted===!0?apiE2eMutexLockUnlock(!1).then(()=>apiE2eGet()).then(n=>{let i=GokapiE2EInfoParse(n);if(i instanceof Error)throw i;let a=GokapiE2EAddFile(t,e,s.name);if(a instanceof Error)throw a;let o=GokapiE2EInfoEncrypt();if(o instanceof Error)throw o;return apiE2eStore(o)}).then(()=>{GokapiE2EDecryptMenu(),removeFileStatus(t)}).catch(e=>{s.accepted=!1,dropzoneObject._errorProcessing([s],e),console.error("Error:",e)}).finally(()=>{apiE2eMutexLockUnlock(!0).catch(e=>{console.error("Failed to release E2E mutex after write: "+e)})}):removeFileStatus(t)}).catch(e=>{let n=dropzoneGetFile(t);n!=null&&dropzoneUploadError(n,e),console.error("Error:",e)})}function parseProgressStatus(e){let s=document.getElementById(`us-container-${e.chunk_id}`);if(s==null)return;s.setAttribute("data-complete","true");let n;switch(e.upload_status){case 0:n=t("status_processing");break;case 1:n=t("status_saving");break;case 2:n=t("status_finalising"),requestFileInfo(e.file_id,e.chunk_id);break;case 3:n=t("status_error");let s=dropzoneGetFile(e.chunk_id);e.error_message==""&&(e.error_message=t("status_server_error")),s!=null&&dropzoneUploadError(s,e.error_message);return;default:n=t("status_unknown");break}document.getElementById(`us-progress-info-${e.chunk_id}`).innerText=n}function showError(e,t){let n=e.upload.uuid;document.getElementById(`us-progressbar-${n}`).style.width="100%",document.getElementById(`us-progressbar-${n}`).style.backgroundColor="red",document.getElementById(`us-progress-info-${n}`).innerText=t,document.getElementById(`us-progress-info-${n}`).classList.add("uploaderror")}function editFile(){const e=document.getElementById("mb_save");e.disabled=!0;let o=e.getAttribute("data-fileid"),i=document.getElementById("mi_edit_down").value,a=document.getElementById("mi_edit_expiry").value,n=document.getElementById("mi_edit_pw").value,r=n===t("editmodal_password_unchanged");document.getElementById("mc_download").checked||(i=0),document.getElementById("mc_expiry").checked||(a=0),document.getElementById("mc_password").checked||(r=!1,n="");let c=!1,s="";document.getElementById("mc_replace").checked&&(s=document.getElementById("mi_edit_replace").value,c=s!=""),apiFilesModify(o,i,a,n,r).then(n=>{if(!c){location.reload();return}apiFilesReplace(o,s).then(e=>{location.reload()}).catch(n=>{alert(t("error_unable_edit_file",n)),console.error("Error:",n),e.disabled=!1})}).catch(n=>{alert(t("error_unable_edit_file",n)),console.error("Error:",n),e.disabled=!1})}function showEditModal(e,n,s,o,i,a,r,c,l){let u=$("#modaledit").clone();$("#modaledit").on("hide.bs.modal",function(){$("#modaledit").remove();let e=u.clone();$("body").append(e)}),document.getElementById("m_filenamelabel").innerText=e,document.getElementById("mc_expiry").setAttribute("data-timestamp",o),document.getElementById("mb_save").setAttribute("data-fileid",n),createCalendar("mi_edit_expiry",o),a?(document.getElementById("mi_edit_down").value="1",document.getElementById("mi_edit_down").disabled=!0,document.getElementById("mc_download").checked=!1):(document.getElementById("mi_edit_down").value=s,document.getElementById("mi_edit_down").disabled=!1,document.getElementById("mc_download").checked=!0),r?(document.getElementById("mi_edit_expiry").value=add14DaysIfBeforeCurrentTime(o),document.getElementById("mi_edit_expiry").disabled=!0,document.getElementById("mc_expiry").checked=!1,calendarInstance._input.disabled=!0):(document.getElementById("mi_edit_expiry").value=o,document.getElementById("mi_edit_expiry").disabled=!1,document.getElementById("mc_expiry").checked=!0,calendarInstance._input.disabled=!1),i?(document.getElementById("mi_edit_pw").value=t("editmodal_password_unchanged"),document.getElementById("mi_edit_pw").disabled=!1,document.getElementById("mc_password").checked=!0):(document.getElementById("mi_edit_pw").value="",document.getElementById("mi_edit_pw").disabled=!0,document.getElementById("mc_password").checked=!1);let d=document.getElementById("mi_edit_replace");if(l)if(document.getElementById("replaceGroup").style.display="flex",c)document.getElementById("mc_replace").disabled=!0,document.getElementById("mc_replace").title=t("editmodal_replace_e2e_notice"),d.add(new Option(t("editmodal_replace_unavailable"),0)),d.title=t("editmodal_replace_e2e_notice"),d.value="0";else{let e=getAllAvailableFiles();for(let t=0;t{changeRowCount(!1,document.getElementById("row-"+e)),showToastFileDeletion(e),notifyWorker({type:"fileDeleted",id:e})}).catch(e=>{alert(t("error_unable_delete_file",e)),console.error("Error:",e)})}function checkBoxChanged(e,t){let n=!e.checked;n?document.getElementById(t).setAttribute("disabled",""):document.getElementById(t).removeAttribute("disabled"),t==="password"&&n&&(document.getElementById("password").value="")}function parseSseData(e){let t;try{t=JSON.parse(e)}catch(e){console.error("Failed to parse event data:",e);return}switch(t.event){case"download":setNewDownloadCount(t.file_id,t.download_count,t.downloads_remaining);return;case"uploadStatus":parseProgressStatus(t);return;default:console.error("Unknown event",t)}}function setNewDownloadCount(e,t,n){let s=document.getElementById("cell-downloads-"+e);if(s!=null&&(s.innerText=t,s.classList.add("updatedDownloadCount"),setTimeout(()=>s.classList.remove("updatedDownloadCount"),500)),n!=-1){let t=document.getElementById("cell-downloadsRemaining-"+e);t!=null&&(t.innerText=n,t.classList.add("updatedDownloadCount"),setTimeout(()=>t.classList.remove("updatedDownloadCount"),500))}}sseWorkerPort=null;function notifyWorker(e){sseWorkerPort!==null&&sseWorkerPort.postMessage(e)}function registerChangeHandler(){if(typeof SharedWorker!="undefined")try{const e=new SharedWorker("./js/sse-worker.js");e.port.onmessage=e=>{if(e.data.type==="message")parseSseData(e.data.data);else if(e.data.type==="error")console.error("SSE worker connection error:",e.data.detail);else if(e.data.type==="shutdown")setTimeout(function(){window.location.href="./login"},1e3);else if(e.data.type==="fileAdded")document.getElementById("row-"+sanitizeId(e.data.item.Id))==null&&addRow(e.data.item);else if(e.data.type==="fileDeleted"){let t=document.getElementById("row-"+sanitizeId(e.data.id));t!=null&&changeRowCount(!1,t)}else if(e.data.type==="log"){const{level:t,message:n,detail:s}=e.data;s?console[t](n,s):console[t](n)}},e.onerror=e=>{console.warn("SharedWorker failed, falling back to direct SSE:",e),sseWorkerPort=null,_registerDirectSSE()},e.port.start(),sseWorkerPort=e.port;return}catch(e){console.warn("SharedWorker unavailable, falling back to direct SSE:",e)}_registerDirectSSE()}function _registerDirectSSE(){const e=new EventSource("./uploadStatus");e.onmessage=e=>{parseSseData(e.data)},e.onerror=t=>{t.target.readyState!==EventSource.CLOSED&&e.close(),console.log("Reconnecting to SSE (direct)..."),setTimeout(_registerDirectSSE,5e3)}}statusItemCount=0;function addFileStatus(e,t){const n=document.createElement("div");n.setAttribute("id",`us-container-${e}`),n.classList.add("us-container");const a=document.createElement("div");a.classList.add("filename"),a.textContent=t,n.appendChild(a);const s=document.createElement("div");s.classList.add("upload-progress-container"),s.setAttribute("id",`us-progress-container-${e}`);const r=document.createElement("div");r.classList.add("upload-progress-bar");const o=document.createElement("div");o.setAttribute("id",`us-progressbar-${e}`),o.classList.add("upload-progress-bar-progress"),o.style.width="0%",r.appendChild(o);const i=document.createElement("div");i.setAttribute("id",`us-progress-info-${e}`),i.classList.add("upload-progress-info"),i.textContent="0%",s.appendChild(r),s.appendChild(i),n.appendChild(s),n.setAttribute("data-starttime",Date.now()),n.setAttribute("data-complete","false");const c=document.getElementById("uploadstatus");c.appendChild(n),c.style.visibility="visible",statusItemCount++}function removeFileStatus(e){const t=document.getElementById(`us-container-${e}`);if(t==null)return;t.remove(),statusItemCount--,statusItemCount<1&&(document.getElementById("uploadstatus").style.visibility="hidden")}function addRow(e){let u=document.getElementById("downloadtable"),n=u.insertRow(0);e.Id=sanitizeId(e.Id),n.id="row-"+e.Id;let a=n.insertCell(0),r=n.insertCell(1),o=n.insertCell(2),c=n.insertCell(3),l=n.insertCell(4),i=n.insertCell(5),d=n.insertCell(6);a.innerText=e.Name,a.id="cell-name-"+e.Id,l.id="cell-downloads-"+e.Id,r.innerText=e.Size,e.UnlimitedDownloads?o.innerText=t("generic_unlimited"):(o.innerText=e.DownloadsRemaining,o.id="cell-downloadsRemaining-"+e.Id),e.UnlimitedTime?c.innerText=t("generic_unlimited"):c.innerText=formatUnixTimestamp(e.ExpireAt),l.innerText=e.DownloadCount;const s=document.createElement("a");if(s.href=e.UrlDownload,s.target="_blank",s.style.color="inherit",s.id="url-href-"+e.Id,s.textContent=e.Id,i.appendChild(s),e.IsPasswordProtected===!0){const e=document.createElement("i");e.className="bi bi-key",e.title=t("filetable_password_protected"),i.appendChild(document.createTextNode(" ")),i.appendChild(e)}return d.appendChild(createButtonGroup(e)),a.classList.add("newItem"),r.classList.add("newItem"),o.classList.add("newItem"),c.classList.add("newItem"),l.classList.add("newItem"),i.classList.add("newItem"),d.classList.add("newItem"),r.setAttribute("data-order",e.SizeBytes),changeRowCount(!0,n),e.Id}function createButtonGroup(e){const f=document.createElement("div");f.className="btn-toolbar justify-content-end",f.setAttribute("role","toolbar");const n=document.createElement("div");n.className="btn-group me-2",n.setAttribute("role","group");const s=document.createElement("button");s.type="button",s.className="copyurl btn btn-outline-light btn-sm",s.dataset.clipboardText=e.UrlDownload,s.id="url-button-"+e.Id,s.title=t("btn_copy_url");const w=document.createElement("i");w.className="bi bi-copy",s.appendChild(w),s.appendChild(document.createTextNode(" "+t("btn_url"))),s.addEventListener("click",()=>{showToast(1e3)}),n.appendChild(s);const p=document.createElement("button");p.type="button",p.className="btn btn-outline-light btn-sm dropdown-toggle dropdown-toggle-split",p.setAttribute("data-bs-toggle","dropdown"),p.setAttribute("aria-expanded","false"),n.appendChild(p);const g=document.createElement("ul");g.className="dropdown-menu dropdown-menu-end",g.setAttribute("data-bs-theme","dark");const j=document.createElement("li"),o=document.createElement("a");e.UrlHotlink!==""?(o.className="dropdown-item copyurl",o.title=t("btn_copy_hotlink"),o.style.cursor="pointer",o.setAttribute("data-clipboard-text",e.UrlHotlink),o.onclick=()=>showToast(1e3),o.innerHTML=' '+t("btn_hotlink")):(o.className="dropdown-item",o.innerText=t("btn_hotlink_unavailable")),j.appendChild(o),g.appendChild(j),n.appendChild(g);const u=document.createElement("button");u.type="button",u.className="btn btn-outline-light btn-sm",u.title=t("btn_share"),u.onclick=()=>shareUrl(event,e.Id),u.innerHTML=` - `,t.appendChild(d);const u=document.createElement("button");u.type="button",u.className="btn btn-outline-light btn-sm dropdown-toggle dropdown-toggle-split",u.setAttribute("data-bs-toggle","dropdown"),u.setAttribute("aria-expanded","false"),u.id=`shareDropdown-${e.Id}`,t.appendChild(u);const h=document.createElement("ul");h.className="dropdown-menu dropdown-menu-end",h.setAttribute("data-bs-theme","dark");const g=document.createElement("li"),o=document.createElement("a");o.className="dropdown-item",o.id=`qrcode-${e.Id}`,o.style.cursor="pointer",o.title="Open QR Code",o.onclick=()=>showQrCode(e.UrlDownload),o.innerHTML=` QR Code`,g.appendChild(o),h.appendChild(g);const v=document.createElement("li"),r=document.createElement("a");r.className="dropdown-item",r.title="Share via email",r.id=`email-${e.Id}`,r.target="_blank",r.href=`mailto:?body=${encodeURIComponent(e.UrlDownload)}`,r.innerHTML=` Email`,v.appendChild(r),h.appendChild(v),t.appendChild(h);const l=document.createElement("div");l.className="btn-group",l.setAttribute("role","group");const a=document.createElement("button");a.type="button",a.className="btn btn-outline-light btn-sm",a.title="Download",e.RequiresClientSideDecryption&&a.classList.add("disabled");const j=document.createElement("i");j.className="bi bi-download",a.appendChild(j),a.addEventListener("click",()=>{downloadFileWithPresign(e.Id)}),l.appendChild(a);const c=document.createElement("button");c.type="button",c.className="btn btn-outline-light btn-sm",c.title="Edit";const y=document.createElement("i");y.className="bi bi-pencil",c.appendChild(y),c.addEventListener("click",()=>{showEditModal(e.Name,e.Id,e.DownloadsRemaining,e.ExpireAt,e.IsPasswordProtected,e.UnlimitedDownloads,e.UnlimitedTime,e.IsEndToEndEncrypted,canReplaceOwnFiles)}),l.appendChild(c);const i=document.createElement("button");i.type="button",i.className="btn btn-outline-danger btn-sm",i.title="Delete",i.id="button-delete-"+e.Id;const w=document.createElement("i");return w.className="bi bi-trash3",i.appendChild(w),i.addEventListener("click",()=>{deleteFile(e.Id)}),l.appendChild(i),m.appendChild(t),m.appendChild(l),m}function sanitizeId(e){return e.replace(/[^a-zA-Z0-9]/g,"")}function changeRowCount(e,t){let n=$("#maintable").DataTable();rowCount==-1&&(rowCount=n.rows().count()),e?(++rowCount,n.row.add(t)):(--rowCount,t.classList.add("rowDeleting"),setTimeout(()=>{n.row(t).remove(),t.remove()},290));let s=document.getElementsByClassName("dataTables_empty")[0];typeof s!="undefined"?s.innerText="Files stored: "+rowCount:document.getElementsByClassName("dataTables_info")[0].innerText="Files stored: "+rowCount}function hideQrCode(){document.getElementById("qroverlay").style.display="none",document.getElementById("qrcode").innerHTML=""}function showQrCode(e){const t=document.getElementById("qroverlay");t.style.display="block",new QRCode(document.getElementById("qrcode"),{text:e,width:200,height:200,colorDark:"#000000",colorLight:"#ffffff",correctLevel:QRCode.CorrectLevel.H}),t.addEventListener("click",hideQrCode)}function showToastFileDeletion(e){let t=document.getElementById("toastnotificationUndo"),n=document.getElementById("cell-name-"+e).innerText,s=document.getElementById("toastFilename"),o=document.getElementById("toastUndoButton");s.innerText=n,o.dataset.fileid=e,hideToast(),t.classList.add("show"),clearTimeout(toastId),toastId=setTimeout(()=>{hideFileToast()},5e3)}function hideFileToast(){document.getElementById("toastnotificationUndo").classList.remove("show")}function handleUndo(e){hideFileToast(),apiFilesRestore(e.dataset.fileid).then(e=>{addRow(e.FileInfo),notifyWorker({type:"fileAdded",item:e.FileInfo}),isE2EEnabled&&GokapiE2EDecryptMenu()}).catch(e=>{alert("Unable to restore file: "+e),console.error("Error:",e)})}function shareUrl(e,t){if(!navigator.share){e.stopPropagation(),bootstrap.Dropdown.getOrCreateInstance(document.getElementById(`shareDropdown-${t}`)).toggle();return}let n=document.getElementById("cell-name-"+t).innerText,s=document.getElementById("url-href-"+t).getAttribute("href");navigator.share({title:n,url:s})}function showDeprecationNotice(){let e=document.getElementById("toastDeprecation");e.classList.add("show"),setTimeout(()=>{e.classList.remove("show")},5e3)}function changeUserPermission(e,t,n){let s=document.getElementById(n);if(s.classList.contains("perm-processing")||s.classList.contains("perm-nochange"))return;let o=s.classList.contains("perm-granted");s.classList.add("perm-processing"),s.classList.remove("perm-granted"),s.classList.remove("perm-notgranted");let i="GRANT";o&&(i="REVOKE"),t=="PERM_REPLACE_OTHER"&&!o&&(hasNotPermissionReplace=document.getElementById("perm_replace_"+e).classList.contains("perm-notgranted"),hasNotPermissionReplace&&(showToast(2e3,"Also granting permission to replace own files"),changeUserPermission(e,"PERM_REPLACE","perm_replace_"+e))),t=="PERM_REPLACE"&&o&&(hasPermissionReplaceOthers=document.getElementById("perm_replace_other_"+e).classList.contains("perm-granted"),hasPermissionReplaceOthers&&(showToast(2e3,"Also revoking permission to replace files of other users"),changeUserPermission(e,"PERM_REPLACE_OTHER","perm_replace_other_"+e))),apiUserModify(e,t,i).then(e=>{o?s.classList.add("perm-notgranted"):s.classList.add("perm-granted"),s.classList.remove("perm-processing")}).catch(e=>{o?s.classList.add("perm-granted"):s.classList.add("perm-notgranted"),s.classList.remove("perm-processing"),alert("Unable to set permission: "+e),console.error("Error:",e)})}function changeRank(e,t,n){let s=document.getElementById(n);if(s.disabled)return;s.disabled=!0,apiUserChangeRank(e,t).then(e=>{location.reload()}).catch(e=>{s.disabled=!1,alert("Unable to change rank: "+e),console.error("Error:",e)})}function showDeleteUserModal(e,t){let n=document.getElementById("checkboxDelete");n.checked=!1,document.getElementById("deleteModalBody").innerText=t,$("#deleteModal").modal("show"),document.getElementById("buttonDelete").onclick=function(){apiUserDelete(e,n.checked).then(t=>{$("#deleteModal").modal("hide"),document.getElementById("row-"+e).classList.add("rowDeleting"),setTimeout(()=>{document.getElementById("row-"+e).remove()},290)}).catch(e=>{alert("Unable to delete user: "+e),console.error("Error:",e)})}}function showAddUserModal(){let e=$("#newUserModal").clone();$("#newUserModal").on("hide.bs.modal",function(){$("#newUserModal").remove();let t=e.clone();$("body").append(t)}),$("#newUserModal").modal("show")}function showResetPwModal(e,t){let n=$("#resetPasswordModal").clone();$("#resetPasswordModal").on("hide.bs.modal",function(){$("#resetPasswordModal").remove();let e=n.clone();$("body").append(e)}),document.getElementById("l_userpwreset").innerText=t;let s=document.getElementById("resetPasswordButton");s.onclick=function(){resetPw(e,document.getElementById("generateRandomPassword").checked)},$("#resetPasswordModal").modal("show")}function resetPw(e,t){let n=document.getElementById("resetPasswordButton");document.getElementById("resetPasswordButton").disabled=!0,apiUserResetPassword(e,t).then(e=>{if(!t){$("#resetPasswordModal").modal("hide"),showToast(1e3,"Password change requirement set successfully");return}n.style.display="none",document.getElementById("cancelPasswordButton").style.display="none",document.getElementById("formentryReset").style.display="none",document.getElementById("randomPasswordContainer").style.display="block",document.getElementById("closeModalResetPw").style.display="block",document.getElementById("l_returnedPw").innerText=e.password,document.getElementById("copypwclip").onclick=function(){navigator.clipboard.writeText(e.password),showToast(1e3,"Password copied to clipboard")}}).catch(e=>{alert("Unable to reset user password: "+e),console.error("Error:",e),n.disabled=!1})}function addNewUser(){let e=document.getElementById("mb_addUser");e.disabled=!0;let t=document.getElementById("newUserForm");if(t.checkValidity()){let t=document.getElementById("e_userName");apiUserCreate(t.value.trim()).then(e=>{$("#newUserModal").modal("hide"),addRowUser(e.id,e.name,e.permissions),console.log(e)}).catch(t=>{t.message=="duplicate"?(alert("A user already exists with that name"),e.disabled=!1):(alert("Unable to create user: "+t),console.error("Error:",t),e.disabled=!1)})}else t.classList.add("was-validated"),e.disabled=!1}const PermissionDefinitions=[{key:"UserPermGuestUploads",bit:1<<8,icon:"bi bi-box-arrow-in-down",title:"Create file requests",htmlId:e=>`perm_guest_upload_${e}`,apiName:"PERM_GUEST_UPLOAD"},{key:"UserPermReplaceUploads",bit:1<<0,icon:"bi bi-recycle",title:"Replace own uploads",htmlId:e=>`perm_replace_${e}`,apiName:"PERM_REPLACE"},{key:"UserPermListOtherUploads",bit:1<<1,icon:"bi bi-eye",title:"List other uploads",htmlId:e=>`perm_list_${e}`,apiName:"PERM_LIST"},{key:"UserPermEditOtherUploads",bit:1<<2,icon:"bi bi-pencil",title:"Edit other uploads",htmlId:e=>`perm_edit_${e}`,apiName:"PERM_EDIT"},{key:"UserPermDeleteOtherUploads",bit:1<<4,icon:"bi bi-trash3",title:"Delete other uploads",htmlId:e=>`perm_delete_${e}`,apiName:"PERM_DELETE"},{key:"UserPermReplaceOtherUploads",bit:1<<3,icon:"bi bi-arrow-left-right",title:"Replace other uploads",htmlId:e=>`perm_replace_other_${e}`,apiName:"PERM_REPLACE_OTHER"},{key:"UserPermManageLogs",bit:1<<5,icon:"bi bi-card-list",title:"Manage system logs",htmlId:e=>`perm_logs_${e}`,apiName:"PERM_LOGS"},{key:"UserPermManageUsers",bit:1<<7,icon:"bi bi-people",title:"Manage users",htmlId:e=>`perm_users_${e}`,apiName:"PERM_USERS"},{key:"UserPermManageApiKeys",bit:1<<6,icon:"bi bi-sliders2",title:"Manage all API keys",htmlId:e=>`perm_api_${e}`,apiName:"PERM_API"}];function hasPermission(e,t){return(e&t)!==0}function addRowUser(e,t,n){e=sanitizeUserId(e);let m=document.getElementById("usertable"),o=m.insertRow(1);o.id="row-"+e;let c=o.insertCell(0),l=o.insertCell(1),d=o.insertCell(2),u=o.insertCell(3),h=o.insertCell(4),r=o.insertCell(5);c.classList.add("newUser"),l.classList.add("newUser"),d.classList.add("newUser"),u.classList.add("newUser"),h.classList.add("newUser"),r.classList.add("newUser"),c.innerText=t,l.innerText="User",d.innerText="Never",u.innerText="0";const a=document.createElement("div");if(a.className="btn-group",a.setAttribute("role","group"),isInternalAuth){const n=document.createElement("button");n.id=`pwchange-${e}`,n.type="button",n.className="btn btn-outline-light btn-sm",n.title="Reset Password",n.onclick=()=>showResetPwModal(e,t),n.innerHTML=``,a.appendChild(n)}const s=document.createElement("button");s.id=`changeRank_${e}`,s.type="button",s.className="btn btn-outline-light btn-sm",s.title="Promote User",isAdmin?s.onclick=()=>changeRank(e,"ADMIN",`changeRank_${e}`):s.disabled=!0,s.innerHTML=``,a.appendChild(s);const i=document.createElement("button");i.id=`delete-${e}`,i.type="button",i.className="btn btn-outline-danger btn-sm",i.title="Delete",i.onclick=()=>showDeleteUserModal(e,t),i.innerHTML=``,a.appendChild(i),r.innerHTML="",r.appendChild(a),h.innerHTML=PermissionDefinitions.map(t=>{let s="perm-notgranted";hasPermission(n,t.bit)&&(s="perm-granted");const o=t.htmlId(e);let i="";return hasPermission(userPermissions,t.bit)||(i="perm-nochange"),` + `,n.appendChild(u);const h=document.createElement("button");h.type="button",h.className="btn btn-outline-light btn-sm dropdown-toggle dropdown-toggle-split",h.setAttribute("data-bs-toggle","dropdown"),h.setAttribute("aria-expanded","false"),h.id=`shareDropdown-${e.Id}`,n.appendChild(h);const m=document.createElement("ul");m.className="dropdown-menu dropdown-menu-end",m.setAttribute("data-bs-theme","dark");const v=document.createElement("li"),i=document.createElement("a");i.className="dropdown-item",i.id=`qrcode-${e.Id}`,i.style.cursor="pointer",i.title=t("btn_open_qr_code"),i.onclick=()=>showQrCode(e.UrlDownload),i.innerHTML=' '+t("btn_qr_code"),v.appendChild(i),m.appendChild(v);const b=document.createElement("li"),c=document.createElement("a");c.className="dropdown-item",c.title=t("btn_share_email"),c.id=`email-${e.Id}`,c.target="_blank",c.href=`mailto:?body=${encodeURIComponent(e.UrlDownload)}`,c.innerHTML=' '+t("btn_email"),b.appendChild(c),m.appendChild(b),n.appendChild(m);const d=document.createElement("div");d.className="btn-group",d.setAttribute("role","group");const r=document.createElement("button");r.type="button",r.className="btn btn-outline-light btn-sm",r.title=t("btn_download"),e.RequiresClientSideDecryption&&r.classList.add("disabled");const y=document.createElement("i");y.className="bi bi-download",r.appendChild(y),r.addEventListener("click",()=>{downloadFileWithPresign(e.Id)}),d.appendChild(r);const l=document.createElement("button");l.type="button",l.className="btn btn-outline-light btn-sm",l.title=t("btn_edit");const _=document.createElement("i");_.className="bi bi-pencil",l.appendChild(_),l.addEventListener("click",()=>{showEditModal(e.Name,e.Id,e.DownloadsRemaining,e.ExpireAt,e.IsPasswordProtected,e.UnlimitedDownloads,e.UnlimitedTime,e.IsEndToEndEncrypted,canReplaceOwnFiles)}),d.appendChild(l);const a=document.createElement("button");a.type="button",a.className="btn btn-outline-danger btn-sm",a.title=t("btn_delete"),a.id="button-delete-"+e.Id;const O=document.createElement("i");return O.className="bi bi-trash3",a.appendChild(O),a.addEventListener("click",()=>{deleteFile(e.Id)}),d.appendChild(a),f.appendChild(n),f.appendChild(d),f}function sanitizeId(e){return e.replace(/[^a-zA-Z0-9]/g,"")}function changeRowCount(e,n){let s=$("#maintable").DataTable();rowCount==-1&&(rowCount=s.rows().count()),e?(++rowCount,s.row.add(n)):(--rowCount,n.classList.add("rowDeleting"),setTimeout(()=>{s.row(n).remove(),n.remove()},290));let o=document.getElementsByClassName("dataTables_empty")[0];typeof o!="undefined"?o.innerText=t("filetable_files_stored",rowCount):document.getElementsByClassName("dataTables_info")[0].innerText=t("filetable_files_stored",rowCount)}function hideQrCode(){document.getElementById("qroverlay").style.display="none",document.getElementById("qrcode").innerHTML=""}function showQrCode(e){const t=document.getElementById("qroverlay");t.style.display="block",new QRCode(document.getElementById("qrcode"),{text:e,width:200,height:200,colorDark:"#000000",colorLight:"#ffffff",correctLevel:QRCode.CorrectLevel.H}),t.addEventListener("click",hideQrCode)}function showToastFileDeletion(e){let t=document.getElementById("toastnotificationUndo"),n=document.getElementById("cell-name-"+e).innerText,s=document.getElementById("toastFilename"),o=document.getElementById("toastUndoButton");s.innerText=n,o.dataset.fileid=e,hideToast(),t.classList.add("show"),clearTimeout(toastId),toastId=setTimeout(()=>{hideFileToast()},5e3)}function hideFileToast(){document.getElementById("toastnotificationUndo").classList.remove("show")}function handleUndo(e){hideFileToast(),apiFilesRestore(e.dataset.fileid).then(e=>{addRow(e.FileInfo),notifyWorker({type:"fileAdded",item:e.FileInfo}),isE2EEnabled&&GokapiE2EDecryptMenu()}).catch(e=>{alert(t("error_unable_restore_file",e)),console.error("Error:",e)})}function shareUrl(e,t){if(!navigator.share){e.stopPropagation(),bootstrap.Dropdown.getOrCreateInstance(document.getElementById(`shareDropdown-${t}`)).toggle();return}let n=document.getElementById("cell-name-"+t).innerText,s=document.getElementById("url-href-"+t).getAttribute("href");navigator.share({title:n,url:s})}function showDeprecationNotice(){let e=document.getElementById("toastDeprecation");e.classList.add("show"),setTimeout(()=>{e.classList.remove("show")},5e3)}function changeUserPermission(e,n,s){let o=document.getElementById(s);if(o.classList.contains("perm-processing")||o.classList.contains("perm-nochange"))return;let i=o.classList.contains("perm-granted");o.classList.add("perm-processing"),o.classList.remove("perm-granted"),o.classList.remove("perm-notgranted");let a="GRANT";i&&(a="REVOKE"),n=="PERM_REPLACE_OTHER"&&!i&&(hasNotPermissionReplace=document.getElementById("perm_replace_"+e).classList.contains("perm-notgranted"),hasNotPermissionReplace&&(showToast(2e3,t("toast_also_granting_replace")),changeUserPermission(e,"PERM_REPLACE","perm_replace_"+e))),n=="PERM_REPLACE"&&i&&(hasPermissionReplaceOthers=document.getElementById("perm_replace_other_"+e).classList.contains("perm-granted"),hasPermissionReplaceOthers&&(showToast(2e3,t("toast_also_revoking_replace")),changeUserPermission(e,"PERM_REPLACE_OTHER","perm_replace_other_"+e))),apiUserModify(e,n,a).then(e=>{i?o.classList.add("perm-notgranted"):o.classList.add("perm-granted"),o.classList.remove("perm-processing")}).catch(e=>{i?o.classList.add("perm-granted"):o.classList.add("perm-notgranted"),o.classList.remove("perm-processing"),alert(t("error_unable_set_permission",e)),console.error("Error:",e)})}function changeRank(e,n,s){let o=document.getElementById(s);if(o.disabled)return;o.disabled=!0,apiUserChangeRank(e,n).then(e=>{location.reload()}).catch(e=>{o.disabled=!1,alert(t("error_unable_change_rank",e)),console.error("Error:",e)})}function showDeleteUserModal(e,n){let s=document.getElementById("checkboxDelete");s.checked=!1,document.getElementById("deleteModalBody").innerText=n,$("#deleteModal").modal("show"),document.getElementById("buttonDelete").onclick=function(){apiUserDelete(e,s.checked).then(t=>{$("#deleteModal").modal("hide"),document.getElementById("row-"+e).classList.add("rowDeleting"),setTimeout(()=>{document.getElementById("row-"+e).remove()},290)}).catch(e=>{alert(t("error_unable_delete_user",e)),console.error("Error:",e)})}}function showAddUserModal(){let e=$("#newUserModal").clone();$("#newUserModal").on("hide.bs.modal",function(){$("#newUserModal").remove();let t=e.clone();$("body").append(t)}),$("#newUserModal").modal("show")}function showResetPwModal(e,t){let n=$("#resetPasswordModal").clone();$("#resetPasswordModal").on("hide.bs.modal",function(){$("#resetPasswordModal").remove();let e=n.clone();$("body").append(e)}),document.getElementById("l_userpwreset").innerText=t;let s=document.getElementById("resetPasswordButton");s.onclick=function(){resetPw(e,document.getElementById("generateRandomPassword").checked)},$("#resetPasswordModal").modal("show")}function resetPw(e,n){let s=document.getElementById("resetPasswordButton");document.getElementById("resetPasswordButton").disabled=!0,apiUserResetPassword(e,n).then(e=>{if(!n){$("#resetPasswordModal").modal("hide"),showToast(1e3,t("toast_pw_change_required_set"));return}s.style.display="none",document.getElementById("cancelPasswordButton").style.display="none",document.getElementById("formentryReset").style.display="none",document.getElementById("randomPasswordContainer").style.display="block",document.getElementById("closeModalResetPw").style.display="block",document.getElementById("l_returnedPw").innerText=e.password,document.getElementById("copypwclip").onclick=function(){navigator.clipboard.writeText(e.password),showToast(1e3,t("toast_password_copied"))}}).catch(e=>{alert(t("error_unable_reset_password",e)),console.error("Error:",e),s.disabled=!1})}function addNewUser(){let e=document.getElementById("mb_addUser");e.disabled=!0;let n=document.getElementById("newUserForm");if(n.checkValidity()){let n=document.getElementById("e_userName");apiUserCreate(n.value.trim()).then(e=>{$("#newUserModal").modal("hide"),addRowUser(e.id,e.name,e.permissions),console.log(e)}).catch(n=>{n.message=="duplicate"?(alert(t("error_user_exists")),e.disabled=!1):(alert(t("error_unable_create_user",n)),console.error("Error:",n),e.disabled=!1)})}else n.classList.add("was-validated"),e.disabled=!1}const PermissionDefinitions=[{key:"UserPermGuestUploads",bit:1<<8,icon:"bi bi-box-arrow-in-down",title:t("userperm_file_requests"),htmlId:e=>`perm_guest_upload_${e}`,apiName:"PERM_GUEST_UPLOAD"},{key:"UserPermReplaceUploads",bit:1<<0,icon:"bi bi-recycle",title:t("userperm_replace_own"),htmlId:e=>`perm_replace_${e}`,apiName:"PERM_REPLACE"},{key:"UserPermListOtherUploads",bit:1<<1,icon:"bi bi-eye",title:t("userperm_list_other"),htmlId:e=>`perm_list_${e}`,apiName:"PERM_LIST"},{key:"UserPermEditOtherUploads",bit:1<<2,icon:"bi bi-pencil",title:t("userperm_edit_other"),htmlId:e=>`perm_edit_${e}`,apiName:"PERM_EDIT"},{key:"UserPermDeleteOtherUploads",bit:1<<4,icon:"bi bi-trash3",title:t("userperm_delete_other"),htmlId:e=>`perm_delete_${e}`,apiName:"PERM_DELETE"},{key:"UserPermReplaceOtherUploads",bit:1<<3,icon:"bi bi-arrow-left-right",title:t("userperm_replace_other"),htmlId:e=>`perm_replace_other_${e}`,apiName:"PERM_REPLACE_OTHER"},{key:"UserPermManageLogs",bit:1<<5,icon:"bi bi-card-list",title:t("userperm_logs"),htmlId:e=>`perm_logs_${e}`,apiName:"PERM_LOGS"},{key:"UserPermManageUsers",bit:1<<7,icon:"bi bi-people",title:t("userperm_users"),htmlId:e=>`perm_users_${e}`,apiName:"PERM_USERS"},{key:"UserPermManageApiKeys",bit:1<<6,icon:"bi bi-sliders2",title:t("userperm_api"),htmlId:e=>`perm_api_${e}`,apiName:"PERM_API"}];function hasPermission(e,t){return(e&t)!==0}function addRowUser(e,n,s){e=sanitizeUserId(e);let f=document.getElementById("usertable"),i=f.insertRow(1);i.id="row-"+e;let l=i.insertCell(0),d=i.insertCell(1),u=i.insertCell(2),h=i.insertCell(3),m=i.insertCell(4),c=i.insertCell(5);l.classList.add("newUser"),d.classList.add("newUser"),u.classList.add("newUser"),h.classList.add("newUser"),m.classList.add("newUser"),c.classList.add("newUser"),l.innerText=n,d.innerText=t("users_group_user"),u.innerText=t("generic_never"),h.innerText="0";const r=document.createElement("div");if(r.className="btn-group",r.setAttribute("role","group"),isInternalAuth){const s=document.createElement("button");s.id=`pwchange-${e}`,s.type="button",s.className="btn btn-outline-light btn-sm",s.title=t("users_reset_password"),s.onclick=()=>showResetPwModal(e,n),s.innerHTML=``,r.appendChild(s)}const o=document.createElement("button");o.id=`changeRank_${e}`,o.type="button",o.className="btn btn-outline-light btn-sm",o.title=t("users_promote"),isAdmin?o.onclick=()=>changeRank(e,"ADMIN",`changeRank_${e}`):o.disabled=!0,o.innerHTML=``,r.appendChild(o);const a=document.createElement("button");a.id=`delete-${e}`,a.type="button",a.className="btn btn-outline-danger btn-sm",a.title=t("btn_delete"),a.onclick=()=>showDeleteUserModal(e,n),a.innerHTML=``,r.appendChild(a),c.innerHTML="",c.appendChild(r),m.innerHTML=PermissionDefinitions.map(t=>{let n="perm-notgranted";hasPermission(s,t.bit)&&(n="perm-granted");const o=t.htmlId(e);let i="";return hasPermission(userPermissions,t.bit)||(i="perm-nochange"),` - `}).join(""),setTimeout(()=>{c.classList.remove("newUser"),l.classList.remove("newUser"),d.classList.remove("newUser"),u.classList.remove("newUser"),h.classList.remove("newUser"),r.classList.remove("newUser")},700)}function sanitizeUserId(e){const t=e.toString().trim();if(!/^\d+$/.test(t))throw new Error("Invalid ID: must contain only digits.");return t} \ No newline at end of file + `}).join(""),setTimeout(()=>{l.classList.remove("newUser"),d.classList.remove("newUser"),u.classList.remove("newUser"),h.classList.remove("newUser"),m.classList.remove("newUser"),c.classList.remove("newUser")},700)}function sanitizeUserId(e){const n=e.toString().trim();if(!/^\d+$/.test(n))throw new Error(t("error_invalid_id"));return n} \ No newline at end of file diff --git a/internal/webserver/web/static/js/min/all_public.min.js b/internal/webserver/web/static/js/min/all_public.min.js index 8f981639..970f6181 100644 --- a/internal/webserver/web/static/js/min/all_public.min.js +++ b/internal/webserver/web/static/js/min/all_public.min.js @@ -1,2 +1,2 @@ -function getUuid(){if(typeof crypto!="undefined"&&crypto.randomUUID)return crypto.randomUUID();if(typeof crypto!="undefined"&&crypto.getRandomValues){const e=new Uint8Array(16);return crypto.getRandomValues(e),e[6]=e[6]&15|64,e[8]=e[8]&63|128,[...e].map((e,t)=>(t===4||t===6||t===8||t===10?"-":"")+e.toString(16).padStart(2,"0")).join("")}let t="",e;for(e=0;e<36;e++)if(e===8||e===13||e===18||e===23)t+="-";else if(e===14)t+="4";else{const n=Math.random()*16|0;t+=(e===19?n&3|8:n).toString(16)}return t}function formatUnixTimestamp(e){const t=new Date(e*1e3),n=e=>String(e).padStart(2,"0"),s=t.getFullYear(),o=n(t.getMonth()+1),i=n(t.getDate()),a=n(t.getHours()),r=n(t.getMinutes());return`${s}-${o}-${i} ${a}:${r}`}function formatTimestampWithNegative(e,t){return t===0[0]&&(t="Never"),e==0?t:formatUnixTimestamp(e)}function insertFormattedDate(e,t){document.getElementById(t).innerText=formatUnixTimestamp(e)}function insertDateWithNegative(e,t,n){document.getElementById(t).innerText=formatTimestampWithNegative(e,n)}function insertLastOnlineDate(e,t){if(Date.now()/1e3-120e?"Expired":formatUnixTimestamp(e)}function insertFileRequestExpiry(e,t){document.getElementById(t).innerText=formatFileRequestExpiry(e)}function getReadableSize(e){if(!e||e==0)return"0 B";const n=["B","kB","MB","GB","TB"];let t=0;for(;e>=1024&&t>2,l=(3&r)<<4|(s=e.charCodeAt(n++))>>4,i=(15&s)<<2|(o=e.charCodeAt(n++))>>6,t=63&o,isNaN(s)?i=t=64:isNaN(o)&&(t=64),a=a+this._keyStr.charAt(c)+this._keyStr.charAt(l)+this._keyStr.charAt(i)+this._keyStr.charAt(t);return a},decode:function(e){var s,o,i,a,r,c,t="",n=0;for(e=e.replace(/[^A-Za-z0-9+/=]/g,"");n>4,i=(15&r)<<4|(s=this._keyStr.indexOf(e.charAt(n++)))>>2,a=(3&s)<<6|(c=this._keyStr.indexOf(e.charAt(n++))),t+=String.fromCharCode(o),64!=s&&(t+=String.fromCharCode(i)),64!=c&&(t+=String.fromCharCode(a));return t=Base64._utf8_decode(t)},_utf8_encode:function(e){e=e.replace(/\r\n/g,` +function t(t,...n){let e=t;typeof gokapiTranslations!="undefined"&&gokapiTranslations!==null&&Object.prototype.hasOwnProperty.call(gokapiTranslations,t)&&(e=gokapiTranslations[t]);for(let t=0;t(t===4||t===6||t===8||t===10?"-":"")+e.toString(16).padStart(2,"0")).join("")}let t="",e;for(e=0;e<36;e++)if(e===8||e===13||e===18||e===23)t+="-";else if(e===14)t+="4";else{const n=Math.random()*16|0;t+=(e===19?n&3|8:n).toString(16)}return t}function formatUnixTimestamp(e){const t=new Date(e*1e3),n=e=>String(e).padStart(2,"0"),s=t.getFullYear(),o=n(t.getMonth()+1),i=n(t.getDate()),a=n(t.getHours()),r=n(t.getMinutes());return`${s}-${o}-${i} ${a}:${r}`}function formatTimestampWithNegative(e,n){return n===0[0]&&(n=t("generic_never")),e==0?n:formatUnixTimestamp(e)}function insertFormattedDate(e,t){document.getElementById(t).innerText=formatUnixTimestamp(e)}function insertDateWithNegative(e,t,n){document.getElementById(t).innerText=formatTimestampWithNegative(e,n)}function insertLastOnlineDate(e,n){if(Date.now()/1e3-120e?t("generic_expired"):formatUnixTimestamp(e)}function insertFileRequestExpiry(e,t){document.getElementById(t).innerText=formatFileRequestExpiry(e)}function getReadableSize(e){if(!e||e==0)return"0 B";const n=["B","kB","MB","GB","TB"];let t=0;for(;e>=1024&&t>2,l=(3&r)<<4|(s=e.charCodeAt(n++))>>4,i=(15&s)<<2|(o=e.charCodeAt(n++))>>6,t=63&o,isNaN(s)?i=t=64:isNaN(o)&&(t=64),a=a+this._keyStr.charAt(c)+this._keyStr.charAt(l)+this._keyStr.charAt(i)+this._keyStr.charAt(t);return a},decode:function(e){var s,o,i,a,r,c,t="",n=0;for(e=e.replace(/[^A-Za-z0-9+/=]/g,"");n>4,i=(15&r)<<4|(s=this._keyStr.indexOf(e.charAt(n++)))>>2,a=(3&s)<<6|(c=this._keyStr.indexOf(e.charAt(n++))),t+=String.fromCharCode(o),64!=s&&(t+=String.fromCharCode(i)),64!=c&&(t+=String.fromCharCode(a));return t=Base64._utf8_decode(t)},_utf8_encode:function(e){e=e.replace(/\r\n/g,` `);for(var t,n="",s=0;s127&&t<2048?(n+=String.fromCharCode(t>>6|192),n+=String.fromCharCode(63&t|128)):(n+=String.fromCharCode(t>>12|224),n+=String.fromCharCode(t>>6&63|128),n+=String.fromCharCode(63&t|128));return n},_utf8_decode:function(e){for(var s="",t=0,n=c1=c2=0;t191&&n<224?(c2=e.charCodeAt(t+1),s+=String.fromCharCode((31&n)<<6|63&c2),t+=2):(c2=e.charCodeAt(t+1),c3=e.charCodeAt(t+2),s+=String.fromCharCode((15&n)<<12|(63&c2)<<6|63&c3),t+=3);return s}} \ No newline at end of file diff --git a/internal/webserver/web/static/js/min/end2end_admin.min.ccb62a8d5b.js b/internal/webserver/web/static/js/min/end2end_admin.min.ccb62a8d5b.js index db3013fc..876ba581 100644 --- a/internal/webserver/web/static/js/min/end2end_admin.min.ccb62a8d5b.js +++ b/internal/webserver/web/static/js/min/end2end_admin.min.ccb62a8d5b.js @@ -1 +1 @@ -function displayError(e){document.getElementById("errordiv").style.display="block";const t=document.getElementById("errormessage");t.innerText="";const n=document.createElement("b");n.innerText="Error: ";const s=document.createTextNode(e.toString().replace(/^Error:/gi,""));t.appendChild(n),t.appendChild(s),console.error("Caught exception",e)}function checkIfE2EKeyIsSet(){isE2EKeySet()?loadWasm(function(){let t=localStorage.getItem("e2ekey"),e=GokapiE2ESetCipher(t);if(e!==null){displayError(e);return}getE2EInfo(),dropzoneObject.enable(),document.getElementById("uploadBoxTitle").innerText="Drag & drop files here",document.getElementById("uploadBoxSubtitle").innerText="or paste or click to upload with end-to-end encryption"}):window.location="./e2eSetup"}function getE2EInfo(){apiE2eMutexLockUnlock(!1).then(()=>apiE2eGet()).then(e=>{let t=GokapiE2EInfoParse(e);t===null?GokapiE2EDecryptMenu():(displayError(t),t.message==="cipher: message authentication failed"?invalidCipherRedirectConfim():displayError("Trying to get E2E info: "+e))}).catch(e=>{displayError("Trying to get E2E info: "+e)}).finally(()=>{apiE2eMutexLockUnlock(!0).catch(e=>{console.error("Failed to release E2E mutex after read: "+e)})})}function storeE2EInfo(e){apiE2eStore(e).catch(e=>{displayError("Trying to store E2E info: "+e)})}function invalidCipherRedirectConfim(){confirm("It appears that an invalid end-to-end encryption key has been entered. Would you like to enter the correct one?")&&(window.location="./e2eSetup")}function isE2EKeySet(){let e=localStorage.getItem("e2ekey");return e!==null&&e!==""}function loadWasm(e){const t=new Go,s="e2e.wasm?v=1";var n;try{"instantiateStreaming"in WebAssembly?WebAssembly.instantiateStreaming(fetch(s),t.importObject).then(function(s){n=s.instance,t.run(n),e()}):fetch(s).then(e=>e.arrayBuffer()).then(s=>WebAssembly.instantiate(s,t.importObject).then(function(s){n=s.instance,t.run(n),e()}))}catch(e){displayError(e)}}function urlencodeFormData(e){let t="";function s(e){return encodeURIComponent(e).replace(/%20/g,"+")}for(var n of e.entries())typeof n[1]=="string"&&(t+=(t?"&":"")+s(n[0])+"="+s(n[1]));return t}function setE2eUpload(){dropzoneObject.uploadFiles=function(e){this._transformFiles(e,t=>{let o=t[0];e[0].upload.chunked=!0,e[0].isEndToEndEncrypted=!0;let i=e[0].upload.filename,s=o.size,r=0,n=GokapiE2EEncryptNew(e[0].upload.uuid,s,i);if(n instanceof Error){displayError(n);return}e[0].upload.totalChunkCount=Math.ceil(n/this.options.chunkSize),e[0].sizeEncrypted=n;let a=e[0],c=0,l=0,d=!1,u=0;uploadChunk(a,0,n,s,dropzoneObject.options.chunkSize,0)})}}function decryptFileEntries(e){let t=$("#maintable").DataTable();const n=t.rows().nodes();for(let o=0;oshowQrCode(s));let m=document.getElementById("email-"+i);m&&(m.href="mailto:?body="+encodeURIComponent(s))}}async function uploadChunk(e,t,n,s,o,i){let c=!1,l=t*o,d=l+o;t===e.upload.totalChunkCount-1&&(c=!0,d=s);let u=e.webkitSlice?e.webkitSlice(l,d):e.slice(l,d),a=await u.arrayBuffer(),r=await GokapiE2EUploadChunk(e.upload.uuid,a.byteLength,c,new Uint8Array(a));if(r instanceof Error){displayError(a);return}let h=await postChunk(e.upload.uuid,i,n,r,e);if(h!==null){e.accepted=!1,dropzoneObject._errorProcessing([e],h);return}i=i+r.byteLength,a=null,r=null,u=null,c?(e.status=Dropzone.SUCCESS,dropzoneObject.emit("success",e,"success",null),dropzoneObject.emit("complete",e),dropzoneObject.processQueue(),dropzoneObject.options.chunksUploaded(e,()=>{})):await uploadChunk(e,t+1,n,s,o,i)}async function postChunk(e,t,n,s,o){return new Promise(i=>{let r=new FormData;r.append("dztotalfilesize",n),r.append("dzchunkbyteoffset",t),r.append("dzuuid",e),r.append("file",new Blob([s]),"encrypted.file");let a=new XMLHttpRequest;a.open("POST","./uploadChunk");let c=a.upload!=null?a.upload:a;c.onprogress=e=>{try{dropzoneObject.emit("uploadprogress",o,100*(e.loaded+t)/n,e.loaded+t)}catch(e){console.log(e)}},a.onreadystatechange=function(){this.readyState==4&&(this.status==200?i(null):(console.log(a.responseText),i(a.responseText)))},a.send(r)})} \ No newline at end of file +function displayError(e){document.getElementById("errordiv").style.display="block";const n=document.getElementById("errormessage");n.innerText="";const s=document.createElement("b");s.innerText=t("error_prefix");const o=document.createTextNode(e.toString().replace(/^Error:/gi,""));n.appendChild(s),n.appendChild(o),console.error("Caught exception",e)}function checkIfE2EKeyIsSet(){isE2EKeySet()?loadWasm(function(){let n=localStorage.getItem("e2ekey"),e=GokapiE2ESetCipher(n);if(e!==null){displayError(e);return}getE2EInfo(),dropzoneObject.enable(),document.getElementById("uploadBoxTitle").innerText=t("upload_dragdrop"),document.getElementById("uploadBoxSubtitle").innerText=t("upload_dragdrop_sub_e2e")}):window.location="./e2eSetup"}function getE2EInfo(){apiE2eMutexLockUnlock(!1).then(()=>apiE2eGet()).then(e=>{let n=GokapiE2EInfoParse(e);n===null?GokapiE2EDecryptMenu():(displayError(n),n.message==="cipher: message authentication failed"?invalidCipherRedirectConfim():displayError(t("e2e_error_get_info",e)))}).catch(e=>{displayError(t("e2e_error_get_info",e))}).finally(()=>{apiE2eMutexLockUnlock(!0).catch(e=>{console.error("Failed to release E2E mutex after read: "+e)})})}function storeE2EInfo(e){apiE2eStore(e).catch(e=>{displayError(t("e2e_error_store_info",e))})}function invalidCipherRedirectConfim(){confirm(t("e2e_invalid_key_confirm"))&&(window.location="./e2eSetup")}function isE2EKeySet(){let e=localStorage.getItem("e2ekey");return e!==null&&e!==""}function loadWasm(e){const t=new Go,s="e2e.wasm?v=1";var n;try{"instantiateStreaming"in WebAssembly?WebAssembly.instantiateStreaming(fetch(s),t.importObject).then(function(s){n=s.instance,t.run(n),e()}):fetch(s).then(e=>e.arrayBuffer()).then(s=>WebAssembly.instantiate(s,t.importObject).then(function(s){n=s.instance,t.run(n),e()}))}catch(e){displayError(e)}}function urlencodeFormData(e){let t="";function s(e){return encodeURIComponent(e).replace(/%20/g,"+")}for(var n of e.entries())typeof n[1]=="string"&&(t+=(t?"&":"")+s(n[0])+"="+s(n[1]));return t}function setE2eUpload(){dropzoneObject.uploadFiles=function(e){this._transformFiles(e,t=>{let o=t[0];e[0].upload.chunked=!0,e[0].isEndToEndEncrypted=!0;let i=e[0].upload.filename,s=o.size,r=0,n=GokapiE2EEncryptNew(e[0].upload.uuid,s,i);if(n instanceof Error){displayError(n);return}e[0].upload.totalChunkCount=Math.ceil(n/this.options.chunkSize),e[0].sizeEncrypted=n;let a=e[0],c=0,l=0,d=!1,u=0;uploadChunk(a,0,n,s,dropzoneObject.options.chunkSize,0)})}}function decryptFileEntries(e){let t=$("#maintable").DataTable();const n=t.rows().nodes();for(let o=0;oshowQrCode(s));let m=document.getElementById("email-"+i);m&&(m.href="mailto:?body="+encodeURIComponent(s))}}async function uploadChunk(e,t,n,s,o,i){let c=!1,l=t*o,d=l+o;t===e.upload.totalChunkCount-1&&(c=!0,d=s);let u=e.webkitSlice?e.webkitSlice(l,d):e.slice(l,d),a=await u.arrayBuffer(),r=await GokapiE2EUploadChunk(e.upload.uuid,a.byteLength,c,new Uint8Array(a));if(r instanceof Error){displayError(a);return}let h=await postChunk(e.upload.uuid,i,n,r,e);if(h!==null){e.accepted=!1,dropzoneObject._errorProcessing([e],h);return}i=i+r.byteLength,a=null,r=null,u=null,c?(e.status=Dropzone.SUCCESS,dropzoneObject.emit("success",e,"success",null),dropzoneObject.emit("complete",e),dropzoneObject.processQueue(),dropzoneObject.options.chunksUploaded(e,()=>{})):await uploadChunk(e,t+1,n,s,o,i)}async function postChunk(e,t,n,s,o){return new Promise(i=>{let r=new FormData;r.append("dztotalfilesize",n),r.append("dzchunkbyteoffset",t),r.append("dzuuid",e),r.append("file",new Blob([s]),"encrypted.file");let a=new XMLHttpRequest;a.open("POST","./uploadChunk");let c=a.upload!=null?a.upload:a;c.onprogress=e=>{try{dropzoneObject.emit("uploadprogress",o,100*(e.loaded+t)/n,e.loaded+t)}catch(e){console.log(e)}},a.onreadystatechange=function(){this.readyState==4&&(this.status==200?i(null):(console.log(a.responseText),i(a.responseText)))},a.send(r)})} \ No newline at end of file diff --git a/internal/webserver/web/static/js/min/public_upload.min.js b/internal/webserver/web/static/js/min/public_upload.min.js index a339934f..38a53a0f 100644 --- a/internal/webserver/web/static/js/min/public_upload.min.js +++ b/internal/webserver/web/static/js/min/public_upload.min.js @@ -1 +1 @@ -function createUploadBox(){fileInput.addEventListener("change",()=>{Array.from(fileInput.files).forEach(e=>{if(e.size>MAX_FILE_SIZE){document.getElementById("span-modal-error").innerText=`The file "${e.name}" exceeds the maximum allowed size of ${formatSize(MAX_FILE_SIZE)}.`,errorModal.show();return}const n=getUuid(),s=document.createElement("div");s.className="pu-file-item",s.dataset.uuid=n;const a=document.createElement("span");a.textContent=e.name,a.className="file-name";const i=document.createElement("span");i.className="upload-status",i.textContent="Ready";const o=document.createElement("progress");o.className="upload-progress",e.size==0?o.max=1:o.max=e.size,o.value=0;const r=document.createElement("span");r.className="file-size",r.textContent=formatSize(e.size);const t=document.createElement("button");t.type="button",t.title="Remove",t.className="btn btn-sm btn-link text-light p-0",t.innerHTML='',t.onclick=async()=>{filesMap.get(n).removed=!0,filesMap.get(n).status="removed";const e=filesMap.get(n);if(e.controller&&e.controller.abort(),s.remove(),updateUploadButtonState(),e.serverUuid)try{await unreserve(e.serverUuid)}catch(e){console.error("Unreserve failed",e)}},s.append(a,i,o,r,t),fileList.appendChild(s),filesMap.set(n,{uuid:n,file:e,removed:!1,status:"pending",controller:new AbortController,lastSpeed:"",elements:{progressBar:o,progressText:i,removeBtn:t,item:s}}),updateUploadButtonState()}),fileInput.value=""}),["dragenter","dragover","dragleave","drop"].forEach(e=>{uploadBox.addEventListener(e,e=>{e.preventDefault(),e.stopPropagation()},!1)}),["dragenter","dragover"].forEach(e=>{uploadBox.addEventListener(e,()=>uploadBox.classList.add("highlight"),!1)}),["dragleave","drop"].forEach(e=>{uploadBox.addEventListener(e,()=>uploadBox.classList.remove("highlight"),!1)}),uploadBox.addEventListener("drop",e=>{const t=e.dataTransfer,n=t.files;handleFiles(n)}),window.addEventListener("paste",e=>{const t=e.clipboardData.items,n=[];for(let e=0;e{const t=new Blob([e],{type:"text/plain"}),n=new File([t],"pasted-text.txt",{type:"text/plain"});handleFiles([n])});n.length>0&&handleFiles(n)})}function setUnload(){window.addEventListener("beforeunload",e=>{const t=Array.from(filesMap.values()).some(e=>!e.removed);t&&(e.preventDefault(),e.returnValue="")}),window.addEventListener("unload",()=>{for(const e of filesMap.values())!e.removed&&e.serverUuid&&unreserve(e.serverUuid)})}function handleFiles(e){const t=new DataTransfer;Array.from(e).forEach(e=>t.items.add(e)),fileInput.files=t.files,fileInput.dispatchEvent(new Event("change"))}function updateUploadButtonState(){const e=document.getElementById("uploadbutton"),t=Array.from(filesMap.values()).filter(e=>!e.removed&&e.status==="pending");e.disabled=isUploadInProgress||t.length===0}function showModal(e){let t="";switch(e){case"alluploaded":new bootstrap.Modal(document.getElementById("allUploadedModal"),{keyboard:!1,backdrop:"static"}).show();return;case"maxfiles":maxFilesRemaining==1?t="Too many files are selected for upload. Please only select 1 file.":t="Too many files are selected for upload. Please only select "+maxFilesRemaining+" files or fewer.";break;case"maxfilesdynamic":t="Some files could not be uploaded because the server rejected the request. This likely occurred because another user was uploading files at the same time and the maximum file limit was reached.";break;case"expired":t="The upload request exceeded the permitted time limit, and uploading additional files is no longer possible.";break}document.getElementById("span-modal-error").innerText=t,errorModal.show()}function formatSize(e){const n=["B","KB","MB","GB"];let t=0;for(;e>=1024&&tsetTimeout(e,5e3));continue}}if(s&&asetTimeout(e,n));else break}}throw r}function getQueuedFileCount(){let e=0;for(const t of filesMap.values())t.removed||e++;return e}async function initUpload(){const e=document.getElementById("uploadbutton");isUploadInProgress=!0,e.disabled=!0;try{await startUpload()}catch(e){console.error(e)}finally{isUploadInProgress=!1,updateUploadButtonState()}}async function startUpload(){if(!IS_UNLIMITED_FILES&&getQueuedFileCount()>maxFilesRemaining){showModal("maxfiles");return}for(const t of filesMap.values()){if(t.removed||t.status!=="pending")continue;const{file:n,uuid:o,elements:e}=t;t.status="uploading",e.progressBar.style.display="",e.progressText.style.color="";let s="";try{e.progressText.textContent="Reserving...";const a=await reserveChunk(e);t.serverUuid=a,e.removeBtn.innerHTML='',e.removeBtn.title="Cancel Upload";let i=0;do{if(t.controller.signal.aborted)return;const o=n.slice(i,i+CHUNK_SIZE);await withRetry(async()=>new Promise((r,c)=>{const d=new FormData;d.append("file",o),d.append("uuid",a),d.append("filesize",n.size),d.append("offset",i);const l=new XMLHttpRequest;t.xhr=l,l.open("POST",UPLOAD_URL),l.setRequestHeader("apikey",API_KEY),l.setRequestHeader("fileRequestId",FILE_REQUEST_ID);const h=Date.now(),u=()=>{l.abort(),c(new Error("Cancelled"))};t.controller.signal.addEventListener("abort",u),l.upload.onprogress=t=>{if(t.lengthComputable){const o=i+t.loaded,r=n.size===0?1:n.size,c=Math.floor(o/r*100),a=(Date.now()-h)/1e3;a>0&&(s=` (${formatSize(t.loaded/a)}/s)`),e.progressBar.value=o,e.progressText.textContent=c+"%"+s}},l.onload=async()=>{t.controller.signal.removeEventListener("abort",u),l.status>=200&&l.status<300?r():c(await parseXhrError(l))},l.onerror=()=>{const e=new Error(`Server Error`);e.status=l.status,c(e)},l.send(d)}),{signal:t.controller.signal,onWait:()=>{e.progressText.textContent="Waiting for upload slot..."},onRetry:(t,n)=>{e.progressText.textContent=`Retry ${t}/3: ${n.message}${s}`}}),i+=o.size}while(ie.responseText||`HTTP ${e.status}`};return await parseErrorResponse(t)}async function parseErrorResponse(e){const n=await e.text();let t=null;try{t=JSON.parse(n)}catch{}if(t&&t.Result==="error"){let n;switch(t.ErrorCode){case 9:n="File size limit exceeded";break;case 14:n="Upload request has expired",showModal("expired");break;case 15:n="Maximum file count reached",showModal("maxfilesdynamic");break;case 16:n="Too many requests, please try again later";break;default:n=t.ErrorMessage||"Unknown upload error"}const s=new Error(n);return s.status=e.status,s.code=t.ErrorCode,s.raw=t,s}const s=new Error(n||`HTTP ${e.status}`);return s.status=e.status,s}async function reserveChunk(e){return withRetry(async()=>{const e=await fetch(RESERVE_URL,{method:"POST",headers:{id:FILE_REQUEST_ID,apikey:API_KEY}});if(!e.ok)throw await parseErrorResponse(e);const t=await e.json();if(!t.Uuid)throw new Error("Invalid reserve response");return t.Uuid},{onRetry:(t,n)=>{e.progressText.textContent=`Retry ${t}/3: ${n.message}`}})}async function finaliseUpload(e,t,n){await withRetry(async()=>{const n=await fetch(COMPLETE_URL,{method:"POST",headers:{uuid:t,fileRequestId:FILE_REQUEST_ID,filename:encodeFilename(e.name),filesize:e.size,nonblocking:!0,contenttype:e.type||"application/octet-stream",apikey:API_KEY}});if(!n.ok)throw await parseErrorResponse(n)},{onRetry:(e,t)=>{n.progressText.textContent=`Retry ${e}/3: ${t.message}`}})}function encodeFilename(e){return"base64:"+Base64.encode(e)}async function unreserve(e){if(!e)return;try{await fetch(UNRESERVE_URL,{method:"POST",headers:{uuid:e,apikey:API_KEY,id:FILE_REQUEST_ID},keepalive:!0})}catch(e){console.error("Unreserve failed",e)}} \ No newline at end of file +function createUploadBox(){fileInput.addEventListener("change",()=>{Array.from(fileInput.files).forEach(e=>{if(e.size>MAX_FILE_SIZE){document.getElementById("span-modal-error").innerText=t("pu_file_too_large",e.name,formatSize(MAX_FILE_SIZE)),errorModal.show();return}const s=getUuid(),o=document.createElement("div");o.className="pu-file-item",o.dataset.uuid=s;const r=document.createElement("span");r.textContent=e.name,r.className="file-name";const a=document.createElement("span");a.className="upload-status",a.textContent=t("pu_status_ready");const i=document.createElement("progress");i.className="upload-progress",e.size==0?i.max=1:i.max=e.size,i.value=0;const c=document.createElement("span");c.className="file-size",c.textContent=formatSize(e.size);const n=document.createElement("button");n.type="button",n.title=t("btn_remove"),n.className="btn btn-sm btn-link text-light p-0",n.innerHTML='',n.onclick=async()=>{filesMap.get(s).removed=!0,filesMap.get(s).status="removed";const e=filesMap.get(s);if(e.controller&&e.controller.abort(),o.remove(),updateUploadButtonState(),e.serverUuid)try{await unreserve(e.serverUuid)}catch(e){console.error("Unreserve failed",e)}},o.append(r,a,i,c,n),fileList.appendChild(o),filesMap.set(s,{uuid:s,file:e,removed:!1,status:"pending",controller:new AbortController,lastSpeed:"",elements:{progressBar:i,progressText:a,removeBtn:n,item:o}}),updateUploadButtonState()}),fileInput.value=""}),["dragenter","dragover","dragleave","drop"].forEach(e=>{uploadBox.addEventListener(e,e=>{e.preventDefault(),e.stopPropagation()},!1)}),["dragenter","dragover"].forEach(e=>{uploadBox.addEventListener(e,()=>uploadBox.classList.add("highlight"),!1)}),["dragleave","drop"].forEach(e=>{uploadBox.addEventListener(e,()=>uploadBox.classList.remove("highlight"),!1)}),uploadBox.addEventListener("drop",e=>{const t=e.dataTransfer,n=t.files;handleFiles(n)}),window.addEventListener("paste",e=>{const t=e.clipboardData.items,n=[];for(let e=0;e{const t=new Blob([e],{type:"text/plain"}),n=new File([t],"pasted-text.txt",{type:"text/plain"});handleFiles([n])});n.length>0&&handleFiles(n)})}function setUnload(){window.addEventListener("beforeunload",e=>{const t=Array.from(filesMap.values()).some(e=>!e.removed);t&&(e.preventDefault(),e.returnValue="")}),window.addEventListener("unload",()=>{for(const e of filesMap.values())!e.removed&&e.serverUuid&&unreserve(e.serverUuid)})}function handleFiles(e){const t=new DataTransfer;Array.from(e).forEach(e=>t.items.add(e)),fileInput.files=t.files,fileInput.dispatchEvent(new Event("change"))}function updateUploadButtonState(){const e=document.getElementById("uploadbutton"),t=Array.from(filesMap.values()).filter(e=>!e.removed&&e.status==="pending");e.disabled=isUploadInProgress||t.length===0}function showModal(e){let n="";switch(e){case"alluploaded":new bootstrap.Modal(document.getElementById("allUploadedModal"),{keyboard:!1,backdrop:"static"}).show();return;case"maxfiles":maxFilesRemaining==1?n=t("pu_too_many_files_single"):n=t("pu_too_many_files",maxFilesRemaining);break;case"maxfilesdynamic":n=t("pu_max_files_dynamic");break;case"expired":n=t("pu_request_expired");break}document.getElementById("span-modal-error").innerText=n,errorModal.show()}function formatSize(e){const n=["B","KB","MB","GB"];let t=0;for(;e>=1024&&tsetTimeout(e,5e3));continue}}if(s&&asetTimeout(e,n));else break}}throw r}function getQueuedFileCount(){let e=0;for(const t of filesMap.values())t.removed||e++;return e}async function initUpload(){const e=document.getElementById("uploadbutton");isUploadInProgress=!0,e.disabled=!0;try{await startUpload()}catch(e){console.error(e)}finally{isUploadInProgress=!1,updateUploadButtonState()}}async function startUpload(){if(!IS_UNLIMITED_FILES&&getQueuedFileCount()>maxFilesRemaining){showModal("maxfiles");return}for(const n of filesMap.values()){if(n.removed||n.status!=="pending")continue;const{file:s,uuid:i,elements:e}=n;n.status="uploading",e.progressBar.style.display="",e.progressText.style.color="";let o="";try{e.progressText.textContent=t("pu_status_reserving");const r=await reserveChunk(e);n.serverUuid=r,e.removeBtn.innerHTML='',e.removeBtn.title=t("pu_cancel_upload");let a=0;do{if(n.controller.signal.aborted)return;const i=s.slice(a,a+CHUNK_SIZE);await withRetry(async()=>new Promise((c,l)=>{const u=new FormData;u.append("file",i),u.append("uuid",r),u.append("filesize",s.size),u.append("offset",a);const d=new XMLHttpRequest;n.xhr=d,d.open("POST",UPLOAD_URL),d.setRequestHeader("apikey",API_KEY),d.setRequestHeader("fileRequestId",FILE_REQUEST_ID);const m=Date.now(),h=()=>{d.abort(),l(new Error("Cancelled"))};n.controller.signal.addEventListener("abort",h),d.upload.onprogress=t=>{if(t.lengthComputable){const n=a+t.loaded,r=s.size===0?1:s.size,c=Math.floor(n/r*100),i=(Date.now()-m)/1e3;i>0&&(o=` (${formatSize(t.loaded/i)}/s)`),e.progressBar.value=n,e.progressText.textContent=c+"%"+o}},d.onload=async()=>{n.controller.signal.removeEventListener("abort",h),d.status>=200&&d.status<300?c():l(await parseXhrError(d))},d.onerror=()=>{const e=new Error(t("status_server_error"));e.status=d.status,l(e)},d.send(u)}),{signal:n.controller.signal,onWait:()=>{e.progressText.textContent=t("pu_status_waiting_slot")},onRetry:(n,s)=>{e.progressText.textContent=t("pu_status_retry",n,s.message)+o}}),a+=i.size}while(a',e.removeBtn.title=t("pu_remove_from_list")}}}async function parseXhrError(e){const t={ok:!1,status:e.status,text:async()=>e.responseText||`HTTP ${e.status}`};return await parseErrorResponse(t)}async function parseErrorResponse(e){const s=await e.text();let n=null;try{n=JSON.parse(s)}catch{}if(n&&n.Result==="error"){let s;switch(n.ErrorCode){case 9:s=t("pu_err_size_limit");break;case 14:s=t("pu_err_expired"),showModal("expired");break;case 15:s=t("pu_err_max_files"),showModal("maxfilesdynamic");break;case 16:s=t("pu_err_rate_limit");break;default:s=n.ErrorMessage||t("pu_err_unknown")}const o=new Error(s);return o.status=e.status,o.code=n.ErrorCode,o.raw=n,o}const o=new Error(s||`HTTP ${e.status}`);return o.status=e.status,o}async function reserveChunk(e){return withRetry(async()=>{const e=await fetch(RESERVE_URL,{method:"POST",headers:{id:FILE_REQUEST_ID,apikey:API_KEY}});if(!e.ok)throw await parseErrorResponse(e);const n=await e.json();if(!n.Uuid)throw new Error(t("pu_err_invalid_reserve"));return n.Uuid},{onRetry:(n,s)=>{e.progressText.textContent=t("pu_status_retry",n,s.message)}})}async function finaliseUpload(e,n,s){await withRetry(async()=>{const t=await fetch(COMPLETE_URL,{method:"POST",headers:{uuid:n,fileRequestId:FILE_REQUEST_ID,filename:encodeFilename(e.name),filesize:e.size,nonblocking:!0,contenttype:e.type||"application/octet-stream",apikey:API_KEY}});if(!t.ok)throw await parseErrorResponse(t)},{onRetry:(e,n)=>{s.progressText.textContent=t("pu_status_retry",e,n.message)}})}function encodeFilename(e){return"base64:"+Base64.encode(e)}async function unreserve(e){if(!e)return;try{await fetch(UNRESERVE_URL,{method:"POST",headers:{uuid:e,apikey:API_KEY,id:FILE_REQUEST_ID},keepalive:!0})}catch(e){console.error("Unreserve failed",e)}} \ No newline at end of file diff --git a/internal/webserver/web/static/js/public_upload.js b/internal/webserver/web/static/js/public_upload.js index af728bfb..1546c922 100644 --- a/internal/webserver/web/static/js/public_upload.js +++ b/internal/webserver/web/static/js/public_upload.js @@ -5,7 +5,7 @@ function createUploadBox() { if (file.size > MAX_FILE_SIZE) { document.getElementById('span-modal-error').innerText = - `The file "${file.name}" exceeds the maximum allowed size of ${formatSize(MAX_FILE_SIZE)}.`; + t("pu_file_too_large", file.name, formatSize(MAX_FILE_SIZE)); errorModal.show(); return; } @@ -21,7 +21,7 @@ function createUploadBox() { const progressText = document.createElement('span'); progressText.className = 'upload-status'; - progressText.textContent = 'Ready'; + progressText.textContent = t("pu_status_ready"); const progressBar = document.createElement('progress'); progressBar.className = 'upload-progress'; @@ -39,7 +39,7 @@ function createUploadBox() { const removeBtn = document.createElement('button'); removeBtn.type = 'button'; - removeBtn.title = 'Remove'; + removeBtn.title = t("btn_remove"); removeBtn.className = 'btn btn-sm btn-link text-light p-0'; removeBtn.innerHTML = ''; removeBtn.onclick = async () => { @@ -197,18 +197,18 @@ function showModal(modalCode) { case "maxfiles": if (maxFilesRemaining == 1) { - message = "Too many files are selected for upload. Please only select 1 file."; + message = t("pu_too_many_files_single"); } else { - message = "Too many files are selected for upload. Please only select " + maxFilesRemaining + " files or fewer."; + message = t("pu_too_many_files", maxFilesRemaining); } break; case "maxfilesdynamic": - message = "Some files could not be uploaded because the server rejected the request. This likely occurred because another user was uploading files at the same time and the maximum file limit was reached."; + message = t("pu_max_files_dynamic"); break; case "expired": - message = "The upload request exceeded the permitted time limit, and uploading additional files is no longer possible."; + message = t("pu_request_expired"); break; } document.getElementById('span-modal-error').innerText = message; @@ -323,12 +323,12 @@ async function startUpload() { let lastSpeedText = ""; try { - elements.progressText.textContent = "Reserving..."; + elements.progressText.textContent = t("pu_status_reserving"); const serverUuid = await reserveChunk(elements); entry.serverUuid = serverUuid; elements.removeBtn.innerHTML = ''; - elements.removeBtn.title = "Cancel Upload"; + elements.removeBtn.title = t("pu_cancel_upload"); let offset = 0; // do-while so that add chunk is run for 0byte files as well @@ -383,7 +383,7 @@ async function startUpload() { }; xhr.onerror = () => { - const err = new Error(`Server Error`); + const err = new Error(t("status_server_error")); err.status = xhr.status; reject(err); }; @@ -393,10 +393,10 @@ async function startUpload() { }, { signal: entry.controller.signal, onWait: () => { - elements.progressText.textContent = "Waiting for upload slot..."; + elements.progressText.textContent = t("pu_status_waiting_slot"); }, onRetry: (a, e) => { - elements.progressText.textContent = `Retry ${a}/3: ${e.message}${lastSpeedText}`; + elements.progressText.textContent = t("pu_status_retry", a, e.message) + lastSpeedText; } }); @@ -406,7 +406,7 @@ async function startUpload() { await finaliseUpload(file, serverUuid, elements); entry.status = 'completed'; - elements.progressText.textContent = "Completed"; + elements.progressText.textContent = t("pu_status_completed"); elements.item.style.opacity = "0.6"; elements.removeBtn.remove(); // Remove button only on success @@ -422,12 +422,12 @@ async function startUpload() { } entry.status = 'error'; - elements.progressText.textContent = err.message || "Upload failed"; + elements.progressText.textContent = err.message || t("pu_upload_failed"); elements.progressText.style.color = "#ff6b6b"; elements.progressBar.style.display = "none"; elements.removeBtn.innerHTML = ''; - elements.removeBtn.title = "Remove from list"; + elements.removeBtn.title = t("pu_remove_from_list"); } } } @@ -455,21 +455,21 @@ async function parseErrorResponse(response) { let message; switch (data.ErrorCode) { case 9: - message = "File size limit exceeded"; + message = t("pu_err_size_limit"); break; case 14: - message = "Upload request has expired"; + message = t("pu_err_expired"); showModal("expired"); break; case 15: - message = "Maximum file count reached"; + message = t("pu_err_max_files"); showModal("maxfilesdynamic"); break; case 16: - message = "Too many requests, please try again later"; + message = t("pu_err_rate_limit"); break; default: - message = data.ErrorMessage || "Unknown upload error"; + message = data.ErrorMessage || t("pu_err_unknown"); } const err = new Error(message); err.status = response.status; @@ -496,11 +496,11 @@ async function reserveChunk(elements) { throw await parseErrorResponse(response); } const data = await response.json(); - if (!data.Uuid) throw new Error("Invalid reserve response"); + if (!data.Uuid) throw new Error(t("pu_err_invalid_reserve")); return data.Uuid; }, { onRetry: (a, e) => { - elements.progressText.textContent = `Retry ${a}/3: ${e.message}`; + elements.progressText.textContent = t("pu_status_retry", a, e.message); } }); } @@ -524,7 +524,7 @@ async function finaliseUpload(file, uuid, elements) { } }, { onRetry: (a, e) => { - elements.progressText.textContent = `Retry ${a}/3: ${e.message}`; + elements.progressText.textContent = t("pu_status_retry", a, e.message); } }); } diff --git a/internal/webserver/web/templates/expired_file_svg.tmpl b/internal/webserver/web/templates/expired_file_svg.tmpl index 3a977af5..bb967d2a 100644 --- a/internal/webserver/web/templates/expired_file_svg.tmpl +++ b/internal/webserver/web/templates/expired_file_svg.tmpl @@ -1,5 +1,5 @@ {{.PublicName}} - The requested file has expired + {{.ExpiredText}} diff --git a/internal/webserver/web/templates/html_admin.tmpl b/internal/webserver/web/templates/html_admin.tmpl index ae4d9f9b..d0f90c01 100644 --- a/internal/webserver/web/templates/html_admin.tmpl +++ b/internal/webserver/web/templates/html_admin.tmpl @@ -1,29 +1,29 @@ {{ define "admin" }}{{ template "header" . }} - +
-

Upload

+

{{ .Lang.T "upload_title" }}


- +

{{ if .EndToEndEncryption }} -

Please wait

-

Loading end-to-end encryption...

+

{{ .Lang.T "upload_please_wait" }}

+

{{ .Lang.T "upload_loading_e2e" }}

{{ else }} -

Drag & drop files here

-

or paste or click to select.

+

{{ .Lang.T "upload_dragdrop" }}

+

{{ .Lang.T "upload_dragdrop_sub" }}

{{ end }}
- +
-
+

- +
@@ -32,20 +32,20 @@
- Download Limit + {{ .Lang.T "upload_option_download_limit" }}
+ aria-label="{{ .Lang.T "upload_option_download_limit_enable" }}">
- Downloads + {{ .Lang.T "upload_option_downloads" }}
@@ -53,20 +53,20 @@
- Expiry + {{ .Lang.T "upload_option_expiry" }}
+ aria-label="{{ .Lang.T "upload_option_time_limit_enable" }}">
- Days + {{ .Lang.T "upload_option_days" }}
@@ -74,17 +74,17 @@
- Password + {{ .Lang.T "upload_option_password" }}
+ aria-label="{{ .Lang.T "upload_option_password_enable" }}">
@@ -97,46 +97,46 @@ - - - - - - - + + + + + + + {{ range .Items }} - {{ if not (or .IsPendingDeletion .IsFileRequest) }} + {{ if not (or .IsPendingDeletion .IsFileRequest) }} {{ if or (gt .ExpireAt $.TimeNow) (.UnlimitedTime) }} {{ if or (gt .DownloadsRemaining 0) (.UnlimitedDownloads) }} {{ if .UnlimitedDownloads }} - + {{ else }} {{ end }} {{ if .UnlimitedTime }} - + {{ else }} {{ end }} - + @@ -151,17 +151,17 @@ - - {{ template "admin_modal_edit" }} - -
Toast Text
+ {{ template "admin_modal_edit" . }} + + +
Toast Text
- File deleted: - Restore + {{ .Lang.T "toast_file_deleted" }} + {{ .Lang.T "toast_restore" }}
- Warning! This server is using a deprecated feature. Detailed information can be found in the system logs. + {{ .Lang.T "toast_deprecation" }}
@@ -169,7 +169,7 @@ @@ -217,27 +229,27 @@ {{ end }} - - + + {{ template "pagename" "FileRequest"}} {{ template "customjs" .}} -{{ template "footer" true}} +{{ template "footer_version" . }} {{ end }} @@ -245,7 +257,7 @@ {{ define "admin_button_download" }} + data-clipboard-text="{{ .CurrentFile.UrlDownload }}" class="copyurl btn btn-outline-light btn-sm" title="{{ .Lang.T "btn_copy_url" }}"> {{ .Lang.T "btn_url" }} {{ end }} {{ define "admin_button_share" }} - - + +
diff --git a/internal/webserver/web/templates/html_api.tmpl b/internal/webserver/web/templates/html_api.tmpl index 6053cef6..f7249aae 100644 --- a/internal/webserver/web/templates/html_api.tmpl +++ b/internal/webserver/web/templates/html_api.tmpl @@ -8,7 +8,7 @@
-

API Keys

+

{{ .Lang.T "api_title" }}

- - Please visit the API documentation for more information about the API.
Click on the API key name to give it a new name. Permissions can be changed by clicking on them. + + {{ .Lang.H "api_hint" }}

FilenameSizeDownloads remainingStored untilDownloadsIDActions{{ .Lang.T "filetable_filename" }}{{ .Lang.T "filetable_size" }}{{ .Lang.T "filetable_downloads_remaining" }}{{ .Lang.T "filetable_stored_until" }}{{ .Lang.T "filetable_downloads" }}{{ .Lang.T "filetable_id" }}{{ .Lang.T "filetable_actions" }}
{{ .Name }} {{ .Size }}Unlimited{{ $.Lang.T "generic_unlimited" }}{{ .DownloadsRemaining }}Unlimited{{ $.Lang.T "generic_unlimited" }} {{ .DownloadCount }}{{ .Id }}{{ if .IsPasswordProtected }} {{ end }}{{ .Id }}{{ if .IsPasswordProtected }} {{ end }}
- - - - + + + {{ if .ActiveUser.HasPermissionManageApi }} - + {{ end }} - + @@ -71,32 +71,32 @@ {{ if $.ActiveUser.HasPermissionManageApi }} {{ end }} - + {{ end }} @@ -104,7 +104,7 @@ -
Toast Text
+
Toast Text
@@ -124,9 +124,9 @@ })(); - + {{ template "pagename" "ApiOverview"}} {{ template "customjs" .}} -{{ template "footer" true }} +{{ template "footer_version" . }} {{ end }} diff --git a/internal/webserver/web/templates/html_changepw.tmpl b/internal/webserver/web/templates/html_changepw.tmpl index 475df2a1..d865ab8b 100644 --- a/internal/webserver/web/templates/html_changepw.tmpl +++ b/internal/webserver/web/templates/html_changepw.tmpl @@ -1,21 +1,21 @@ -{{define "changepw"}}{{ template "header" . }} +{{define "changepw"}}{{ template "header" . }}
-

Change Password

+

{{ .Lang.T "changepw_title" }}


-

A password change has been requested.
Please enter a new password.

+

{{ .Lang.H "changepw_text" }}

-
-
@@ -25,12 +25,12 @@ {{ end }}
@@ -70,5 +70,5 @@ function submitForm() { {{ template "pagename" "ChangePw"}} {{ template "customjs" .}} -{{ template "footer" }} +{{ template "footer" . }} {{end}} diff --git a/internal/webserver/web/templates/html_download.tmpl b/internal/webserver/web/templates/html_download.tmpl index 49f6dc1a..85675a33 100644 --- a/internal/webserver/web/templates/html_download.tmpl +++ b/internal/webserver/web/templates/html_download.tmpl @@ -3,7 +3,7 @@
- +
{{ if .EndToEndEncryption }} @@ -17,9 +17,9 @@
{{ if .EndToEndEncryption }} -

Decrypting...

+

{{ .Lang.T "download_decrypting" }}

- Encrypted + {{ .Lang.T "download_encrypted" }}
{{ else }}

{{ .Name }}

@@ -27,7 +27,7 @@
  • - Size + {{ .Lang.T "download_size" }} {{ .Size }}
@@ -36,11 +36,11 @@ {{ if .ClientSideDecryption }} {{ else }} {{ end }}
@@ -59,12 +59,12 @@ - + {{ else }} - {{ end }} {{ if .EndToEndEncryption }} - {{ end }} - + {{ template "pagename" "PublicDownload"}} {{ template "customjs" .}} - -{{template "footer"}} + +{{template "footer" . }} {{end}} diff --git a/internal/webserver/web/templates/html_download_password.tmpl b/internal/webserver/web/templates/html_download_password.tmpl index 77a01be2..9c67c4be 100644 --- a/internal/webserver/web/templates/html_download_password.tmpl +++ b/internal/webserver/web/templates/html_download_password.tmpl @@ -1,25 +1,25 @@ {{define "download_password"}}{{template "header" .}} - + {{ if .EndToEndEncryption }} {{ end }} - +
-

Password required

+

{{ .Lang.T "downloadpw_title" }}

-
+
{{ if .IsFailedLogin }} -
Incorrect password!
+
{{ .Lang.T "downloadpw_incorrect" }}
{{ end }} -
+
@@ -36,5 +36,5 @@ function submitForm(){ {{ template "pagename" "PublicDownloadPw"}} {{ template "customjs" .}} -{{template "footer"}} +{{template "footer" . }} {{end}} diff --git a/internal/webserver/web/templates/html_end2end.tmpl b/internal/webserver/web/templates/html_end2end.tmpl index 1ee52c63..2c831d86 100644 --- a/internal/webserver/web/templates/html_end2end.tmpl +++ b/internal/webserver/web/templates/html_end2end.tmpl @@ -1,37 +1,37 @@ {{define "e2esetup"}}{{template "header" .}} - + - -{{ if not .HasBeenSetup }} + +{{ if not .HasBeenSetup }}
-

End-to-End Encryption Setup

+

{{ .Lang.T "e2e_title" }}



-

Your password for decryption is:
- Generating.......

+

{{ .Lang.T "e2e_password_intro" }}
+ {{ .Lang.T "e2e_generating" }}

- Save this password to a secure location, without it you will not be able to decrypt/share your files if your browser data gets deleted or you login from a different machine! This password will only be shown once. -

If you need to reset the password, run the Gokapi setup again. -

+ {{ .Lang.T "e2e_password_warning" }} +

{{ .Lang.T "e2e_reset_hint" }} +

- - + + - -{{ else }} + +{{ else }}
-

End-to-End Encryption Setup

+

{{ .Lang.T "e2e_title" }}



-

End-to-end encryption has been set up, however no key was found on the local machine. Please enter the password in the text field below. If you do not know the decryption password, please re-run the Gokapi setup to reset the password.

+

{{ .Lang.T "e2e_existing_setup" }}


-

+

- - + + {{ end }} {{ template "pagename" "E2EGeneration"}} {{ template "customjs" .}} -{{template "footer" true}} +{{template "footer_version" . }} {{end}} diff --git a/internal/webserver/web/templates/html_error.tmpl b/internal/webserver/web/templates/html_error.tmpl index a7b10f89..45bd70a5 100644 --- a/internal/webserver/web/templates/html_error.tmpl +++ b/internal/webserver/web/templates/html_error.tmpl @@ -1,5 +1,5 @@ {{define "error"}}{{template "header" .}} - +
@@ -8,54 +8,53 @@ {{ if eq .ErrorId 0 }}

- File not found + {{ .Lang.T "errorpage_file_not_found_title" }}


- The link may have expired or the file has been downloaded too many times. + {{ .Lang.T "errorpage_file_not_found_text" }} {{ end }} - + {{ if eq .ErrorId 1 }}

- Unable to upload files + {{ .Lang.T "errorpage_upload_title" }}


- This can happen for one of the following reasons: + {{ .Lang.T "errorpage_upload_text" }}
    -
  • - The upload request has expired (time limit reached)
  • -
  • - The file limit for this upload request has been reached
  • -
  • - An invalid upload URL was submitted
  • +
  • - {{ .Lang.T "errorpage_upload_reason_expired" }}
  • +
  • - {{ .Lang.T "errorpage_upload_reason_limit" }}
  • +
  • - {{ .Lang.T "errorpage_upload_reason_url" }}
{{ end }} - + {{ if eq .ErrorId 2 }}

- Missing or invalid decryption key + {{ .Lang.T "errorpage_decryption_title" }}


- This file is encrypted, but no key was provided or the key is invalid.

- Please contact the uploader and make sure the complete URL is used, including the value after the hash. + {{ .Lang.H "errorpage_decryption_text" }} {{ end }} - + {{ if eq .ErrorId 3 }} -

Unauthorised user

+

{{ .Lang.T "errorpage_unauthorised_title" }}


-

Login with OAuth provider was sucessful, however this user is not authorised to use Gokapi.



- Log in as different user +

{{ .Lang.T "errorpage_unauthorised_text" }}



+ {{ .Lang.T "errorpage_login_other_user" }} {{ end }} - + {{ else }} - + {{ if eq .ErrorId 4 }} -

OIDC Provider Error: {{.ErrorTitle}}

+

{{ .Lang.Tf "errorpage_oidc_title" .ErrorTitle }}


-

Login with OAuth provider was not sucessful, the following error was raised:

+

{{ .Lang.T "errorpage_oidc_text" }}

{{ if .ErrorOauthMessage }}

{{ .ErrorOauthMessage }}

{{ end}}

{{ .ErrorMessage }}


- Try again + {{ .Lang.T "errorpage_try_again" }} {{ else }} - +

{{ .ErrorTitle }}

@@ -74,5 +73,5 @@
{{ template "pagename" "PublicError"}} {{ template "customjs" .}} -{{template "footer"}} +{{template "footer" . }} {{end}} diff --git a/internal/webserver/web/templates/html_footer.tmpl b/internal/webserver/web/templates/html_footer.tmpl index 7522079d..1e199989 100644 --- a/internal/webserver/web/templates/html_footer.tmpl +++ b/internal/webserver/web/templates/html_footer.tmpl @@ -1,8 +1,22 @@ +{{/* Both footer templates expect the current view as their argument, so that + the text can be translated. "footer_version" additionally displays the + Gokapi version and is used on the pages that require a login. */}} {{define "footer"}} +
+ + +{{end}} + +{{define "footer_version"}} + + +
diff --git a/internal/webserver/web/templates/html_forgotpw.tmpl b/internal/webserver/web/templates/html_forgotpw.tmpl index 8dd82d35..d7e8a5f8 100644 --- a/internal/webserver/web/templates/html_forgotpw.tmpl +++ b/internal/webserver/web/templates/html_forgotpw.tmpl @@ -1,12 +1,12 @@ -{{define "forgotpw"}}{{ template "header" . }} +{{define "forgotpw"}}{{ template "header" . }}
-

Forgot password

+

{{ .Lang.T "forgotpw_title" }}


- If you forgot your user password, please ask your administrator to reset it.

To reset an administrator password, restart the server with the argument --reconfigure and change it in the authentication section. + {{ .Lang.H "forgotpw_text" }}


@@ -14,5 +14,5 @@
{{ template "pagename" "ForgotPw"}} {{ template "customjs" .}} -{{ template "footer" }} +{{ template "footer" . }} {{end}} diff --git a/internal/webserver/web/templates/html_header.tmpl b/internal/webserver/web/templates/html_header.tmpl index 8818c2ad..953e62c4 100644 --- a/internal/webserver/web/templates/html_header.tmpl +++ b/internal/webserver/web/templates/html_header.tmpl @@ -1,10 +1,11 @@ {{define "header"}} - + + @@ -16,7 +17,7 @@ {{ if .IsAdminView }} - {{.PublicName}} Admin + {{.PublicName}} {{ .Lang.T "nav_admin_title" }} @@ -40,17 +41,17 @@ {{ else }} {{ if .IsDownloadView }} {{ if .IsPasswordView }} - {{.PublicName}}: Password required + {{.PublicName}}: {{ .Lang.T "downloadpw_title" }} - - + + - + {{ else }} {{.PublicName}}: {{.Name}} - + {{end }} @@ -66,37 +67,41 @@ {{ if .IsAdminView }} +
{{ if .IsUserTabAvailable }} -
{{.ActiveUser.Name}} -
{{ end }} + {{ template "language_selector" . }} +

{{.PublicName}}

{{ else }} +
+ {{ template "language_selector" . }} +
@@ -107,3 +112,16 @@ {{ end }} {{end}} +{{define "language_selector"}} +{{ if gt (len .Lang.Available) 1 }} + +{{ end }} +{{end}} diff --git a/internal/webserver/web/templates/html_login.tmpl b/internal/webserver/web/templates/html_login.tmpl index 6156db6f..86cbbebe 100644 --- a/internal/webserver/web/templates/html_login.tmpl +++ b/internal/webserver/web/templates/html_login.tmpl @@ -1,45 +1,45 @@ -{{define "login"}}{{ template "header" . }} +{{define "login"}}{{ template "header" . }}
-

Login

+

{{ .Lang.T "login_title" }}


- +
- +
{{ if .IsFailedLogin }} {{ if .IsFailedCsfr }}
- The login page was open too long and expired. Please try again. + {{ .Lang.T "login_expired" }}
{{ else }}
- Incorrect username or password! + {{ .Lang.T "login_incorrect" }}
{{ end }} {{ end }}


- Forgot password

+ {{ .Lang.T "login_forgot_password" }}

@@ -56,5 +56,5 @@ function submitForm(){ {{ template "pagename" "Login"}} {{ template "customjs" .}} -{{ template "footer" }} +{{ template "footer" . }} {{end}} diff --git a/internal/webserver/web/templates/html_logs.tmpl b/internal/webserver/web/templates/html_logs.tmpl index 7af5cbc5..4051dd21 100644 --- a/internal/webserver/web/templates/html_logs.tmpl +++ b/internal/webserver/web/templates/html_logs.tmpl @@ -1,12 +1,12 @@ {{ define "logs" }}{{ template "header" . }}
- +
-
Uptime
+
{{ .Lang.T "logs_uptime" }}
@@ -15,7 +15,7 @@
-
CPU Load
+
{{ .Lang.T "logs_cpu_load" }}
{{ .CpuLoad}}%
@@ -27,7 +27,7 @@
-
Memory Usage
+
{{ .Lang.T "logs_memory_usage" }}
/
@@ -39,7 +39,7 @@
-
Disk Usage
+
{{ .Lang.T "logs_disk_usage" }}
/
@@ -51,9 +51,9 @@
-
Data Served
+
{{ .Lang.T "logs_data_served" }}
- +
@@ -63,8 +63,8 @@
-
Uploaded Files
-
{{ .TotalFiles }} Total
+
{{ .Lang.T "logs_uploaded_files" }}
+
{{ .TotalFiles }} {{ .Lang.T "generic_total" }}
@@ -73,18 +73,18 @@
-
System Logs
+
{{ .Lang.T "logs_system_logs" }}
- +
-
@@ -92,40 +92,40 @@
- Filter + {{ .Lang.T "logs_filter" }}
- +
{{ if or (eq .ActiveUser.UserLevel 0) (eq .ActiveUser.UserLevel 1) }} - - +
- + {{ end }}
@@ -137,13 +137,13 @@ {{ template "pagename" "LogOverview"}} {{ template "customjs" .}} -{{ template "footer" true}} +{{ template "footer_version" . }} {{ end }} diff --git a/internal/webserver/web/templates/html_public_upload.tmpl b/internal/webserver/web/templates/html_public_upload.tmpl index 68f5f44e..0d856fc6 100644 --- a/internal/webserver/web/templates/html_public_upload.tmpl +++ b/internal/webserver/web/templates/html_public_upload.tmpl @@ -6,19 +6,19 @@
-

Upload Files

+

{{ .Lang.T "pu_title" }}

{{ if ne .FileRequest.Notes "" }}
-
Note
+
{{ .Lang.T "pu_note" }}

{{.FileRequest.Notes}}

{{ end }} {{ if .FileRequest.HasRestrictions }}
-
Upload restrictions
+
{{ .Lang.T "pu_restrictions" }}
    {{ if not (.FileRequest.IsUnlimitedTime) }} -
  • Upload possible until: +
  • {{ .Lang.T "pu_restriction_until" }} {{ .FileRequest.Expiry }}
  • @@ -27,7 +27,7 @@ {{ end }} {{ if or (not (.FileRequest.IsUnlimitedSize)) (lt .FileRequest.CombinedMaxSize 5000) }} -
  • Maximum file size: +
  • {{ .Lang.T "pu_restriction_max_size" }}
  • @@ -36,21 +36,21 @@ {{ end }} {{ if not (.FileRequest.IsUnlimitedFiles) }} -
  • Maximum number of files: {{ .FileRequest.FilesRemaining }} +
  • {{ .Lang.T "pu_restriction_max_files" }} {{ .FileRequest.FilesRemaining }}
  • {{ end }}
{{ end }}
@@ -65,13 +65,13 @@ @@ -82,10 +82,10 @@ - +
NameAPI KeyLast UsedPermissions + {{ .Lang.T "api_col_name" }}{{ .Lang.T "api_col_key" }}{{ .Lang.T "api_col_last_used" }}{{ .Lang.T "api_col_permissions" }} User{{ .Lang.T "api_col_user" }}Actions{{ .Lang.T "api_col_actions" }}
- - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + {{(index $.UserMap .UserId).Name}}
- - - - - + + + + + {{ if .ActiveUser.HasPermissionListOtherUploads }} - + {{ end }} - + - + {{ range $fileRequest := .FileRequests }} - - {{ template "uRFileCell" . }} - + + {{ template "uRFileCell" (newFileRequestContext . $.Lang) }} + - + - - + + {{ if $.ActiveUser.HasPermissionListOtherUploads }} - + {{ end }} - - + {{ end }} diff --git a/internal/webserver/web/templates/html_users.tmpl b/internal/webserver/web/templates/html_users.tmpl index ffac71cc..6061c062 100644 --- a/internal/webserver/web/templates/html_users.tmpl +++ b/internal/webserver/web/templates/html_users.tmpl @@ -9,7 +9,7 @@
-

Users

+

{{ .Lang.T "users_title" }}

NameUploaded FilesTotal SizeLast UploadExpiry{{ .Lang.T "fr_col_name" }}{{ .Lang.T "fr_col_uploaded_files" }}{{ .Lang.T "fr_col_total_size" }}{{ .Lang.T "fr_col_last_upload" }}{{ .Lang.T "fr_col_expiry" }}User{{ .Lang.T "fr_col_user" }}Actions{{ .Lang.T "fr_col_actions" }}
{{ .Name }}{{ .GetReadableTotalSize }}{{ .Name }}{{ .GetReadableTotalSize }} {{(index $.UserMap .UserId).Name}}{{(index $.UserMap .UserId).Name}} - +
+
@@ -88,13 +88,13 @@ - +
-
Toast Text
+
Toast Text
- File deleted: - Restore + {{ .Lang.T "toast_file_deleted" }} + {{ .Lang.T "toast_restore" }}
@@ -130,18 +130,18 @@ var canViewOtherRequests = {{.ActiveUser.HasPermissionListOtherUploads}}; var limitMaxSize = {{.FileRequestMaxSize}}; var limitMaxFiles = {{.FileRequestMaxFiles}}; - + -{{ template "urequest_modal_confirm" }} -{{ template "urequest_modal_addedit" }} - +{{ template "urequest_modal_confirm" . }} +{{ template "urequest_modal_addedit" . }} + {{ template "pagename" "UploadRequest"}} {{ template "customjs" .}} -{{ template "footer" true }} +{{ template "footer_version" . }} {{ end }} @@ -151,15 +151,14 @@ @@ -172,7 +171,7 @@ @@ -237,13 +236,13 @@ {{ define "uRDownloadbutton" }} - {{ if eq .UploadedFiles 0 }} - + {{ if eq .Request.UploadedFiles 0 }} + {{ else }} - {{ if eq .UploadedFiles 1 }} - + {{ if eq .Request.UploadedFiles 1 }} + {{ else }} - + {{ end }} {{ end }} {{ end }} @@ -251,17 +250,17 @@ {{ define "uRFileCell" }}
- {{ .UploadedFiles }}{{ if ne .ReservedUploads 0 }}+{{.ReservedUploads}}{{end}}{{ if ne .MaxFiles 0 }} / {{ .MaxFiles }}{{end}} - {{ if gt .UploadedFiles 0 }} + {{ .Request.UploadedFiles }}{{ if ne .Request.ReservedUploads 0 }}+{{.Request.ReservedUploads}}{{end}}{{ if ne .Request.MaxFiles 0 }} / {{ .Request.MaxFiles }}{{end}} + {{ if gt .Request.UploadedFiles 0 }} {{end}} -
- - - - - + + + + - + {{ range .Users }} - + {{ end }} @@ -112,119 +112,118 @@ -
Toast Text
- +
Toast Text
+ - - - + + + - - + +
UserGroupLast onlineUploadsPermissions + {{ .Lang.T "users_col_user" }}{{ .Lang.T "users_col_group" }}{{ .Lang.T "users_col_last_online" }}{{ .Lang.T "users_col_uploads" }}{{ .Lang.T "users_col_permissions" }} Actions{{ .Lang.T "users_col_actions" }}
{{ .User.Name }}{{ .User.GetReadableUserLevel }}{{ if eq .User.UserLevel 0 }}{{ $.Lang.T "userlevel_super_admin" }}{{ else if eq .User.UserLevel 1 }}{{ $.Lang.T "userlevel_admin" }}{{ else if eq .User.UserLevel 2 }}{{ $.Lang.T "userlevel_user" }}{{ else }}{{ $.Lang.T "userlevel_invalid" }}{{ end }} {{ .UploadCount }} - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + +
{{if $.IsInternalAuth}} - + {{end}} - + {{if not .User.IsAdmin}} - {{ else }} - + {{ end }} - - - + + +