From c71f0426ae2ec2c5609fb9677d97f13dfdce59d1 Mon Sep 17 00:00:00 2001 From: Nicola Murino Date: Sun, 10 Dec 2023 16:40:13 +0100 Subject: [PATCH] WebClient WIP: add support for localizations Signed-off-by: Nicola Murino --- .github/workflows/codeql.yml | 2 +- .github/workflows/development.yml | 8 +- .github/workflows/release.yml | 4 +- go.mod | 81 +- go.sum | 176 +- internal/common/connection.go | 2 +- internal/dataprovider/dataprovider.go | 129 +- internal/dataprovider/share.go | 71 +- internal/httpd/api_http_user.go | 16 +- internal/httpd/api_shares.go | 36 +- internal/httpd/api_utils.go | 41 +- internal/httpd/handler.go | 8 +- internal/httpd/httpd_test.go | 223 +- internal/httpd/internal_test.go | 63 +- internal/httpd/middleware.go | 26 +- internal/httpd/oidc.go | 4 +- internal/httpd/oidc_test.go | 4 +- internal/httpd/server.go | 86 +- internal/httpd/web.go | 37 +- internal/httpd/webadmin.go | 2 - internal/httpd/webclient.go | 364 ++- internal/util/i18n.go | 171 ++ internal/util/util.go | 2 +- static/locales/en/translation.json | 370 +++ static/locales/it/translation.json | 370 +++ static/vendor/i18next/i18next.js | 2297 +++++++++++++++++ .../i18next/i18nextBrowserLanguageDetector.js | 420 +++ .../vendor/i18next/i18nextChainedBackend.js | 266 ++ static/vendor/i18next/i18nextHttpBackend.js | 414 +++ .../i18next/i18nextLocalStorageBackend.js | 190 ++ static/vendor/i18next/jquery-i18next.js | 128 + templates/common/base.html | 92 +- templates/webadmin/login.html | 2 +- templates/webadmin/twofactor-recovery.html | 2 +- templates/webadmin/twofactor.html | 2 +- templates/webclient/base.html | 41 +- templates/webclient/baselogin.html | 15 +- templates/webclient/changepassword.html | 32 +- templates/webclient/editfile.html | 59 +- templates/webclient/files.html | 408 +-- templates/webclient/forgot-password.html | 15 +- templates/webclient/login.html | 17 +- templates/webclient/message.html | 7 +- templates/webclient/mfa.html | 216 +- templates/webclient/profile.html | 31 +- templates/webclient/reset-password.html | 19 +- templates/webclient/share.html | 70 +- templates/webclient/sharedownload.html | 6 +- templates/webclient/sharelogin.html | 9 +- templates/webclient/shares.html | 135 +- templates/webclient/shareupload.html | 39 +- templates/webclient/twofactor-recovery.html | 13 +- templates/webclient/twofactor.html | 17 +- templates/webclient/viewpdf.html | 2 +- 54 files changed, 6160 insertions(+), 1100 deletions(-) create mode 100644 internal/util/i18n.go create mode 100644 static/locales/en/translation.json create mode 100644 static/locales/it/translation.json create mode 100644 static/vendor/i18next/i18next.js create mode 100644 static/vendor/i18next/i18nextBrowserLanguageDetector.js create mode 100644 static/vendor/i18next/i18nextChainedBackend.js create mode 100644 static/vendor/i18next/i18nextHttpBackend.js create mode 100644 static/vendor/i18next/i18nextLocalStorageBackend.js create mode 100644 static/vendor/i18next/jquery-i18next.js diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 25f018b6..f774ee34 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -20,7 +20,7 @@ jobs: fetch-depth: 0 - name: Set up Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: go-version: '1.21' diff --git a/.github/workflows/development.yml b/.github/workflows/development.yml index 23845b44..bd0d47d1 100644 --- a/.github/workflows/development.yml +++ b/.github/workflows/development.yml @@ -25,7 +25,7 @@ jobs: fetch-depth: 0 - name: Set up Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: go-version: ${{ matrix.go }} @@ -230,7 +230,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: go-version: '1.21' @@ -294,7 +294,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: go-version: '1.21' @@ -515,7 +515,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Set up Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: go-version: '1.21' - uses: actions/checkout@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7dbdd036..e8f610d9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,7 +14,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: go-version: ${{ env.GO_VERSION }} @@ -48,7 +48,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: go-version: ${{ env.GO_VERSION }} diff --git a/go.mod b/go.mod index 941192e3..9e0ee00c 100644 --- a/go.mod +++ b/go.mod @@ -9,37 +9,37 @@ require ( github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5 github.com/alexedwards/argon2id v1.0.0 github.com/amoghe/go-crypt v0.0.0-20220222110647-20eada5f5964 - github.com/aws/aws-sdk-go-v2 v1.23.1 - github.com/aws/aws-sdk-go-v2/config v1.25.6 - github.com/aws/aws-sdk-go-v2/credentials v1.16.5 - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.5 - github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.14.4 - github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.18.4 - github.com/aws/aws-sdk-go-v2/service/s3 v1.45.1 - github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.24.1 - github.com/aws/aws-sdk-go-v2/service/sts v1.25.5 + github.com/aws/aws-sdk-go-v2 v1.24.0 + github.com/aws/aws-sdk-go-v2/config v1.26.1 + github.com/aws/aws-sdk-go-v2/credentials v1.16.12 + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.10 + github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.15.7 + github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.19.5 + github.com/aws/aws-sdk-go-v2/service/s3 v1.47.5 + github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.25.5 + github.com/aws/aws-sdk-go-v2/service/sts v1.26.5 github.com/bmatcuk/doublestar/v4 v4.6.1 github.com/cockroachdb/cockroach-go/v2 v2.3.5 - github.com/coreos/go-oidc/v3 v3.8.0 + github.com/coreos/go-oidc/v3 v3.9.0 github.com/drakkan/webdav v0.0.0-20230227175313-32996838bcd8 github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001 github.com/fclairamb/ftpserverlib v0.22.0 github.com/fclairamb/go-log v0.4.1 github.com/go-acme/lego/v4 v4.14.2 github.com/go-chi/chi/v5 v5.0.10 - github.com/go-chi/jwtauth/v5 v5.2.0 + github.com/go-chi/jwtauth/v5 v5.3.0 github.com/go-chi/render v1.0.3 github.com/go-sql-driver/mysql v1.7.1 github.com/golang/mock v1.6.0 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/google/uuid v1.4.0 - github.com/hashicorp/go-hclog v1.5.0 + github.com/hashicorp/go-hclog v1.6.1 github.com/hashicorp/go-plugin v1.6.0 github.com/hashicorp/go-retryablehttp v0.7.5 - github.com/jackc/pgx/v5 v5.5.0 + github.com/jackc/pgx/v5 v5.5.1 github.com/jlaffaye/ftp v0.0.0-20201112195030-9aae4d151126 - github.com/klauspost/compress v1.17.3 - github.com/lestrrat-go/jwx/v2 v2.0.17 + github.com/klauspost/compress v1.17.4 + github.com/lestrrat-go/jwx/v2 v2.0.18 github.com/lithammer/shortuuid/v3 v3.0.7 github.com/mattn/go-sqlite3 v1.14.18 github.com/mhale/smtpd v0.8.0 @@ -54,10 +54,10 @@ require ( github.com/rs/xid v1.5.0 github.com/rs/zerolog v1.31.0 github.com/sftpgo/sdk v0.1.6-0.20231105181545-b44c8058fc25 - github.com/shirou/gopsutil/v3 v3.23.10 + github.com/shirou/gopsutil/v3 v3.23.11 github.com/spf13/afero v1.11.0 github.com/spf13/cobra v1.8.0 - github.com/spf13/viper v1.17.0 + github.com/spf13/viper v1.18.1 github.com/stretchr/testify v1.8.4 github.com/studio-b12/gowebdav v0.9.0 github.com/subosito/gotenv v1.6.0 @@ -67,36 +67,36 @@ require ( github.com/yl2chen/cidranger v1.0.3-0.20210928021809-d1cb2c52f37a go.etcd.io/bbolt v1.3.8 go.uber.org/automaxprocs v1.5.3 - gocloud.dev v0.34.0 + gocloud.dev v0.35.0 golang.org/x/crypto v0.16.0 golang.org/x/net v0.19.0 golang.org/x/oauth2 v0.15.0 golang.org/x/sys v0.15.0 golang.org/x/term v0.15.0 golang.org/x/time v0.5.0 - google.golang.org/api v0.152.0 + google.golang.org/api v0.153.0 gopkg.in/natefinch/lumberjack.v2 v2.2.1 ) require ( - cloud.google.com/go v0.110.10 // indirect + cloud.google.com/go v0.111.0 // indirect cloud.google.com/go/compute v1.23.3 // indirect cloud.google.com/go/compute/metadata v0.2.3 // indirect cloud.google.com/go/iam v1.1.5 // indirect - github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1 // indirect github.com/ajg/form v1.5.1 // indirect - github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.1 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.4 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.4 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.7.1 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.4 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.1 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.4 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.4 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.4 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.17.4 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.20.2 // indirect - github.com/aws/smithy-go v1.17.0 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.9 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.9 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.9 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.9 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.9 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.9 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.18.5 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.5 // indirect + github.com/aws/smithy-go v1.19.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/boombuler/barcode v1.0.1 // indirect github.com/cenkalti/backoff/v4 v4.2.1 // indirect @@ -108,6 +108,8 @@ require ( github.com/fatih/color v1.16.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-jose/go-jose/v3 v3.0.1 // indirect + github.com/go-logr/logr v1.3.0 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect @@ -120,7 +122,7 @@ require ( github.com/hashicorp/yamux v0.1.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect - github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/kr/fs v0.1.0 // indirect @@ -145,19 +147,22 @@ require ( github.com/prometheus/common v0.45.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/sagikazarmark/locafero v0.3.0 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/segmentio/asm v1.2.0 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/cast v1.6.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/tklauser/go-sysconf v0.3.12 // indirect - github.com/tklauser/numcpus v0.6.1 // indirect + github.com/tklauser/go-sysconf v0.3.13 // indirect + github.com/tklauser/numcpus v0.7.0 // indirect github.com/yusufpapurcu/wmi v1.2.3 // indirect go.opencensus.io v0.24.0 // indirect + go.opentelemetry.io/otel v1.21.0 // indirect + go.opentelemetry.io/otel/metric v1.21.0 // indirect + go.opentelemetry.io/otel/trace v1.21.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/exp v0.0.0-20231127185646-65229373498e // indirect + golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb // indirect golang.org/x/mod v0.14.0 // indirect golang.org/x/sync v0.5.0 // indirect golang.org/x/text v0.14.0 // indirect diff --git a/go.sum b/go.sum index 46b89463..566000c9 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,6 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.110.10 h1:LXy9GEO+timppncPIAZoOj3l58LIU9k+kn48AN7IO3Y= -cloud.google.com/go v0.110.10/go.mod h1:v1OoFqYxiBkUrruItNM3eT4lLByNjxmJSV/xDKJNnic= +cloud.google.com/go v0.111.0 h1:YHLKNupSD1KqjDbQ3+LVdQ81h/UJbJyZG203cEfnQgM= +cloud.google.com/go v0.111.0/go.mod h1:0mibmpKP1TyOOFYQY5izo0LnT+ecvOQ0Sg3OdmMiNRU= cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk= cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI= cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= @@ -14,16 +14,16 @@ cloud.google.com/go/storage v1.35.1/go.mod h1:M6M/3V/D3KpzMTJyPOR/HU6n2Si5QdaXYE github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.0 h1:fb8kj/Dh4CSwgsOzHeZY4Xh68cFVbzXx+ONXGMY//4w= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.0/go.mod h1:uReU2sSxZExRPBAg3qKzmAucSi51+SP1OhohieR821Q= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0 h1:vcYCAze6p19qBW7MhZybIsqD8sMV8js0NyQM8JDnVtg= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0/go.mod h1:OQeznEEkTZ9OrhHJoDD8ZDq51FHgXjqtP9z6bEwBq9U= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.0 h1:d81/ng9rET2YqdVkVwkb6EXeRrLJIwyGnJcAlAWKwhs= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.0/go.mod h1:s4kgfzA0covAXNicZHDMN58jExvcng2mC/DepXiF1EI= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0 h1:BMAjVKJM0U/CYF27gA0ZMmXGkOcvfFtD0oHVZ1TIPRI= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0/go.mod h1:1fXstnBMas5kzG+S3q8UoJcmyU6nUeunJcMDHcRYHhs= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1 h1:6oNBlSdi1QqM1PNW7FPA6xOGA5UNsXnkaYZz9vdPGhA= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1/go.mod h1:s4kgfzA0covAXNicZHDMN58jExvcng2mC/DepXiF1EI= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.2.0 h1:Ma67P/GGprNwsslzEH6+Kb8nybI8jpDTm4Wmzu2ReK8= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.2.0/go.mod h1:c+Lifp3EDEamAkPVzMooRNOK6CZjNSdEnf1A7jsI9u4= github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.2.0 h1:gggzg0SUMs6SQbEw+3LoSsYf9YMjkupeAnHMX8O9mmY= github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.2.0/go.mod h1:+6KLcKIVgxoBDMqMO/Nvy7bZ9a0nbU3I1DtFQK3YvB4= -github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0 h1:OBhqkivkhkMqLPymWEppkm7vgPQY2XsHoEkaMQ0AdZY= -github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0/go.mod h1:kgDmCTgBzIEPFElEF+FK0SdjAor06dRq2Go927dnQ6o= +github.com/AzureAD/microsoft-authentication-library-for-go v1.2.0 h1:hVeq+yCyUi+MsoO/CU95yqCIcdzra5ovzk8Q2BBpV2M= +github.com/AzureAD/microsoft-authentication-library-for-go v1.2.0/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5 h1:IEjq88XO4PuBDcvmjQJcQGg+w+UaafSy8G5Kcb5tBhI= github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5/go.mod h1:exZ0C/1emQJAw5tHOaUDyY1ycttqBAPcxuzf7QbY6ec= @@ -33,48 +33,48 @@ github.com/alexedwards/argon2id v1.0.0 h1:wJzDx66hqWX7siL/SRUmgz3F8YMrd/nfX/xHHc github.com/alexedwards/argon2id v1.0.0/go.mod h1:tYKkqIjzXvZdzPvADMWOEZ+l6+BD6CtBXMj5fnJppiw= github.com/amoghe/go-crypt v0.0.0-20220222110647-20eada5f5964 h1:I9YN9WMo3SUh7p/4wKeNvD/IQla3U3SUa61U7ul+xM4= github.com/amoghe/go-crypt v0.0.0-20220222110647-20eada5f5964/go.mod h1:eFiR01PwTcpbzXtdMces7zxg6utvFM5puiWHpWB8D/k= -github.com/aws/aws-sdk-go-v2 v1.23.1 h1:qXaFsOOMA+HsZtX8WoCa+gJnbyW7qyFFBlPqvTSzbaI= -github.com/aws/aws-sdk-go-v2 v1.23.1/go.mod h1:i1XDttT4rnf6vxc9AuskLc6s7XBee8rlLilKlc03uAA= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.1 h1:ZY3108YtBNq96jNZTICHxN1gSBSbnvIdYwwqnvCV4Mc= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.1/go.mod h1:t8PYl/6LzdAqsU4/9tz28V/kU+asFePvpOMkdul0gEQ= -github.com/aws/aws-sdk-go-v2/config v1.25.6 h1:p7b0sR6lHVNNOK/dE4xZgq2R+NNFRjtAXy8WNE6jbpo= -github.com/aws/aws-sdk-go-v2/config v1.25.6/go.mod h1:E/nt0ERX9ZX2RCcJWBax94jFn738UERvjSn4R3msEeQ= -github.com/aws/aws-sdk-go-v2/credentials v1.16.5 h1:oJz7X2VzKl8Y9pX7Fa5sIy4+3OnknF+Ne0KYu7DCoQQ= -github.com/aws/aws-sdk-go-v2/credentials v1.16.5/go.mod h1:2HvVzcP9ih6XR66omXIsgWjtolkL0MlQVqPcK3nXK+E= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.5 h1:KehRNiVzIfAcj6gw98zotVbb/K67taJE0fkfgM6vzqU= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.5/go.mod h1:VhnExhw6uXy9QzetvpXDolo1/hjhx4u9qukBGkuUwjs= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.14.4 h1:021nsPdnrpRJJHF4fAJb/uOTYeC1UHaWEtjAp7QLQmc= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.14.4/go.mod h1:tb4mNW+u7WObIYuOj5rqjo5rZTkSQI677lhuPl1SLtA= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.4 h1:LAm3Ycm9HJfbSCd5I+wqC2S9Ej7FPrgr5CQoOljJZcE= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.4/go.mod h1:xEhvbJcyUf/31yfGSQBe01fukXwXJ0gxDp7rLfymWE0= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.4 h1:4GV0kKZzUxiWxSVpn/9gwR0g21NF1Jsyduzo9rHgC/Q= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.4/go.mod h1:dYvTNAggxDZy6y1AF7YDwXsPuHFy/VNEpEI/2dWK9IU= -github.com/aws/aws-sdk-go-v2/internal/ini v1.7.1 h1:uR9lXYjdPX0xY+NhvaJ4dD8rpSRz5VY81ccIIoNG+lw= -github.com/aws/aws-sdk-go-v2/internal/ini v1.7.1/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.4 h1:40Q4X5ebZruRtknEZH/bg91sT5pR853F7/1X9QRbI54= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.4/go.mod h1:u77N7eEECzUv7F0xl2gcfK/vzc8wcjWobpy+DcrLJ5E= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.1 h1:rpkF4n0CyFcrJUG/rNNohoTmhtWlFTRI4BsZOh9PvLs= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.1/go.mod h1:l9ymW25HOqymeU2m1gbUQ3rUIsTwKs8gYHXkqDQUhiI= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.4 h1:6DRKQc+9cChgzL5gplRGusI5dBGeiEod4m/pmGbcX48= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.4/go.mod h1:s8ORvrW4g4v7IvYKIAoBg17w3GQ+XuwXDXYrQ5SkzU0= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.4 h1:rdovz3rEu0vZKbzoMYPTehp0E8veoE9AyfzqCr5Eeao= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.4/go.mod h1:aYCGNjyUCUelhofxlZyj63srdxWUSsBSGg5l6MCuXuE= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.4 h1:o3DcfCxGDIT20pTbVKVhp3vWXOj/VvgazNJvumWeYW0= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.4/go.mod h1:Uy0KVOxuTK2ne+/PKQ+VvEeWmjMMksE17k/2RK/r5oM= -github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.18.4 h1:Du/aKtsWdiWsfUhnLsWOxG9gvO1SWbP7pw/kH6SBd78= -github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.18.4/go.mod h1:2sF7pT0z9zalHaTQ2JeksaK3lOwj8Cu/znj0jONV/Jc= -github.com/aws/aws-sdk-go-v2/service/s3 v1.45.1 h1:D/QGsEd+pZNLFMA0PCU/aoYCRUWrMGtEwW5xy6OraSE= -github.com/aws/aws-sdk-go-v2/service/s3 v1.45.1/go.mod h1:dqJ5JBL0clzgHriH35Amx3LRFY6wNIPUX7QO/BerSBo= -github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.24.1 h1:zKRDPVW3scBeTl1/ZiNyLwpTxh4gZ48xQVIyaghb6+c= -github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.24.1/go.mod h1:LDD9wCQ1tvjMIWEIFPvZ8JgJsEOjded+X5jav9tD/zg= -github.com/aws/aws-sdk-go-v2/service/sso v1.17.4 h1:WSMiDIMaDGyIiXwruNITU0IJF0d0foXwjxpxRylamqQ= -github.com/aws/aws-sdk-go-v2/service/sso v1.17.4/go.mod h1:oA6VjNsLll2eVuUoF2D+CMyORgNzPEW/3PyUdq6WQjI= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.20.2 h1:GsrlsvTPBNxHvE3KBCwUMnR76MTO/6qnnO1ILSUOpTA= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.20.2/go.mod h1:hHL974p5auvXlZPIjJTblXJpbkfK4klBczlsEaMCGVY= -github.com/aws/aws-sdk-go-v2/service/sts v1.25.5 h1:jwpmP8FnZPdpmJ8hkximoPQFGCUzfIekccwkxlfVfHQ= -github.com/aws/aws-sdk-go-v2/service/sts v1.25.5/go.mod h1:feTnm2Tk/pJxdX+eooEsxvlvTWBvDm6CasRZ+JOs2IY= -github.com/aws/smithy-go v1.17.0 h1:wWJD7LX6PBV6etBUwO0zElG0nWN9rUhp0WdYeHSHAaI= -github.com/aws/smithy-go v1.17.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE= +github.com/aws/aws-sdk-go-v2 v1.24.0 h1:890+mqQ+hTpNuw0gGP6/4akolQkSToDJgHfQE7AwGuk= +github.com/aws/aws-sdk-go-v2 v1.24.0/go.mod h1:LNh45Br1YAkEKaAqvmE1m8FUx6a5b/V0oAKV7of29b4= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4 h1:OCs21ST2LrepDfD3lwlQiOqIGp6JiEUqG84GzTDoyJs= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4/go.mod h1:usURWEKSNNAcAZuzRn/9ZYPT8aZQkR7xcCtunK/LkJo= +github.com/aws/aws-sdk-go-v2/config v1.26.1 h1:z6DqMxclFGL3Zfo+4Q0rLnAZ6yVkzCRxhRMsiRQnD1o= +github.com/aws/aws-sdk-go-v2/config v1.26.1/go.mod h1:ZB+CuKHRbb5v5F0oJtGdhFTelmrxd4iWO1lf0rQwSAg= +github.com/aws/aws-sdk-go-v2/credentials v1.16.12 h1:v/WgB8NxprNvr5inKIiVVrXPuuTegM+K8nncFkr1usU= +github.com/aws/aws-sdk-go-v2/credentials v1.16.12/go.mod h1:X21k0FjEJe+/pauud82HYiQbEr9jRKY3kXEIQ4hXeTQ= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.10 h1:w98BT5w+ao1/r5sUuiH6JkVzjowOKeOJRHERyy1vh58= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.10/go.mod h1:K2WGI7vUvkIv1HoNbfBA1bvIZ+9kL3YVmWxeKuLQsiw= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.15.7 h1:FnLf60PtjXp8ZOzQfhJVsqF0OtYKQZWQfqOLshh8YXg= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.15.7/go.mod h1:tDVvl8hyU6E9B8TrnNrZQEVkQlB8hjJwcgpPhgtlnNg= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.9 h1:v+HbZaCGmOwnTTVS86Fleq0vPzOd7tnJGbFhP0stNLs= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.9/go.mod h1:Xjqy+Nyj7VDLBtCMkQYOw1QYfAEZCVLrfI0ezve8wd4= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.9 h1:N94sVhRACtXyVcjXxrwK1SKFIJrA9pOJ5yu2eSHnmls= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.9/go.mod h1:hqamLz7g1/4EJP+GH5NBhcUMLjW+gKLQabgyz6/7WAU= +github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 h1:GrSw8s0Gs/5zZ0SX+gX4zQjRnRsMJDJ2sLur1gRBhEM= +github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.9 h1:ugD6qzjYtB7zM5PN/ZIeaAIyefPaD82G8+SJopgvUpw= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.9/go.mod h1:YD0aYBWCrPENpHolhKw2XDlTIWae2GKXT1T4o6N6hiM= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 h1:/b31bi3YVNlkzkBrm9LfpaKoaYZUxIAj4sHfOTmLfqw= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4/go.mod h1:2aGXHFmbInwgP9ZfpmdIfOELL79zhdNYNmReK8qDfdQ= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.9 h1:/90OR2XbSYfXucBMJ4U14wrjlfleq/0SB6dZDPncgmo= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.9/go.mod h1:dN/Of9/fNZet7UrQQ6kTDo/VSwKPIq94vjlU16bRARc= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.9 h1:Nf2sHxjMJR8CSImIVCONRi4g0Su3J+TSTbS7G0pUeMU= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.9/go.mod h1:idky4TER38YIjr2cADF1/ugFMKvZV7p//pVeV5LZbF0= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.9 h1:iEAeF6YC3l4FzlJPP9H3Ko1TXpdjdqWffxXjp8SY6uk= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.9/go.mod h1:kjsXoK23q9Z/tLBrckZLLyvjhZoS+AGrzqzUfEClvMM= +github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.19.5 h1:Fp3Gcbp3lAJAxeFRVSxc6tWOUPSG8iSkJEiFl3eZZ3o= +github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.19.5/go.mod h1:XABJbVXMa0xnVqaGbhkfUeVV0GrPsc3Jqscu87IovXU= +github.com/aws/aws-sdk-go-v2/service/s3 v1.47.5 h1:Keso8lIOS+IzI2MkPZyK6G0LYcK3My2LQ+T5bxghEAY= +github.com/aws/aws-sdk-go-v2/service/s3 v1.47.5/go.mod h1:vADO6Jn+Rq4nDtfwNjhgR84qkZwiC6FqCaXdw/kYwjA= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.25.5 h1:qYi/BfDrWXZxlmRjlKCyFmtI4HKJwW8OKDKhKRAOZQI= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.25.5/go.mod h1:4Ae1NCLK6ghmjzd45Tc33GgCKhUWD2ORAlULtMO1Cbs= +github.com/aws/aws-sdk-go-v2/service/sso v1.18.5 h1:ldSFWz9tEHAwHNmjx2Cvy1MjP5/L9kNoR0skc6wyOOM= +github.com/aws/aws-sdk-go-v2/service/sso v1.18.5/go.mod h1:CaFfXLYL376jgbP7VKC96uFcU8Rlavak0UlAwk1Dlhc= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.5 h1:2k9KmFawS63euAkY4/ixVNsYYwrwnd5fIvgEKkfZFNM= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.5/go.mod h1:W+nd4wWDVkSUIox9bacmkBP5NMFQeTJ/xqNabpzSR38= +github.com/aws/aws-sdk-go-v2/service/sts v1.26.5 h1:5UYvv8JUvllZsRnfrcMQ+hJ9jNICmcgKPAO1CER25Wg= +github.com/aws/aws-sdk-go-v2/service/sts v1.26.5/go.mod h1:XX5gh4CB7wAs4KhcF46G6C8a2i7eupU19dcAAE+EydU= +github.com/aws/smithy-go v1.19.0 h1:KWFKQV80DpP3vJrrA9sVAHQ5gc2z8i4EzrLhLlWXcBM= +github.com/aws/smithy-go v1.19.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I= @@ -93,8 +93,8 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cockroachdb/cockroach-go/v2 v2.3.5 h1:Khtm8K6fTTz/ZCWPzU9Ne3aOW9VyAnj4qIPCJgKtwK0= github.com/cockroachdb/cockroach-go/v2 v2.3.5/go.mod h1:1wNJ45eSXW9AnOc3skntW9ZUZz6gxrQK3cOj3rK+BC8= -github.com/coreos/go-oidc/v3 v3.8.0 h1:s3e30r6VEl3/M7DTSCEuImmrfu1/1WBgA0cXkdzkrAY= -github.com/coreos/go-oidc/v3 v3.8.0/go.mod h1:yQzSCqBnK3e6Fs5l+f5i0F8Kwf0zpH9bPEsbY00KanM= +github.com/coreos/go-oidc/v3 v3.9.0 h1:0J/ogVOd4y8P0f0xUh8l9t07xRP/d8tccvjHl2dcsSo= +github.com/coreos/go-oidc/v3 v3.9.0/go.mod h1:rTKz2PYwftcrtoCzV5g5kvfJoWcm0Mk8AF8y1iAQro4= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= @@ -137,8 +137,8 @@ github.com/go-acme/lego/v4 v4.14.2 h1:/D/jqRgLi8Cbk33sLGtu2pX2jEg3bGJWHyV8kFuUHG github.com/go-acme/lego/v4 v4.14.2/go.mod h1:kBXxbeTg0x9AgaOYjPSwIeJy3Y33zTz+tMD16O4MO6c= github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= -github.com/go-chi/jwtauth/v5 v5.2.0 h1:rw2wRNY6QHxyjYhoZYrQ4IeXVpPeun9nCZ9DBItDFPc= -github.com/go-chi/jwtauth/v5 v5.2.0/go.mod h1:2PoGm/KbnzRN9ILY6HFZAI6fTnb1gEZAKogAyqkd6fY= +github.com/go-chi/jwtauth/v5 v5.3.0 h1:X7RKGks1lrVeIe2omGyz47pNaNjG2YmwlRN5UKhN8qg= +github.com/go-chi/jwtauth/v5 v5.3.0/go.mod h1:2PoGm/KbnzRN9ILY6HFZAI6fTnb1gEZAKogAyqkd6fY= github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4= github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0= github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA= @@ -147,6 +147,11 @@ github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA= github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= +github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= @@ -157,8 +162,8 @@ github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MG github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= -github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= -github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v5 v5.1.0 h1:UGKbA/IPjtS6zLcdB7i5TyACMgSbOTiR8qzXgw8HWQU= +github.com/golang-jwt/jwt/v5 v5.1.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= @@ -209,8 +214,8 @@ github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qK github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= -github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= -github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-hclog v1.6.1 h1:pa92nu9bPoAqI7p+uPDCIWGAibUdlCi6TYWJEQQkLf8= +github.com/hashicorp/go-hclog v1.6.1/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-plugin v1.6.0 h1:wgd4KxHJTVGGqWBq4QPB1i5BZNEx9BR8+OFmHDmTk8A= github.com/hashicorp/go-plugin v1.6.0/go.mod h1:lBS5MtSSBZk0SHc66KACcjjlU6WzEVP/8pwz68aMkCI= github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT9yvm0e+Nd5M= @@ -223,10 +228,10 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.5.0 h1:NxstgwndsTRy7eq9/kqYc/BZh5w2hHJV86wjvO+1xPw= -github.com/jackc/pgx/v5 v5.5.0/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA= +github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA= +github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.5.1 h1:5I9etrGkLrN+2XPCsi6XLlV5DITbSL/xBZdmAxFcXPI= +github.com/jackc/pgx/v5 v5.5.1/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA= github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= @@ -235,8 +240,8 @@ github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9Y github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= -github.com/klauspost/compress v1.17.3 h1:qkRjuerhUU1EmXLYGkSH6EZL+vPSxIrYjLNAK4slzwA= -github.com/klauspost/compress v1.17.3/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= +github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -253,8 +258,8 @@ github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJG github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= -github.com/lestrrat-go/jwx/v2 v2.0.17 h1:+WavkdKVWO90ECnIzUetOnjY+kcqqw4WXEUmil7sMCE= -github.com/lestrrat-go/jwx/v2 v2.0.17/go.mod h1:G8randPHLGAqhcNCqtt6/V/7E6fvJRl3Sf9z777eTQ0= +github.com/lestrrat-go/jwx/v2 v2.0.18 h1:HHZkYS5wWDDyAiNBwztEtDoX07WDhGEdixm8G06R50o= +github.com/lestrrat-go/jwx/v2 v2.0.18/go.mod h1:fAJ+k5eTgKdDqanzCuK6DAt3W7n3cs2/FX7JhQdk83U= github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= @@ -335,8 +340,8 @@ github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sagikazarmark/locafero v0.3.0 h1:zT7VEGWC2DTflmccN/5T1etyKvxSxpHsjb9cJvm4SvQ= -github.com/sagikazarmark/locafero v0.3.0/go.mod h1:w+v7UsPNFwzF1cHuOajOOzoq4U7v/ig1mpRjqV+Bu1U= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4 h1:PT+ElG/UUFMfqy5HrxJxNzj3QBOf7dZwupeVC+mG1Lo= @@ -345,8 +350,8 @@ github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/sftpgo/sdk v0.1.6-0.20231105181545-b44c8058fc25 h1:R8cTb41ZX5WSYw8q8ufTKQfOvXh7aLQWqdnteDY/96U= github.com/sftpgo/sdk v0.1.6-0.20231105181545-b44c8058fc25/go.mod h1:6s/PFoLUd7FXG3wGlrdVhrA0SJOwri2h9kzTph/2oiU= -github.com/shirou/gopsutil/v3 v3.23.10 h1:/N42opWlYzegYaVkWejXWJpbzKv2JDy3mrgGzKsh9hM= -github.com/shirou/gopsutil/v3 v3.23.10/go.mod h1:JIE26kpucQi+innVlAUnIEOSBhBUkirr5b44yr55+WE= +github.com/shirou/gopsutil/v3 v3.23.11 h1:i3jP9NjCPUz7FiZKxlMnODZkdSIp2gnzfrvsu9CuWEQ= +github.com/shirou/gopsutil/v3 v3.23.11/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= @@ -361,8 +366,8 @@ github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.17.0 h1:I5txKw7MJasPL/BrfkbA0Jyo/oELqVmux4pR/UxOMfI= -github.com/spf13/viper v1.17.0/go.mod h1:BmMMMLQXSbcHK6KAOiFLz0l5JHrU89OdIRHvsk0+yVI= +github.com/spf13/viper v1.18.1 h1:rmuU42rScKWlhhJDyXZRKJQHXFX02chSVW1IvkPGiVM= +github.com/spf13/viper v1.18.1/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -380,10 +385,12 @@ github.com/studio-b12/gowebdav v0.9.0 h1:1j1sc9gQnNxbXXM4M/CebPOX4aXYtr7MojAVcN4 github.com/studio-b12/gowebdav v0.9.0/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= -github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/go-sysconf v0.3.13 h1:GBUpcahXSpR2xN01jhkNAbTLRk2Yzgggk8IM08lq3r4= +github.com/tklauser/go-sysconf v0.3.13/go.mod h1:zwleP4Q4OehZHGn4CYZDipCgg9usW5IJePewFCGVEa0= github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/tklauser/numcpus v0.7.0 h1:yjuerZP127QG9m5Zh/mSO4wqurYil27tHrqwRoRjpr4= +github.com/tklauser/numcpus v0.7.0/go.mod h1:bb6dMVcj8A42tSE7i32fsIUCbQNllK5iDguyOZRUzAY= github.com/unrolled/secure v1.13.0 h1:sdr3Phw2+f8Px8HE5sd1EHdj1aV3yUwed/uZXChLFsk= github.com/unrolled/secure v1.13.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40= github.com/wagslane/go-password-validator v0.3.0 h1:vfxOPzGHkz5S146HDpavl0cw1DSVP061Ry2PX0/ON6I= @@ -400,15 +407,23 @@ go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA= go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc= +go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= +go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4= +go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= +go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o= +go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A= +go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc= +go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8= go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -gocloud.dev v0.34.0 h1:LzlQY+4l2cMtuNfwT2ht4+fiXwWf/NmPTnXUlLmGif4= -gocloud.dev v0.34.0/go.mod h1:psKOachbnvY3DAOPbsFVmLIErwsbWPUG2H5i65D38vE= +gocloud.dev v0.35.0 h1:x/Gtt5OJdT4j+ir1AXAIXb7bBnFawXAAaJptCUGk3HU= +gocloud.dev v0.35.0/go.mod h1:wbyF+BhfdtLWyUtVEWRW13hFLb1vXnV2ovEhYGQe3ck= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20231127185646-65229373498e h1:Gvh4YaCaXNs6dKTlfgismwWZKyjVZXwOPfIyUaqU3No= -golang.org/x/exp v0.0.0-20231127185646-65229373498e/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= +golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb h1:c0vyKkb6yr3KR7jEfJaOSv4lG7xPkbN6r52aJz1d8a8= +golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= @@ -465,7 +480,6 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -501,8 +515,8 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= -google.golang.org/api v0.152.0 h1:t0r1vPnfMc260S2Ci+en7kfCZaLOPs5KI0sVV/6jZrY= -google.golang.org/api v0.152.0/go.mod h1:3qNJX5eOmhiWYc67jRA/3GsDw97UFb5ivv7Y2PrriAY= +google.golang.org/api v0.153.0 h1:N1AwGhielyKFaUqH07/ZSIQR3uNPcV7NVw0vj+j4iR4= +google.golang.org/api v0.153.0/go.mod h1:3qNJX5eOmhiWYc67jRA/3GsDw97UFb5ivv7Y2PrriAY= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= diff --git a/internal/common/connection.go b/internal/common/connection.go index e1dedad0..6de1f5bb 100644 --- a/internal/common/connection.go +++ b/internal/common/connection.go @@ -1722,7 +1722,7 @@ func (c *BaseConnection) GetFsAndResolvedPath(virtualPath string) (vfs.Fs, strin if c.protocol == ProtocolWebDAV && strings.Contains(err.Error(), vfs.ErrSFTPLoop.Error()) { // if there is an SFTP loop we return a permission error, for WebDAV, so the problematic folder // will not be listed - return nil, "", c.GetPermissionDeniedError() + return nil, "", util.NewI18nError(c.GetPermissionDeniedError(), util.I18nError403Message) } return nil, "", c.GetGenericError(err) } diff --git a/internal/dataprovider/dataprovider.go b/internal/dataprovider/dataprovider.go index bf7874fb..b391a31e 100644 --- a/internal/dataprovider/dataprovider.go +++ b/internal/dataprovider/dataprovider.go @@ -2632,14 +2632,23 @@ func buildUserHomeDir(user *User) { func validateFolderQuotaLimits(folder vfs.VirtualFolder) error { if folder.QuotaSize < -1 { - return util.NewValidationError(fmt.Sprintf("invalid quota_size: %v folder path %q", folder.QuotaSize, folder.MappedPath)) + return util.NewI18nError( + util.NewValidationError(fmt.Sprintf("invalid quota_size: %v folder path %q", folder.QuotaSize, folder.MappedPath)), + util.I18nErrorFolderQuotaSizeInvalid, + ) } if folder.QuotaFiles < -1 { - return util.NewValidationError(fmt.Sprintf("invalid quota_file: %v folder path %q", folder.QuotaFiles, folder.MappedPath)) + return util.NewI18nError( + util.NewValidationError(fmt.Sprintf("invalid quota_file: %v folder path %q", folder.QuotaFiles, folder.MappedPath)), + util.I18nErrorFolderQuotaFileInvalid, + ) } if (folder.QuotaSize == -1 && folder.QuotaFiles != -1) || (folder.QuotaFiles == -1 && folder.QuotaSize != -1) { - return util.NewValidationError(fmt.Sprintf("virtual folder quota_size and quota_files must be both -1 or >= 0, quota_size: %v quota_files: %v", - folder.QuotaFiles, folder.QuotaSize)) + return util.NewI18nError( + util.NewValidationError(fmt.Sprintf("virtual folder quota_size and quota_files must be both -1 or >= 0, quota_size: %v quota_files: %v", + folder.QuotaFiles, folder.QuotaSize)), + util.I18nErrorFolderQuotaInvalid, + ) } return nil } @@ -2657,12 +2666,18 @@ func validateUserGroups(user *User) error { } if g.Type == sdk.GroupTypePrimary { if hasPrimary { - return util.NewValidationError("only one primary group is allowed") + return util.NewI18nError( + util.NewValidationError("only one primary group is allowed"), + util.I18nErrorPrimaryGroup, + ) } hasPrimary = true } if groupNames[g.Name] { - return util.NewValidationError(fmt.Sprintf("the group %q is duplicated", g.Name)) + return util.NewI18nError( + util.NewValidationError(fmt.Sprintf("the group %q is duplicated", g.Name)), + util.I18nErrorDuplicateGroup, + ) } groupNames[g.Name] = true } @@ -2678,22 +2693,31 @@ func validateAssociatedVirtualFolders(vfolders []vfs.VirtualFolder) ([]vfs.Virtu for _, v := range vfolders { if v.VirtualPath == "" { - return nil, util.NewValidationError("mount/virtual path is mandatory") + return nil, util.NewI18nError( + util.NewValidationError("mount/virtual path is mandatory"), + util.I18nErrorFolderMountPathRequired, + ) } cleanedVPath := util.CleanPath(v.VirtualPath) if err := validateFolderQuotaLimits(v); err != nil { return nil, err } if v.Name == "" { - return nil, util.NewValidationError("folder name is mandatory") + return nil, util.NewI18nError(util.NewValidationError("folder name is mandatory"), util.I18nErrorFolderNameRequired) } if folderNames[v.Name] { - return nil, util.NewValidationError(fmt.Sprintf("the folder %q is duplicated", v.Name)) + return nil, util.NewI18nError( + util.NewValidationError(fmt.Sprintf("the folder %q is duplicated", v.Name)), + util.I18nErrorDuplicatedFolders, + ) } for _, vFolder := range virtualFolders { if util.IsDirOverlapped(vFolder.VirtualPath, cleanedVPath, false, "/") { - return nil, util.NewValidationError(fmt.Sprintf("invalid virtual folder %q, it overlaps with virtual folder %q", - v.VirtualPath, vFolder.VirtualPath)) + return nil, util.NewI18nError( + util.NewValidationError(fmt.Sprintf("invalid virtual folder %q, it overlaps with virtual folder %q", + v.VirtualPath, vFolder.VirtualPath)), + util.I18nErrorOverlappedFolders, + ) } } virtualFolders = append(virtualFolders, vfs.VirtualFolder{ @@ -2794,14 +2818,14 @@ func validateUserPermissions(permsToCheck map[string][]string) (map[string][]str func validatePermissions(user *User) error { if len(user.Permissions) == 0 { - return util.NewValidationError("please grant some permissions to this user") + return util.NewI18nError(util.NewValidationError("please grant some permissions to this user"), util.I18nErrorNoPermission) } if _, ok := user.Permissions["/"]; !ok { - return util.NewValidationError("permissions for the root dir \"/\" must be set") + return util.NewI18nError(util.NewValidationError("permissions for the root dir \"/\" must be set"), util.I18nErrorNoRootPermission) } permissions, err := validateUserPermissions(user.Permissions) if err != nil { - return err + return util.NewI18nError(err, util.I18nErrorGenericPermission) } user.Permissions = permissions return nil @@ -2836,10 +2860,16 @@ func validateFiltersPatternExtensions(baseFilters *sdk.BaseUserFilters) error { for _, f := range baseFilters.FilePatterns { cleanedPath := filepath.ToSlash(path.Clean(f.Path)) if !path.IsAbs(cleanedPath) { - return util.NewValidationError(fmt.Sprintf("invalid path %q for file patterns filter", f.Path)) + return util.NewI18nError( + util.NewValidationError(fmt.Sprintf("invalid path %q for file patterns filter", f.Path)), + util.I18nErrorFilePatternPathInvalid, + ) } if util.Contains(filteredPaths, cleanedPath) { - return util.NewValidationError(fmt.Sprintf("duplicate file patterns filter for path %q", f.Path)) + return util.NewI18nError( + util.NewValidationError(fmt.Sprintf("duplicate file patterns filter for path %q", f.Path)), + util.I18nErrorFilePatternDuplicated, + ) } if len(f.AllowedPatterns) == 0 && len(f.DeniedPatterns) == 0 { return util.NewValidationError(fmt.Sprintf("empty file patterns filter for path %q", f.Path)) @@ -2853,14 +2883,20 @@ func validateFiltersPatternExtensions(baseFilters *sdk.BaseUserFilters) error { for _, pattern := range f.AllowedPatterns { _, err := path.Match(pattern, "abc") if err != nil { - return util.NewValidationError(fmt.Sprintf("invalid file pattern filter %q", pattern)) + return util.NewI18nError( + util.NewValidationError(fmt.Sprintf("invalid file pattern filter %q", pattern)), + util.I18nErrorFilePatternInvalid, + ) } allowed = append(allowed, strings.ToLower(pattern)) } for _, pattern := range f.DeniedPatterns { _, err := path.Match(pattern, "abc") if err != nil { - return util.NewValidationError(fmt.Sprintf("invalid file pattern filter %q", pattern)) + return util.NewI18nError( + util.NewValidationError(fmt.Sprintf("invalid file pattern filter %q", pattern)), + util.I18nErrorFilePatternInvalid, + ) } denied = append(denied, strings.ToLower(pattern)) } @@ -2964,10 +3000,10 @@ func validateFilterProtocols(filters *sdk.BaseUserFilters) error { func validateBaseFilters(filters *sdk.BaseUserFilters) error { checkEmptyFiltersStruct(filters) if err := validateIPFilters(filters); err != nil { - return err + return util.NewI18nError(err, util.I18nErrorIPFiltersInvalid) } if err := validateBandwidthLimitsFilter(filters); err != nil { - return err + return util.NewI18nError(err, util.I18nErrorSourceBWLimitInvalid) } if len(filters.DeniedLoginMethods) >= len(ValidLoginMethods) { return util.NewValidationError("invalid denied_login_methods") @@ -2991,8 +3027,11 @@ func validateBaseFilters(filters *sdk.BaseUserFilters) error { } } if filters.MaxSharesExpiration > 0 && filters.MaxSharesExpiration < filters.DefaultSharesExpiration { - return util.NewValidationError(fmt.Sprintf("default shares expiration: %d must be less than or equal to max shares expiration: %d", - filters.DefaultSharesExpiration, filters.MaxSharesExpiration)) + return util.NewI18nError( + util.NewValidationError(fmt.Sprintf("default shares expiration: %d must be less than or equal to max shares expiration: %d", + filters.DefaultSharesExpiration, filters.MaxSharesExpiration)), + util.I18nErrorShareExpirationInvalid, + ) } updateFiltersValues(filters) @@ -3001,40 +3040,54 @@ func validateBaseFilters(filters *sdk.BaseUserFilters) error { func validateCombinedUserFilters(user *User) error { if user.Filters.TOTPConfig.Enabled && util.Contains(user.Filters.WebClient, sdk.WebClientMFADisabled) { - return util.NewValidationError("two-factor authentication cannot be disabled for a user with an active configuration") + return util.NewI18nError( + util.NewValidationError("two-factor authentication cannot be disabled for a user with an active configuration"), + util.I18nErrorDisableActive2FA, + ) } if user.Filters.RequirePasswordChange && util.Contains(user.Filters.WebClient, sdk.WebClientPasswordChangeDisabled) { - return util.NewValidationError("you cannot require password change and at the same time disallow it") + return util.NewI18nError( + util.NewValidationError("you cannot require password change and at the same time disallow it"), + util.I18nErrorPwdChangeConflict, + ) } return nil } func validateBaseParams(user *User) error { if user.Username == "" { - return util.NewValidationError("username is mandatory") + return util.NewI18nError(util.NewValidationError("username is mandatory"), util.I18nErrorUsernameRequired) } if err := checkReservedUsernames(user.Username); err != nil { - return err + return util.NewI18nError(err, util.I18nErrorReservedUsername) } if user.Email != "" && !util.IsEmailValid(user.Email) { - return util.NewValidationError(fmt.Sprintf("email %q is not valid", user.Email)) + return util.NewI18nError( + util.NewValidationError(fmt.Sprintf("email %q is not valid", user.Email)), + util.I18nErrorInvalidEmail, + ) } if config.NamingRules&1 == 0 && !usernameRegex.MatchString(user.Username) { - return util.NewValidationError(fmt.Sprintf("username %q is not valid, the following characters are allowed: a-zA-Z0-9-_.~", - user.Username)) + return util.NewI18nError( + util.NewValidationError(fmt.Sprintf("username %q is not valid, the following characters are allowed: a-zA-Z0-9-_.~", user.Username)), + util.I18nErrorInvalidUser, + ) } if user.hasRedactedSecret() { return util.NewValidationError("cannot save a user with a redacted secret") } if user.HomeDir == "" { - return util.NewValidationError("home_dir is mandatory") + return util.NewI18nError(util.NewValidationError("home_dir is mandatory"), util.I18nErrorHomeRequired) } // we can have users with no passwords and public keys, they can authenticate via SSH user certs or OIDC /*if user.Password == "" && len(user.PublicKeys) == 0 { return util.NewValidationError("please set a password or at least a public_key") }*/ if !filepath.IsAbs(user.HomeDir) { - return util.NewValidationError(fmt.Sprintf("home_dir must be an absolute path, actual value: %v", user.HomeDir)) + return util.NewI18nError( + util.NewValidationError(fmt.Sprintf("home_dir must be an absolute path, actual value: %v", user.HomeDir)), + util.I18nErrorHomeInvalid, + ) } if user.DownloadBandwidth < 0 { user.DownloadBandwidth = 0 @@ -3050,7 +3103,11 @@ func validateBaseParams(user *User) error { if user.Filters.IsAnonymous { user.setAnonymousSettings() } - return user.FsConfig.Validate(user.GetEncryptionAdditionalData()) + err := user.FsConfig.Validate(user.GetEncryptionAdditionalData()) + if err != nil { + return util.NewI18nError(err, util.I18nErrorFsValidation) + } + return nil } func hashPlainPassword(plainPwd string) (string, error) { @@ -3072,7 +3129,7 @@ func createUserPasswordHash(user *User) error { if user.Password != "" && !user.IsPasswordHashed() { if minEntropy := user.getMinPasswordEntropy(); minEntropy > 0 { if err := passwordvalidator.Validate(user.Password, minEntropy); err != nil { - return util.NewValidationError(err.Error()) + return util.NewI18nError(util.NewValidationError(err.Error()), util.I18nErrorPasswordComplexity) } } hashedPwd, err := hashPlainPassword(user.Password) @@ -3127,10 +3184,10 @@ func ValidateUser(user *User) error { return err } if err := validateUserTOTPConfig(&user.Filters.TOTPConfig, user.Username); err != nil { - return err + return util.NewI18nError(err, util.I18nError2FAInvalid) } if err := validateUserRecoveryCodes(user); err != nil { - return err + return util.NewI18nError(err, util.I18nErrorRecoveryCodesInvalid) } vfolders, err := validateAssociatedVirtualFolders(user.VirtualFolders) if err != nil { @@ -3144,7 +3201,7 @@ func ValidateUser(user *User) error { return err } if err := validatePublicKeys(user); err != nil { - return err + return util.NewI18nError(err, util.I18nErrorPubKeyInvalid) } if err := validateBaseFilters(&user.Filters.BaseUserFilters); err != nil { return err diff --git a/internal/dataprovider/share.go b/internal/dataprovider/share.go index 339651e5..1e7aede6 100644 --- a/internal/dataprovider/share.go +++ b/internal/dataprovider/share.go @@ -76,19 +76,6 @@ type Share struct { IsRestore bool `json:"-"` } -// GetScopeAsString returns the share's scope as string. -// Used in web pages -func (s *Share) GetScopeAsString() string { - switch s.Scope { - case ShareScopeWrite: - return "Write" - case ShareScopeReadWrite: - return "Read/Write" - default: - return "Read" - } -} - // IsExpired returns true if the share is expired func (s *Share) IsExpired() bool { if s.ExpiresAt > 0 { @@ -97,31 +84,6 @@ func (s *Share) IsExpired() bool { return false } -// GetInfoString returns share's info as string. -func (s *Share) GetInfoString() string { - var result strings.Builder - if s.ExpiresAt > 0 { - t := util.GetTimeFromMsecSinceEpoch(s.ExpiresAt) - result.WriteString(fmt.Sprintf("Expiration: %v. ", t.Format("2006-01-02 15:04"))) // YYYY-MM-DD HH:MM - } - if s.LastUseAt > 0 { - t := util.GetTimeFromMsecSinceEpoch(s.LastUseAt) - result.WriteString(fmt.Sprintf("Last use: %v. ", t.Format("2006-01-02 15:04"))) - } - if s.MaxTokens > 0 { - result.WriteString(fmt.Sprintf("Usage: %v/%v. ", s.UsedTokens, s.MaxTokens)) - } else { - result.WriteString(fmt.Sprintf("Used tokens: %v. ", s.UsedTokens)) - } - if len(s.AllowFrom) > 0 { - result.WriteString(fmt.Sprintf("Allowed IP/Mask: %v. ", len(s.AllowFrom))) - } - if s.Password != "" { - result.WriteString("Password protected.") - } - return result.String() -} - // GetAllowedFromAsString returns the allowed IP as comma separated string func (s *Share) GetAllowedFromAsString() string { return strings.Join(s.AllowFrom, ",") @@ -185,7 +147,7 @@ func (s *Share) hashPassword() error { } if minEntropy := user.getMinPasswordEntropy(); minEntropy > 0 { if err := passwordvalidator.Validate(s.Password, minEntropy); err != nil { - return util.NewValidationError(err.Error()) + return util.NewI18nError(util.NewValidationError(err.Error()), util.I18nErrorPasswordComplexity) } } if config.PasswordHashing.Algo == HashingAlgoBcrypt { @@ -214,14 +176,14 @@ func (s *Share) validatePaths() error { } s.Paths = paths if len(s.Paths) == 0 { - return util.NewValidationError("at least a shared path is required") + return util.NewI18nError(util.NewValidationError("at least a shared path is required"), util.I18nErrorSharePathRequired) } for idx := range s.Paths { s.Paths[idx] = util.CleanPath(s.Paths[idx]) } s.Paths = util.RemoveDuplicates(s.Paths, false) if s.Scope >= ShareScopeWrite && len(s.Paths) != 1 { - return util.NewValidationError("the write share scope requires exactly one path") + return util.NewI18nError(util.NewValidationError("the write share scope requires exactly one path"), util.I18nErrorShareWriteScope) } // check nested paths if len(s.Paths) > 1 { @@ -230,8 +192,8 @@ func (s *Share) validatePaths() error { if idx == innerIdx { continue } - if util.IsDirOverlapped(s.Paths[idx], s.Paths[innerIdx], true, "/") { - return util.NewGenericError("shared paths cannot be nested") + if s.Paths[idx] == "/" || s.Paths[innerIdx] == "/" || util.IsDirOverlapped(s.Paths[idx], s.Paths[innerIdx], true, "/") { + return util.NewI18nError(util.NewGenericError("shared paths cannot be nested"), util.I18nErrorShareNestedPaths) } } } @@ -244,26 +206,26 @@ func (s *Share) validate() error { return util.NewValidationError("share_id is mandatory") } if s.Name == "" { - return util.NewValidationError("name is mandatory") + return util.NewI18nError(util.NewValidationError("name is mandatory"), util.I18nErrorNameRequired) } if s.Scope < ShareScopeRead || s.Scope > ShareScopeReadWrite { - return util.NewValidationError(fmt.Sprintf("invalid scope: %v", s.Scope)) + return util.NewI18nError(util.NewValidationError(fmt.Sprintf("invalid scope: %v", s.Scope)), util.I18nErrorShareScope) } if err := s.validatePaths(); err != nil { return err } if s.ExpiresAt > 0 { if !s.IsRestore && s.ExpiresAt < util.GetTimeAsMsSinceEpoch(time.Now()) { - return util.NewValidationError("expiration must be in the future") + return util.NewI18nError(util.NewValidationError("expiration must be in the future"), util.I18nErrorShareExpirationPast) } } else { s.ExpiresAt = 0 } if s.MaxTokens < 0 { - return util.NewValidationError("invalid max tokens") + return util.NewI18nError(util.NewValidationError("invalid max tokens"), util.I18nErrorShareMaxTokens) } if s.Username == "" { - return util.NewValidationError("username is mandatory") + return util.NewI18nError(util.NewValidationError("username is mandatory"), util.I18nErrorUsernameRequired) } if s.HasRedactedPassword() { return util.NewValidationError("cannot save a share with a redacted password") @@ -275,7 +237,10 @@ func (s *Share) validate() error { for _, IPMask := range s.AllowFrom { _, _, err := net.ParseCIDR(IPMask) if err != nil { - return util.NewValidationError(fmt.Sprintf("could not parse allow from entry %q : %v", IPMask, err)) + return util.NewI18nError( + util.NewValidationError(fmt.Sprintf("could not parse allow from entry %q : %v", IPMask, err)), + util.I18nErrorInvalidIPMask, + ) } } return nil @@ -313,11 +278,11 @@ func (s *Share) GetRelativePath(name string) string { // IsUsable checks if the share is usable from the specified IP func (s *Share) IsUsable(ip string) (bool, error) { if s.MaxTokens > 0 && s.UsedTokens >= s.MaxTokens { - return false, util.NewRecordNotFoundError("max share usage exceeded") + return false, util.NewI18nError(util.NewRecordNotFoundError("max share usage exceeded"), util.I18nErrorShareUsage) } if s.ExpiresAt > 0 { if s.ExpiresAt < util.GetTimeAsMsSinceEpoch(time.Now()) { - return false, util.NewRecordNotFoundError("share expired") + return false, util.NewI18nError(util.NewRecordNotFoundError("share expired"), util.I18nErrorShareExpired) } } if len(s.AllowFrom) == 0 { @@ -325,7 +290,7 @@ func (s *Share) IsUsable(ip string) (bool, error) { } parsedIP := net.ParseIP(ip) if parsedIP == nil { - return false, ErrLoginNotAllowedFromIP + return false, util.NewI18nError(ErrLoginNotAllowedFromIP, util.I18nErrorLoginFromIPDenied) } for _, ipMask := range s.AllowFrom { _, network, err := net.ParseCIDR(ipMask) @@ -336,5 +301,5 @@ func (s *Share) IsUsable(ip string) (bool, error) { return true, nil } } - return false, ErrLoginNotAllowedFromIP + return false, util.NewI18nError(ErrLoginNotAllowedFromIP, util.I18nErrorLoginFromIPDenied) } diff --git a/internal/httpd/api_http_user.go b/internal/httpd/api_http_user.go index 9bb8c7d8..8fb0aa13 100644 --- a/internal/httpd/api_http_user.go +++ b/internal/httpd/api_http_user.go @@ -532,22 +532,28 @@ func changeUserPassword(w http.ResponseWriter, r *http.Request) { func doChangeUserPassword(r *http.Request, currentPassword, newPassword, confirmNewPassword string) error { if currentPassword == "" || newPassword == "" || confirmNewPassword == "" { - return util.NewValidationError("please provide the current password and the new one two times") + return util.NewI18nError( + util.NewValidationError("please provide the current password and the new one two times"), + util.I18nErrorChangePwdRequiredFields, + ) } if newPassword != confirmNewPassword { - return util.NewValidationError("the two password fields do not match") + return util.NewI18nError(util.NewValidationError("the two password fields do not match"), util.I18nErrorChangePwdNoMatch) } if currentPassword == newPassword { - return util.NewValidationError("the new password must be different from the current one") + return util.NewI18nError( + util.NewValidationError("the new password must be different from the current one"), + util.I18nErrorChangePwdNoDifferent, + ) } claims, err := getTokenClaims(r) if err != nil || claims.Username == "" { - return errors.New("invalid token claims") + return util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken) } _, err = dataprovider.CheckUserAndPass(claims.Username, currentPassword, util.GetIPFromRemoteAddress(r.RemoteAddr), getProtocolFromRequest(r)) if err != nil { - return util.NewValidationError("current password does not match") + return util.NewI18nError(util.NewValidationError("current password does not match"), util.I18nErrorChangePwdCurrentNoMatch) } return dataprovider.UpdateUserPassword(claims.Username, newPassword, dataprovider.ActionExecutorSelf, diff --git a/internal/httpd/api_shares.go b/internal/httpd/api_shares.go index 7bdd09ab..e4b61d3f 100644 --- a/internal/httpd/api_shares.go +++ b/internal/httpd/api_shares.go @@ -455,7 +455,7 @@ func (s *httpdServer) checkPublicShare(w http.ResponseWriter, r *http.Request, v isWebClient := isWebClientRequest(r) renderError := func(err error, message string, statusCode int) { if isWebClient { - s.renderClientMessagePage(w, r, "Unable to access the share", message, statusCode, err, "") + s.renderClientMessagePage(w, r, util.I18nShareAccessErrorTitle, statusCode, err, message) } else { sendAPIResponse(w, r, err, message, statusCode) } @@ -466,14 +466,15 @@ func (s *httpdServer) checkPublicShare(w http.ResponseWriter, r *http.Request, v if err != nil { statusCode := getRespStatus(err) if statusCode == http.StatusNotFound { - err = errors.New("share does not exist") + err = util.NewI18nError(errors.New("share does not exist"), util.I18nError404Message) } renderError(err, "", statusCode) return share, nil, err } if !util.Contains(validScopes, share.Scope) { - renderError(nil, "Invalid share scope", http.StatusForbidden) - return share, nil, errors.New("invalid share scope") + err := errors.New("invalid share scope") + renderError(util.NewI18nError(err, util.I18nErrorShareScope), "", http.StatusForbidden) + return share, nil, err } ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr) ok, err := share.IsUsable(ipAddr) @@ -524,20 +525,29 @@ func getUserForShare(share dataprovider.Share) (dataprovider.User, error) { return user, err } if !user.CanManageShares() { - return user, util.NewRecordNotFoundError("this share does not exist") + return user, util.NewI18nError(util.NewRecordNotFoundError("this share does not exist"), util.I18nError404Message) } if share.Password == "" && util.Contains(user.Filters.WebClient, sdk.WebClientShareNoPasswordDisabled) { - return user, fmt.Errorf("sharing without a password was disabled: %w", os.ErrPermission) + return user, util.NewI18nError( + fmt.Errorf("sharing without a password was disabled: %w", os.ErrPermission), + util.I18nError403Message, + ) } if user.MustSetSecondFactorForProtocol(common.ProtocolHTTP) { - return user, util.NewMethodDisabledError("two-factor authentication requirements not met") + return user, util.NewI18nError( + util.NewMethodDisabledError("two-factor authentication requirements not met"), + util.I18nError403Message, + ) } return user, nil } func validateBrowsableShare(share dataprovider.Share, connection *Connection) error { if len(share.Paths) != 1 { - return util.NewValidationError("a share with multiple paths is not browsable") + return util.NewI18nError( + util.NewValidationError("a share with multiple paths is not browsable"), + util.I18nErrorShareBrowsePaths, + ) } basePath := share.Paths[0] info, err := connection.Stat(basePath, 0) @@ -545,7 +555,10 @@ func validateBrowsableShare(share dataprovider.Share, connection *Connection) er return fmt.Errorf("unable to check the share directory: %w", err) } if !info.IsDir() { - return util.NewValidationError("the shared object is not a directory and so it is not browsable") + return util.NewI18nError( + util.NewValidationError("the shared object is not a directory and so it is not browsable"), + util.I18nErrorShareBrowseNoDir, + ) } return nil } @@ -556,7 +569,10 @@ func getBrowsableSharedPath(share dataprovider.Share, r *http.Request) (string, return name, nil } if name != share.Paths[0] && !strings.HasPrefix(name, share.Paths[0]+"/") { - return "", util.NewValidationError(fmt.Sprintf("Invalid path %q", r.URL.Query().Get("path"))) + return "", util.NewI18nError( + util.NewValidationError(fmt.Sprintf("Invalid path %q", r.URL.Query().Get("path"))), + util.I18nErrorPathInvalid, + ) } return name, nil } diff --git a/internal/httpd/api_utils.go b/internal/httpd/api_utils.go index af34de1a..88447647 100644 --- a/internal/httpd/api_utils.go +++ b/internal/httpd/api_utils.go @@ -628,23 +628,32 @@ func updateLoginMetrics(user *dataprovider.User, loginMethod, ip string, err err func checkHTTPClientUser(user *dataprovider.User, r *http.Request, connectionID string, checkSessions bool) error { if util.Contains(user.Filters.DeniedProtocols, common.ProtocolHTTP) { logger.Info(logSender, connectionID, "cannot login user %q, protocol HTTP is not allowed", user.Username) - return fmt.Errorf("protocol HTTP is not allowed for user %q", user.Username) + return util.NewI18nError( + fmt.Errorf("protocol HTTP is not allowed for user %q", user.Username), + util.I18nErrorProtocolForbidden, + ) } if !isLoggedInWithOIDC(r) && !user.IsLoginMethodAllowed(dataprovider.LoginMethodPassword, common.ProtocolHTTP) { logger.Info(logSender, connectionID, "cannot login user %q, password login method is not allowed", user.Username) - return fmt.Errorf("login method password is not allowed for user %q", user.Username) + return util.NewI18nError( + fmt.Errorf("login method password is not allowed for user %q", user.Username), + util.I18nErrorPwdLoginForbidden, + ) } if checkSessions && user.MaxSessions > 0 { activeSessions := common.Connections.GetActiveSessions(user.Username) if activeSessions >= user.MaxSessions { logger.Info(logSender, connectionID, "authentication refused for user: %q, too many open sessions: %v/%v", user.Username, activeSessions, user.MaxSessions) - return fmt.Errorf("too many open sessions: %v", activeSessions) + return util.NewI18nError(fmt.Errorf("too many open sessions: %v", activeSessions), util.I18nError429Message) } } if !user.IsLoginFromAddrAllowed(r.RemoteAddr) { logger.Info(logSender, connectionID, "cannot login user %q, remote address is not allowed: %v", user.Username, r.RemoteAddr) - return fmt.Errorf("login for user %q is not allowed from this address: %v", user.Username, r.RemoteAddr) + return util.NewI18nError( + fmt.Errorf("login for user %q is not allowed from this address: %v", user.Username, r.RemoteAddr), + util.I18nErrorIPForbidden, + ) } return nil } @@ -656,7 +665,7 @@ func handleForgotPassword(r *http.Request, username string, isAdmin bool) error var user dataprovider.User if username == "" { - return util.NewValidationError("username is mandatory") + return util.NewI18nError(util.NewValidationError("username is mandatory"), util.I18nErrorUsernameRequired) } if isAdmin { admin, err = dataprovider.AdminExists(username) @@ -668,7 +677,10 @@ func handleForgotPassword(r *http.Request, username string, isAdmin bool) error subject = fmt.Sprintf("Email Verification Code for user %q", username) if err == nil { if !isUserAllowedToResetPassword(r, &user) { - return util.NewValidationError("you are not allowed to reset your password") + return util.NewI18nError( + util.NewValidationError("you are not allowed to reset your password"), + util.I18nErrorPwdResetForbidded, + ) } } } @@ -679,10 +691,13 @@ func handleForgotPassword(r *http.Request, username string, isAdmin bool) error username, isAdmin) return nil } - return util.NewGenericError("Error retrieving your account, please try again later") + return util.NewI18nError(util.NewGenericError("Error retrieving your account, please try again later"), util.I18nErrorGetUser) } if email == "" { - return util.NewValidationError("Your account does not have an email address, it is not possible to reset your password by sending an email verification code") + return util.NewI18nError( + util.NewValidationError("Your account does not have an email address, it is not possible to reset your password by sending an email verification code"), + util.I18nErrorPwdResetNoEmail, + ) } c := newResetCode(username, isAdmin) body := new(bytes.Buffer) @@ -696,7 +711,10 @@ func handleForgotPassword(r *http.Request, username string, isAdmin bool) error if err := smtp.SendEmail([]string{email}, nil, subject, body.String(), smtp.EmailContentTypeTextHTML); err != nil { logger.Warn(logSender, middleware.GetReqID(r.Context()), "unable to send password reset code via email: %v, elapsed: %v", err, time.Since(startTime)) - return util.NewGenericError(fmt.Sprintf("Unable to send confirmation code via email: %v", err)) + return util.NewI18nError( + util.NewGenericError(fmt.Sprintf("Error sending confirmation code via email: %v", err)), + util.I18nErrorPwdResetSendEmail, + ) } logger.Debug(logSender, middleware.GetReqID(r.Context()), "reset code sent via email to %q, email: %q, is admin? %v, elapsed: %v", username, email, isAdmin, time.Since(startTime)) @@ -744,7 +762,10 @@ func handleResetPassword(r *http.Request, code, newPassword string, isAdmin bool } if err == nil { if !isUserAllowedToResetPassword(r, &user) { - return &admin, &user, util.NewValidationError("you are not allowed to reset your password") + return &admin, &user, util.NewI18nError( + util.NewValidationError("you are not allowed to reset your password"), + util.I18nErrorPwdResetForbidded, + ) } } err = dataprovider.UpdateUserPassword(user.Username, newPassword, dataprovider.ActionExecutorSelf, diff --git a/internal/httpd/handler.go b/internal/httpd/handler.go index e64c00e5..d36b2669 100644 --- a/internal/httpd/handler.go +++ b/internal/httpd/handler.go @@ -100,16 +100,16 @@ func (c *Connection) getFileReader(name string, offset int64, method string) (io transferQuota := c.GetTransferQuota() if !transferQuota.HasDownloadSpace() { c.Log(logger.LevelInfo, "denying file read due to quota limits") - return nil, c.GetReadQuotaExceededError() + return nil, util.NewI18nError(c.GetReadQuotaExceededError(), util.I18nErrorQuotaRead) } if !c.User.HasPerm(dataprovider.PermDownload, path.Dir(name)) { - return nil, c.GetPermissionDeniedError() + return nil, util.NewI18nError(c.GetPermissionDeniedError(), util.I18nError403Message) } if ok, policy := c.User.IsFileAllowed(name); !ok { c.Log(logger.LevelWarn, "reading file %q is not allowed", name) - return nil, c.GetErrorForDeniedFile(policy) + return nil, util.NewI18nError(c.GetErrorForDeniedFile(policy), util.I18nError403Message) } fs, p, err := c.GetFsAndResolvedPath(name) @@ -120,7 +120,7 @@ func (c *Connection) getFileReader(name string, offset int64, method string) (io if method != http.MethodHead { if _, err := common.ExecutePreAction(c.BaseConnection, common.OperationPreDownload, p, name, 0, 0); err != nil { c.Log(logger.LevelDebug, "download for file %q denied by pre action: %v", name, err) - return nil, c.GetPermissionDeniedError() + return nil, util.NewI18nError(c.GetPermissionDeniedError(), util.I18nError403Message) } } diff --git a/internal/httpd/httpd_test.go b/internal/httpd/httpd_test.go index 40942368..2757c08e 100644 --- a/internal/httpd/httpd_test.go +++ b/internal/httpd/httpd_test.go @@ -3209,7 +3209,7 @@ func TestMustChangePasswordRequirement(t *testing.T) { setJWTCookieForReq(req, webToken) rr = executeRequest(req) checkResponseCode(t, http.StatusForbidden, rr) - assert.Contains(t, rr.Body.String(), "Password change required. Please set a new password to continue to use your account") + assert.Contains(t, rr.Body.String(), util.I18nErrorChangePwdRequired) // change pwd pwd := make(map[string]string) pwd["current_password"] = defaultPassword @@ -3317,7 +3317,7 @@ func TestTwoFactorRequirements(t *testing.T) { setJWTCookieForReq(req, webToken) rr = executeRequest(req) checkResponseCode(t, http.StatusForbidden, rr) - assert.Contains(t, rr.Body.String(), "Two-factor authentication requirements not met, please configure two-factor authentication for the following protocols") + assert.Contains(t, rr.Body.String(), util.I18nError2FARequired) configName, key, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], user.Username) assert.NoError(t, err) @@ -6425,7 +6425,7 @@ func TestNamingRules(t *testing.T) { setJWTCookieForReq(req, token) rr := executeRequest(req) checkResponseCode(t, http.StatusOK, rr) - assert.Contains(t, rr.Body.String(), "the following characters are allowed") + assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidUser) // test user reset password. Setting the new password will fail because the username is not valid form = make(url.Values) form.Set("username", user.Username) @@ -6449,7 +6449,7 @@ func TestNamingRules(t *testing.T) { req.Header.Set("Content-Type", "application/x-www-form-urlencoded") rr = executeRequest(req) assert.Equal(t, http.StatusOK, rr.Code) - assert.Contains(t, rr.Body.String(), "the following characters are allowed") + assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidUser) adminAPIToken, err = getJWTAPITokenFromTestServer(admin.Username, defaultTokenAuthPass) assert.NoError(t, err) @@ -6743,7 +6743,7 @@ func TestSaveErrors(t *testing.T) { req.Header.Set("Content-Type", "application/x-www-form-urlencoded") rr = executeRequest(req) assert.Equal(t, http.StatusInternalServerError, rr.Code) - assert.Contains(t, rr.Body.String(), "unable to set the recovery code as used") + assert.Contains(t, rr.Body.String(), util.I18nError500Message) _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) @@ -6886,7 +6886,7 @@ func TestProviderErrors(t *testing.T) { req.Header.Set("Content-Type", "application/x-www-form-urlencoded") rr = executeRequest(req) assert.Equal(t, http.StatusOK, rr.Code) - assert.Contains(t, rr.Body.String(), "Error retrieving your account, please try again later") + assert.Contains(t, rr.Body.String(), util.I18nErrorGetUser) req, err = http.NewRequest(http.MethodGet, webClientSharesPath, nil) assert.NoError(t, err) @@ -9615,7 +9615,7 @@ func TestWebUserTwoFactorLogin(t *testing.T) { req.Header.Set("Content-Type", "application/x-www-form-urlencoded") rr = executeRequest(req) assert.Equal(t, http.StatusOK, rr.Code) - assert.Contains(t, rr.Body.String(), "unable to verify form token") + assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCSRF) form.Set(csrfFormToken, csrfToken) form.Set("passcode", "invalid_user_passcode") @@ -9626,7 +9626,7 @@ func TestWebUserTwoFactorLogin(t *testing.T) { req.Header.Set("Content-Type", "application/x-www-form-urlencoded") rr = executeRequest(req) assert.Equal(t, http.StatusOK, rr.Code) - assert.Contains(t, rr.Body.String(), "Invalid authentication code") + assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCredentials) form.Set("passcode", "") req, err = http.NewRequest(http.MethodPost, webClientTwoFactorPath, bytes.NewBuffer([]byte(form.Encode()))) @@ -9636,7 +9636,7 @@ func TestWebUserTwoFactorLogin(t *testing.T) { req.Header.Set("Content-Type", "application/x-www-form-urlencoded") rr = executeRequest(req) assert.Equal(t, http.StatusOK, rr.Code) - assert.Contains(t, rr.Body.String(), "Invalid credentials") + assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCredentials) form.Set("passcode", passcode) req, err = http.NewRequest(http.MethodPost, webClientTwoFactorPath, bytes.NewBuffer([]byte(form.Encode()))) @@ -9677,7 +9677,7 @@ func TestWebUserTwoFactorLogin(t *testing.T) { req.Header.Set("Content-Type", "application/x-www-form-urlencoded") rr = executeRequest(req) assert.Equal(t, http.StatusOK, rr.Code) - assert.Contains(t, rr.Body.String(), "unable to verify form token") + assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCSRF) form.Set(csrfFormToken, csrfToken) form.Set("recovery_code", "") @@ -9688,7 +9688,7 @@ func TestWebUserTwoFactorLogin(t *testing.T) { req.Header.Set("Content-Type", "application/x-www-form-urlencoded") rr = executeRequest(req) assert.Equal(t, http.StatusOK, rr.Code) - assert.Contains(t, rr.Body.String(), "Invalid credentials") + assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCredentials) form.Set("recovery_code", recoveryCode) req, err = http.NewRequest(http.MethodPost, webClientTwoFactorRecoveryPath, bytes.NewBuffer([]byte(form.Encode()))) @@ -9764,7 +9764,7 @@ func TestWebUserTwoFactorLogin(t *testing.T) { req.Header.Set("Content-Type", "application/x-www-form-urlencoded") rr = executeRequest(req) assert.Equal(t, http.StatusOK, rr.Code) - assert.Contains(t, rr.Body.String(), "This recovery code was already used") + assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCredentials) form.Set("recovery_code", "invalid_user_recovery_code") req, err = http.NewRequest(http.MethodPost, webClientTwoFactorRecoveryPath, bytes.NewBuffer([]byte(form.Encode()))) @@ -9774,7 +9774,7 @@ func TestWebUserTwoFactorLogin(t *testing.T) { req.Header.Set("Content-Type", "application/x-www-form-urlencoded") rr = executeRequest(req) assert.Equal(t, http.StatusOK, rr.Code) - assert.Contains(t, rr.Body.String(), "Invalid recovery code") + assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCredentials) form = getLoginForm(defaultUsername, defaultPassword, csrfToken) req, err = http.NewRequest(http.MethodPost, webClientLoginPath, bytes.NewBuffer([]byte(form.Encode()))) @@ -9815,7 +9815,7 @@ func TestWebUserTwoFactorLogin(t *testing.T) { req.Header.Set("Content-Type", "application/x-www-form-urlencoded") rr = executeRequest(req) assert.Equal(t, http.StatusOK, rr.Code) - assert.Contains(t, rr.Body.String(), "Two factory authentication is not enabled") + assert.Contains(t, rr.Body.String(), util.I18n2FADisabled) _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) @@ -9829,7 +9829,7 @@ func TestWebUserTwoFactorLogin(t *testing.T) { req.Header.Set("Content-Type", "application/x-www-form-urlencoded") rr = executeRequest(req) assert.Equal(t, http.StatusOK, rr.Code) - assert.Contains(t, rr.Body.String(), "Invalid credentials") + assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCredentials) req, err = http.NewRequest(http.MethodPost, webClientTwoFactorPath, bytes.NewBuffer([]byte(form.Encode()))) assert.NoError(t, err) @@ -9838,7 +9838,7 @@ func TestWebUserTwoFactorLogin(t *testing.T) { req.Header.Set("Content-Type", "application/x-www-form-urlencoded") rr = executeRequest(req) assert.Equal(t, http.StatusOK, rr.Code) - assert.Contains(t, rr.Body.String(), "Invalid credentials") + assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCredentials) req, err = http.NewRequest(http.MethodGet, webClientMFAPath, nil) assert.NoError(t, err) @@ -10880,7 +10880,7 @@ func TestPermGroupOverride(t *testing.T) { setJWTCookieForReq(req, webToken) rr = executeRequest(req) checkResponseCode(t, http.StatusForbidden, rr) - assert.Contains(t, rr.Body.String(), "Two-factor authentication requirements not met, please configure two-factor authentication for the following protocols") + assert.Contains(t, rr.Body.String(), util.I18nError2FARequired) _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) @@ -12333,14 +12333,14 @@ func TestWebClientLoginMock(t *testing.T) { setJWTCookieForReq(req, webToken) rr = executeRequest(req) checkResponseCode(t, http.StatusNotFound, rr) - assert.Contains(t, rr.Body.String(), "Unable to retrieve your user") + assert.Contains(t, rr.Body.String(), util.I18nErrorGetUser) req, _ = http.NewRequest(http.MethodGet, webClientDirsPath, nil) req.RemoteAddr = defaultRemoteAddr setJWTCookieForReq(req, webToken) rr = executeRequest(req) checkResponseCode(t, http.StatusNotFound, rr) - assert.Contains(t, rr.Body.String(), "Unable to retrieve your user") + assert.Contains(t, rr.Body.String(), util.I18nErrorDirListUser) form := make(url.Values) form.Set("files", `[]`) @@ -12351,7 +12351,7 @@ func TestWebClientLoginMock(t *testing.T) { setJWTCookieForReq(req, webToken) rr = executeRequest(req) checkResponseCode(t, http.StatusNotFound, rr) - assert.Contains(t, rr.Body.String(), "Unable to retrieve your user") + assert.Contains(t, rr.Body.String(), util.I18nErrorGetUser) req, _ = http.NewRequest(http.MethodGet, userDirsPath, nil) setBearerForReq(req, apiUserToken) @@ -12389,7 +12389,7 @@ func TestWebClientLoginErrorsMock(t *testing.T) { req.Header.Set("Content-Type", "application/x-www-form-urlencoded") rr := executeRequest(req) assert.Equal(t, http.StatusOK, rr.Code) - assert.Contains(t, rr.Body.String(), "Invalid credentials") + assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCredentials) form = getLoginForm(defaultUsername, defaultPassword, "") req, _ = http.NewRequest(http.MethodPost, webClientLoginPath, bytes.NewBuffer([]byte(form.Encode()))) @@ -12397,7 +12397,7 @@ func TestWebClientLoginErrorsMock(t *testing.T) { req.Header.Set("Content-Type", "application/x-www-form-urlencoded") rr = executeRequest(req) assert.Equal(t, http.StatusOK, rr.Code) - assert.Contains(t, rr.Body.String(), "unable to verify form token") + assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCSRF) } func TestWebClientMaxConnections(t *testing.T) { @@ -12514,7 +12514,7 @@ func TestDefender(t *testing.T) { req.RemoteAddr = remoteAddr rr = executeRequest(req) checkResponseCode(t, http.StatusForbidden, rr) - assert.Contains(t, rr.Body.String(), "your IP address is blocked") + assert.Contains(t, rr.Body.String(), util.I18nErrorIPForbidden) req, _ = http.NewRequest(http.MethodGet, webUsersPath, nil) req.RequestURI = webUsersPath @@ -12636,35 +12636,35 @@ func TestMaxSessions(t *testing.T) { setJWTCookieForReq(req, webToken) rr = executeRequest(req) checkResponseCode(t, http.StatusTooManyRequests, rr) - assert.Contains(t, rr.Body.String(), "too many open sessions") + assert.Contains(t, rr.Body.String(), util.I18nError429Message) req, err = http.NewRequest(http.MethodGet, webClientDirsPath, nil) assert.NoError(t, err) setJWTCookieForReq(req, webToken) rr = executeRequest(req) checkResponseCode(t, http.StatusTooManyRequests, rr) - assert.Contains(t, rr.Body.String(), "too many open sessions") + assert.Contains(t, rr.Body.String(), util.I18nErrorDirList429) req, err = http.NewRequest(http.MethodGet, webClientFilesPath+"?path=p", nil) assert.NoError(t, err) setJWTCookieForReq(req, webToken) rr = executeRequest(req) checkResponseCode(t, http.StatusTooManyRequests, rr) - assert.Contains(t, rr.Body.String(), "too many open sessions") + assert.Contains(t, rr.Body.String(), util.I18nError429Message) req, err = http.NewRequest(http.MethodGet, webClientEditFilePath+"?path=file", nil) //nolint:goconst assert.NoError(t, err) setJWTCookieForReq(req, webToken) rr = executeRequest(req) checkResponseCode(t, http.StatusTooManyRequests, rr) - assert.Contains(t, rr.Body.String(), "too many open sessions") + assert.Contains(t, rr.Body.String(), util.I18nError429Message) req, err = http.NewRequest(http.MethodGet, webClientGetPDFPath+"?path=file", nil) assert.NoError(t, err) setJWTCookieForReq(req, webToken) rr = executeRequest(req) checkResponseCode(t, http.StatusTooManyRequests, rr) - assert.Contains(t, rr.Body.String(), "too many open sessions") + assert.Contains(t, rr.Body.String(), util.I18nError429Message) // test reset password smtpCfg := smtp.Config{ @@ -12698,7 +12698,7 @@ func TestMaxSessions(t *testing.T) { req.Header.Set("Content-Type", "application/x-www-form-urlencoded") rr = executeRequest(req) assert.Equal(t, http.StatusOK, rr.Code) - assert.Contains(t, rr.Body.String(), "Password reset successfully but unable to login") + assert.Contains(t, rr.Body.String(), util.I18nError429Message) smtpCfg = smtp.Config{} err = smtpCfg.Initialize(configDir, true) @@ -13012,7 +13012,7 @@ func TestSFTPLoopError(t *testing.T) { req.Header.Set("Content-Type", "application/x-www-form-urlencoded") rr = executeRequest(req) assert.Equal(t, http.StatusOK, rr.Code) - assert.Contains(t, rr.Body.String(), "Password reset successfully but unable to login") + assert.Contains(t, rr.Body.String(), util.I18nErrorLoginAfterReset) smtpCfg = smtp.Config{} err = smtpCfg.Initialize(configDir, true) @@ -13090,7 +13090,7 @@ func TestWebClientChangePwd(t *testing.T) { setJWTCookieForReq(req, webToken) rr = executeRequest(req) checkResponseCode(t, http.StatusForbidden, rr) - assert.Contains(t, rr.Body.String(), "unable to verify form token") + assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCSRF) form.Set(csrfFormToken, csrfToken) req, _ = http.NewRequest(http.MethodPost, webChangeClientPwdPath, bytes.NewBuffer([]byte(form.Encode()))) @@ -13099,7 +13099,7 @@ func TestWebClientChangePwd(t *testing.T) { setJWTCookieForReq(req, webToken) rr = executeRequest(req) checkResponseCode(t, http.StatusOK, rr) - assert.Contains(t, rr.Body.String(), "the new password must be different from the current one") + assert.Contains(t, rr.Body.String(), util.I18nErrorChangePwdNoDifferent) form.Set("current_password", defaultPassword+"2") form.Set("new_password1", defaultPassword+"1") @@ -13110,7 +13110,7 @@ func TestWebClientChangePwd(t *testing.T) { setJWTCookieForReq(req, webToken) rr = executeRequest(req) checkResponseCode(t, http.StatusOK, rr) - assert.Contains(t, rr.Body.String(), "current password does not match") + assert.Contains(t, rr.Body.String(), util.I18nErrorChangePwdCurrentNoMatch) form.Set("current_password", defaultPassword) form.Set("new_password1", defaultPassword+"1") @@ -13203,7 +13203,7 @@ func TestPreDownloadHook(t *testing.T) { setJWTCookieForReq(req, webToken) rr = executeRequest(req) checkResponseCode(t, http.StatusOK, rr) - assert.Contains(t, rr.Body.String(), "permission denied") + assert.Contains(t, rr.Body.String(), util.I18nError403Message) req, err = http.NewRequest(http.MethodGet, userFilesPath+"?path="+testFileName, nil) assert.NoError(t, err) @@ -13387,7 +13387,7 @@ func TestShareUsage(t *testing.T) { assert.NoError(t, err) rr = executeRequest(req) checkResponseCode(t, http.StatusForbidden, rr) - assert.Contains(t, rr.Body.String(), "Invalid share scope") + assert.Contains(t, rr.Body.String(), "invalid share scope") share.MaxTokens = 3 share.Scope = dataprovider.ShareScopeWrite @@ -13652,7 +13652,7 @@ func TestShareMaxExpiration(t *testing.T) { setJWTCookieForReq(req, webClientToken) rr = executeRequest(req) checkResponseCode(t, http.StatusOK, rr) - assert.Contains(t, rr.Body.String(), "share must expire before") + assert.Contains(t, rr.Body.String(), util.I18nErrorShareExpirationOutOfRange) req, err = http.NewRequest(http.MethodPost, path.Join(webClientSharePath, shareID), bytes.NewBuffer([]byte(form.Encode()))) assert.NoError(t, err) @@ -13661,7 +13661,7 @@ func TestShareMaxExpiration(t *testing.T) { setJWTCookieForReq(req, webClientToken) rr = executeRequest(req) checkResponseCode(t, http.StatusOK, rr) - assert.Contains(t, rr.Body.String(), "share must expire before") + assert.Contains(t, rr.Body.String(), util.I18nErrorShareExpirationOutOfRange) _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) @@ -13675,7 +13675,7 @@ func TestShareMaxExpiration(t *testing.T) { setJWTCookieForReq(req, webClientToken) rr = executeRequest(req) checkResponseCode(t, http.StatusOK, rr) - assert.Contains(t, rr.Body.String(), "Unable to retrieve your user") + assert.Contains(t, rr.Body.String(), util.I18nErrorGetUser) } func TestWebClientShareCredentials(t *testing.T) { @@ -13748,7 +13748,7 @@ func TestWebClientShareCredentials(t *testing.T) { req.Header.Set("Content-Type", "application/x-www-form-urlencoded") rr = executeRequest(req) checkResponseCode(t, http.StatusOK, rr) - assert.Contains(t, rr.Body.String(), "unable to verify form token") + assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCSRF) // set the CSRF token csrfToken, err := getCSRFToken(httpBaseURL + webClientLoginPath) assert.NoError(t, err) @@ -13759,7 +13759,7 @@ func TestWebClientShareCredentials(t *testing.T) { req.Header.Set("Content-Type", "application/x-www-form-urlencoded") rr = executeRequest(req) checkResponseCode(t, http.StatusOK, rr) - assert.Contains(t, rr.Body.String(), "Share login successful") + assert.Contains(t, rr.Body.String(), util.I18nShareLoginOK) cookie := rr.Header().Get("Set-Cookie") cookie = strings.TrimPrefix(cookie, "jwt=") assert.NotEmpty(t, cookie) @@ -13806,7 +13806,7 @@ func TestWebClientShareCredentials(t *testing.T) { req.Header.Set("Content-Type", "application/x-www-form-urlencoded") rr = executeRequest(req) checkResponseCode(t, http.StatusOK, rr) - assert.Contains(t, rr.Body.String(), dataprovider.ErrInvalidCredentials.Error()) + assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCredentials) // login with the next param set form.Set("share_password", defaultPassword) nextURI := path.Join(webClientPubSharesPath, shareReadID, "browse") @@ -13826,7 +13826,7 @@ func TestWebClientShareCredentials(t *testing.T) { req.Header.Set("Content-Type", "application/x-www-form-urlencoded") rr = executeRequest(req) checkResponseCode(t, http.StatusOK, rr) - assert.Contains(t, rr.Body.String(), dataprovider.ErrInvalidCredentials.Error()) + assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCredentials) _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) @@ -13885,13 +13885,13 @@ func TestShareMaxSessions(t *testing.T) { assert.NoError(t, err) rr = executeRequest(req) checkResponseCode(t, http.StatusTooManyRequests, rr) - assert.Contains(t, rr.Body.String(), "too many open sessions") + assert.Contains(t, rr.Body.String(), util.I18nError429Message) req, err = http.NewRequest(http.MethodGet, webClientPubSharesPath+"/"+objectID+"/browse", nil) assert.NoError(t, err) rr = executeRequest(req) - checkResponseCode(t, http.StatusTooManyRequests, rr) - assert.Contains(t, rr.Body.String(), "too many open sessions") + checkResponseCode(t, http.StatusOK, rr) + assert.Contains(t, rr.Body.String(), util.I18nError429Message) req, err = http.NewRequest(http.MethodGet, sharesPath+"/"+objectID+"/files?path=afile", nil) assert.NoError(t, err) @@ -13906,7 +13906,7 @@ func TestShareMaxSessions(t *testing.T) { assert.NoError(t, err) rr = executeRequest(req) checkResponseCode(t, http.StatusTooManyRequests, rr) - assert.Contains(t, rr.Body.String(), "too many open sessions") + assert.Contains(t, rr.Body.String(), util.I18nError429Message) req, err = http.NewRequest(http.MethodGet, sharesPath+"/"+objectID, nil) assert.NoError(t, err) @@ -14131,7 +14131,7 @@ func TestShareReadWrite(t *testing.T) { req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.SetBasicAuth(defaultUsername, defaultPassword) rr = executeRequest(req) - checkResponseCode(t, http.StatusInternalServerError, rr) + checkResponseCode(t, http.StatusBadRequest, rr) // invalid files list form.Set("files", fmt.Sprintf(`[%s]`, testFileName)) req, err = http.NewRequest(http.MethodPost, path.Join(webClientPubSharesPath, objectID, "partial"), @@ -14141,8 +14141,8 @@ func TestShareReadWrite(t *testing.T) { req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.SetBasicAuth(defaultUsername, defaultPassword) rr = executeRequest(req) - checkResponseCode(t, http.StatusInternalServerError, rr) - assert.Contains(t, rr.Body.String(), "Unable to get files list") + checkResponseCode(t, http.StatusBadRequest, rr) + assert.Contains(t, rr.Body.String(), util.I18nError400Message) // missing directory req, err = http.NewRequest(http.MethodPost, path.Join(webClientPubSharesPath, objectID, "partial?path=missing"), bytes.NewBuffer([]byte(form.Encode()))) @@ -14151,8 +14151,8 @@ func TestShareReadWrite(t *testing.T) { req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.SetBasicAuth(defaultUsername, defaultPassword) rr = executeRequest(req) - checkResponseCode(t, http.StatusInternalServerError, rr) - assert.Contains(t, rr.Body.String(), "Unable to get files list") + checkResponseCode(t, http.StatusBadRequest, rr) + assert.Contains(t, rr.Body.String(), util.I18nError400Message) req, err = http.NewRequest(http.MethodPost, path.Join(sharesPath, objectID)+"/"+url.PathEscape("../"+testFileName), bytes.NewBuffer(content)) @@ -14390,7 +14390,7 @@ func TestBrowseShares(t *testing.T) { assert.NoError(t, err) rr = executeRequest(req) checkResponseCode(t, http.StatusForbidden, rr) - assert.Contains(t, rr.Body.String(), "Invalid share scope") + assert.Contains(t, rr.Body.String(), "invalid share scope") req, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, "browse?path=%2F"), nil) assert.NoError(t, err) @@ -14434,7 +14434,7 @@ func TestBrowseShares(t *testing.T) { assert.NoError(t, err) rr = executeRequest(req) checkResponseCode(t, http.StatusBadRequest, rr) - assert.Contains(t, rr.Body.String(), "Invalid share path") + assert.Contains(t, rr.Body.String(), util.I18nErrorPathInvalid) req, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, "dirs?path=%2F.."), nil) assert.NoError(t, err) @@ -14467,7 +14467,7 @@ func TestBrowseShares(t *testing.T) { req.Header.Set("Content-Type", "application/x-www-form-urlencoded") rr = executeRequest(req) checkResponseCode(t, http.StatusBadRequest, rr) - assert.Contains(t, rr.Body.String(), "Invalid share path") + assert.Contains(t, rr.Body.String(), util.I18nErrorPathInvalid) req, err = http.NewRequest(http.MethodGet, path.Join(sharesPath, objectID, "files?path="+testFileName), nil) //nolint:goconst assert.NoError(t, err) @@ -14487,7 +14487,7 @@ func TestBrowseShares(t *testing.T) { assert.NoError(t, err) rr = executeRequest(req) checkResponseCode(t, http.StatusOK, rr) - assert.Contains(t, rr.Body.String(), "file does not exist") + assert.Contains(t, rr.Body.String(), util.I18nErrorFsGeneric) req, err = http.NewRequest(http.MethodGet, path.Join(sharesPath, objectID, "files?path=missing"), nil) assert.NoError(t, err) @@ -14508,7 +14508,7 @@ func TestBrowseShares(t *testing.T) { assert.NoError(t, err) rr = executeRequest(req) checkResponseCode(t, http.StatusOK, rr) - assert.Contains(t, rr.Body.String(), "non regular files are not supported for shares") + assert.Contains(t, rr.Body.String(), util.I18nErrorFsGeneric) req, err = http.NewRequest(http.MethodGet, path.Join(sharesPath, objectID, "files?path="+testFileNameLink), nil) assert.NoError(t, err) @@ -14520,25 +14520,25 @@ func TestBrowseShares(t *testing.T) { assert.NoError(t, err) rr = executeRequest(req) checkResponseCode(t, http.StatusBadRequest, rr) - assert.Contains(t, rr.Body.String(), "does not look like a PDF") + assert.Contains(t, rr.Body.String(), util.I18nErrorPDFMessage) req, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, "getpdf?path=missing"), nil) assert.NoError(t, err) rr = executeRequest(req) checkResponseCode(t, http.StatusBadRequest, rr) - assert.Contains(t, rr.Body.String(), "Unable to get file") + assert.Contains(t, rr.Body.String(), util.I18nErrorFsGeneric) req, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, "getpdf?path=%2F"), nil) assert.NoError(t, err) rr = executeRequest(req) checkResponseCode(t, http.StatusBadRequest, rr) - assert.Contains(t, rr.Body.String(), "is not a file") + assert.Contains(t, rr.Body.String(), util.I18nErrorPDFMessage) req, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, "getpdf?path=%2F.."), nil) assert.NoError(t, err) rr = executeRequest(req) checkResponseCode(t, http.StatusBadRequest, rr) - assert.Contains(t, rr.Body.String(), "Invalid share path") + assert.Contains(t, rr.Body.String(), util.I18nErrorPathInvalid) fakePDF := []byte(`%PDF-1.6`) for i := 0; i < 128; i++ { @@ -14612,13 +14612,13 @@ func TestBrowseShares(t *testing.T) { req.Header.Set("Content-Type", "application/x-www-form-urlencoded") rr = executeRequest(req) checkResponseCode(t, http.StatusBadRequest, rr) - assert.Contains(t, rr.Body.String(), "Unable to validate share") + assert.Contains(t, rr.Body.String(), util.I18nErrorShareBrowseNoDir) req, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, "getpdf?path="+testFileName), nil) assert.NoError(t, err) rr = executeRequest(req) checkResponseCode(t, http.StatusBadRequest, rr) - assert.Contains(t, rr.Body.String(), "Unable to validate share") + assert.Contains(t, rr.Body.String(), util.I18nErrorShareBrowseNoDir) req, err = http.NewRequest(http.MethodGet, path.Join(sharesPath, objectID, "files?path="+testFileName), nil) assert.NoError(t, err) @@ -14635,7 +14635,7 @@ func TestBrowseShares(t *testing.T) { assert.NoError(t, err) rr = executeRequest(req) checkResponseCode(t, http.StatusBadRequest, rr) - assert.Contains(t, rr.Body.String(), "the shared object is not a directory and so it is not browsable") + assert.Contains(t, rr.Body.String(), util.I18nErrorShareBrowseNoDir) // now test a missing shareID objectID = "123456" @@ -14720,7 +14720,7 @@ func TestBrowseShares(t *testing.T) { assert.NoError(t, err) rr = executeRequest(req) checkResponseCode(t, http.StatusBadRequest, rr) - assert.Contains(t, rr.Body.String(), "a share with multiple paths is not browsable") + assert.Contains(t, rr.Body.String(), util.I18nErrorShareBrowsePaths) // share the root path share = dataprovider.Share{ Name: "test share root", @@ -15366,14 +15366,14 @@ func TestWebClientViewPDF(t *testing.T) { setJWTCookieForReq(req, webToken) rr = executeRequest(req) checkResponseCode(t, http.StatusBadRequest, rr) - assert.Contains(t, rr.Body.String(), "Unable to get file") + assert.Contains(t, rr.Body.String(), util.I18nErrorFsGeneric) req, err = http.NewRequest(http.MethodGet, webClientGetPDFPath+"?path=%2F", nil) assert.NoError(t, err) setJWTCookieForReq(req, webToken) rr = executeRequest(req) checkResponseCode(t, http.StatusBadRequest, rr) - assert.Contains(t, rr.Body.String(), "Invalid file") + assert.Contains(t, rr.Body.String(), util.I18nErrorPDFMessage) err = os.WriteFile(filepath.Join(user.GetHomeDir(), "test.pdf"), []byte("some text data"), 0666) assert.NoError(t, err) @@ -15382,8 +15382,8 @@ func TestWebClientViewPDF(t *testing.T) { assert.NoError(t, err) setJWTCookieForReq(req, webToken) rr = executeRequest(req) - checkResponseCode(t, http.StatusBadRequest, rr) - assert.Contains(t, rr.Body.String(), "Invalid PDF file") + checkResponseCode(t, http.StatusInternalServerError, rr) + assert.Contains(t, rr.Body.String(), util.I18nErrorPDFMessage) err = createTestFile(filepath.Join(user.GetHomeDir(), "test.pdf"), 1024) assert.NoError(t, err) @@ -15393,7 +15393,7 @@ func TestWebClientViewPDF(t *testing.T) { setJWTCookieForReq(req, webToken) rr = executeRequest(req) checkResponseCode(t, http.StatusBadRequest, rr) - assert.Contains(t, rr.Body.String(), "does not look like a PDF") + assert.Contains(t, rr.Body.String(), util.I18nErrorPDFMessage) fakePDF := []byte(`%PDF-1.6`) for i := 0; i < 128; i++ { @@ -15421,7 +15421,7 @@ func TestWebClientViewPDF(t *testing.T) { setJWTCookieForReq(req, webToken) rr = executeRequest(req) checkResponseCode(t, http.StatusForbidden, rr) - assert.Contains(t, rr.Body.String(), "Unable to get a reader for the file") + assert.Contains(t, rr.Body.String(), util.I18nError403Message) user.Filters.FilePatterns = []sdk.PatternsFilter{ { @@ -15476,21 +15476,21 @@ func TestWebEditFile(t *testing.T) { setJWTCookieForReq(req, webToken) rr = executeRequest(req) checkResponseCode(t, http.StatusBadRequest, rr) - assert.Contains(t, rr.Body.String(), "exceeds the maximum allowed size") + assert.Contains(t, rr.Body.String(), util.I18nErrorEditSize) req, err = http.NewRequest(http.MethodGet, webClientEditFilePath+"?path=missing", nil) assert.NoError(t, err) setJWTCookieForReq(req, webToken) rr = executeRequest(req) checkResponseCode(t, http.StatusBadRequest, rr) - assert.Contains(t, rr.Body.String(), "Unable to stat file") + assert.Contains(t, rr.Body.String(), util.I18nErrorFsGeneric) req, err = http.NewRequest(http.MethodGet, webClientEditFilePath+"?path=%2F", nil) assert.NoError(t, err) setJWTCookieForReq(req, webToken) rr = executeRequest(req) checkResponseCode(t, http.StatusBadRequest, rr) - assert.Contains(t, rr.Body.String(), "does not point to a file") + assert.Contains(t, rr.Body.String(), util.I18nErrorEditDir) user.Filters.DeniedProtocols = []string{common.ProtocolHTTP} _, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") @@ -15517,7 +15517,7 @@ func TestWebEditFile(t *testing.T) { setJWTCookieForReq(req, webToken) rr = executeRequest(req) checkResponseCode(t, http.StatusForbidden, rr) - assert.Contains(t, rr.Body.String(), "Unable to get a reader") + assert.Contains(t, rr.Body.String(), util.I18nError403Message) _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) @@ -15614,7 +15614,7 @@ func TestWebGetFiles(t *testing.T) { req.Header.Set("Content-Type", "application/x-www-form-urlencoded") setJWTCookieForReq(req, webToken) rr = executeRequest(req) - checkResponseCode(t, http.StatusInternalServerError, rr) + checkResponseCode(t, http.StatusBadRequest, rr) filesList := []string{testFileName, testDir, testFileName + extensions[2]} asJSON, err := json.Marshal(filesList) @@ -15650,8 +15650,8 @@ func TestWebGetFiles(t *testing.T) { req.Header.Set("Content-Type", "application/x-www-form-urlencoded") setJWTCookieForReq(req, webToken) rr = executeRequest(req) - checkResponseCode(t, http.StatusInternalServerError, rr) - assert.Contains(t, rr.Body.String(), "Unable to get files list") + checkResponseCode(t, http.StatusBadRequest, rr) + assert.Contains(t, rr.Body.String(), util.I18nError400Message) req, _ = http.NewRequest(http.MethodGet, webClientDirsPath+"?path=/", nil) //nolint:goconst setJWTCookieForReq(req, webToken) @@ -15675,7 +15675,7 @@ func TestWebGetFiles(t *testing.T) { setJWTCookieForReq(req, webToken) rr = executeRequest(req) checkResponseCode(t, http.StatusNotFound, rr) - assert.Contains(t, rr.Body.String(), "Unable to get directory contents") + assert.Contains(t, rr.Body.String(), util.I18nErrorDirListGeneric) req, _ = http.NewRequest(http.MethodGet, userDirsPath+"?path=missing", nil) setBearerForReq(req, webAPIToken) @@ -16797,7 +16797,7 @@ func TestWebFilesTransferQuotaLimits(t *testing.T) { assert.NoError(t, err) rr = executeRequest(req) checkResponseCode(t, http.StatusForbidden, rr) - assert.Contains(t, rr.Body.String(), "Denying share read due to quota limits") + assert.Contains(t, rr.Body.String(), util.I18nErrorQuotaRead) share2 := dataprovider.Share{ Name: "share2", @@ -17489,28 +17489,29 @@ func TestGetFilesSFTPBackend(t *testing.T) { rr = executeRequest(req) checkResponseCode(t, http.StatusOK, rr) + htmlErrFrag := `div id="errorMsg" class="rounded border-warning border border-dashed bg-light-warning` req, _ = http.NewRequest(http.MethodGet, webClientFilesPath+"?path="+path.Join(testDir, "missing"), nil) setJWTCookieForReq(req, webToken) rr = executeRequest(req) checkResponseCode(t, http.StatusOK, rr) - assert.Contains(t, rr.Body.String(), `div id="errorMsg" class="alert alert-dismissible bg-light-warning`) + assert.Contains(t, rr.Body.String(), htmlErrFrag) req, _ = http.NewRequest(http.MethodGet, webClientFilesPath+"?path=adir/sub", nil) setJWTCookieForReq(req, webToken) rr = executeRequest(req) checkResponseCode(t, http.StatusOK, rr) - assert.Contains(t, rr.Body.String(), `div id="errorMsg" class="alert alert-dismissible bg-light-warning`) + assert.Contains(t, rr.Body.String(), htmlErrFrag) req, _ = http.NewRequest(http.MethodGet, webClientFilesPath+"?path=adir1/afile", nil) setJWTCookieForReq(req, webToken) rr = executeRequest(req) checkResponseCode(t, http.StatusOK, rr) - assert.Contains(t, rr.Body.String(), `div id="errorMsg" class="alert alert-dismissible bg-light-warning`) + assert.Contains(t, rr.Body.String(), htmlErrFrag) req, _ = http.NewRequest(http.MethodGet, webClientFilesPath+"?path=adir2/afile.txt", nil) setJWTCookieForReq(req, webToken) rr = executeRequest(req) checkResponseCode(t, http.StatusOK, rr) - assert.Contains(t, rr.Body.String(), `div id="errorMsg" class="alert alert-dismissible bg-light-warning`) + assert.Contains(t, rr.Body.String(), htmlErrFrag) req, _ = http.NewRequest(http.MethodGet, webClientFilesPath+"?path="+testFileName, nil) setJWTCookieForReq(req, webToken) @@ -17573,7 +17574,7 @@ func TestClientUserClose(t *testing.T) { setJWTCookieForReq(req, webToken) rr := executeRequest(req) checkResponseCode(t, http.StatusInternalServerError, rr) - assert.Contains(t, rr.Body.String(), "Unable to read the file") + assert.Contains(t, rr.Body.String(), util.I18nError500Message) }() wg.Add(1) go func() { @@ -18015,7 +18016,7 @@ func TestWebUserShare(t *testing.T) { setJWTCookieForReq(req, token) rr := executeRequest(req) checkResponseCode(t, http.StatusOK, rr) - assert.Contains(t, rr.Body.String(), "cannot parse") + assert.Contains(t, rr.Body.String(), util.I18nErrorShareExpiration) form.Set("expiration_date", util.GetTimeFromMsecSinceEpoch(share.ExpiresAt).UTC().Format("2006-01-02 15:04:05")) form.Set("scope", "") // invalid scope @@ -18026,7 +18027,7 @@ func TestWebUserShare(t *testing.T) { setJWTCookieForReq(req, token) rr = executeRequest(req) checkResponseCode(t, http.StatusOK, rr) - assert.Contains(t, rr.Body.String(), "invalid syntax") + assert.Contains(t, rr.Body.String(), util.I18nErrorShareScope) form.Set("scope", strconv.Itoa(int(share.Scope))) // invalid max tokens form.Set("max_tokens", "t") @@ -18037,7 +18038,7 @@ func TestWebUserShare(t *testing.T) { setJWTCookieForReq(req, token) rr = executeRequest(req) checkResponseCode(t, http.StatusOK, rr) - assert.Contains(t, rr.Body.String(), "invalid syntax") + assert.Contains(t, rr.Body.String(), util.I18nErrorShareMaxTokens) form.Set("max_tokens", strconv.Itoa(share.MaxTokens)) // no csrf token req, err = http.NewRequest(http.MethodPost, webClientSharePath, bytes.NewBuffer([]byte(form.Encode()))) @@ -18047,7 +18048,7 @@ func TestWebUserShare(t *testing.T) { setJWTCookieForReq(req, token) rr = executeRequest(req) checkResponseCode(t, http.StatusForbidden, rr) - assert.Contains(t, rr.Body.String(), "unable to verify form token") + assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCSRF) form.Set(csrfFormToken, csrfToken) form.Set("scope", "100") @@ -18058,7 +18059,7 @@ func TestWebUserShare(t *testing.T) { setJWTCookieForReq(req, token) rr = executeRequest(req) checkResponseCode(t, http.StatusOK, rr) - assert.Contains(t, rr.Body.String(), "Validation error: invalid scope") + assert.Contains(t, rr.Body.String(), util.I18nErrorShareScope) form.Set("scope", strconv.Itoa(int(share.Scope))) req, err = http.NewRequest(http.MethodPost, webClientSharePath, bytes.NewBuffer([]byte(form.Encode()))) @@ -18106,7 +18107,7 @@ func TestWebUserShare(t *testing.T) { setJWTCookieForReq(req, token) rr = executeRequest(req) checkResponseCode(t, http.StatusOK, rr) - assert.Contains(t, rr.Body.String(), "cannot parse") + assert.Contains(t, rr.Body.String(), util.I18nErrorShareExpiration) form.Set("expiration_date", "") form.Set(csrfFormToken, "") @@ -18117,7 +18118,7 @@ func TestWebUserShare(t *testing.T) { setJWTCookieForReq(req, token) rr = executeRequest(req) checkResponseCode(t, http.StatusForbidden, rr) - assert.Contains(t, rr.Body.String(), "unable to verify form token") + assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCSRF) form.Set(csrfFormToken, csrfToken) form.Set("allowed_ip", "1.1.1") @@ -18128,7 +18129,7 @@ func TestWebUserShare(t *testing.T) { setJWTCookieForReq(req, token) rr = executeRequest(req) checkResponseCode(t, http.StatusOK, rr) - assert.Contains(t, rr.Body.String(), "Validation error: could not parse allow from entry") + assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidIPMask) form.Set("allowed_ip", "") req, err = http.NewRequest(http.MethodPost, webClientSharePath+"/"+share.ShareID, bytes.NewBuffer([]byte(form.Encode()))) @@ -18170,7 +18171,7 @@ func TestWebUserShare(t *testing.T) { setJWTCookieForReq(req, token) rr = executeRequest(req) checkResponseCode(t, http.StatusBadRequest, rr) - assert.Contains(t, rr.Body.String(), "Invalid share list") + assert.Contains(t, rr.Body.String(), util.I18nError400Message) req, err = http.NewRequest(http.MethodGet, webClientSharePath+"?path=%2F&files=%5B\"adir\"%5D", nil) assert.NoError(t, err) @@ -18246,8 +18247,8 @@ func TestWebUserShareNoPasswordDisabled(t *testing.T) { req.Header.Set("Content-Type", "application/x-www-form-urlencoded") setJWTCookieForReq(req, token) rr := executeRequest(req) - checkResponseCode(t, http.StatusForbidden, rr) - assert.Contains(t, rr.Body.String(), "You are not authorized to share files/folders without a password") + checkResponseCode(t, http.StatusOK, rr) + assert.Contains(t, rr.Body.String(), util.I18nErrorShareNoPwd) form.Set("password", defaultPassword) req, err = http.NewRequest(http.MethodPost, webClientSharePath, bytes.NewBuffer([]byte(form.Encode()))) @@ -18287,8 +18288,8 @@ func TestWebUserShareNoPasswordDisabled(t *testing.T) { req.Header.Set("Content-Type", "application/x-www-form-urlencoded") setJWTCookieForReq(req, token) rr = executeRequest(req) - checkResponseCode(t, http.StatusForbidden, rr) - assert.Contains(t, rr.Body.String(), "You are not authorized to share files/folders without a password") + checkResponseCode(t, http.StatusOK, rr) + assert.Contains(t, rr.Body.String(), util.I18nErrorShareNoPwd) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) @@ -18322,7 +18323,7 @@ func TestWebUserProfile(t *testing.T) { setJWTCookieForReq(req, token) rr := executeRequest(req) checkResponseCode(t, http.StatusForbidden, rr) - assert.Contains(t, rr.Body.String(), "unable to verify form token") + assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidCSRF) form.Set(csrfFormToken, csrfToken) req, _ = http.NewRequest(http.MethodPost, webClientProfilePath, bytes.NewBuffer([]byte(form.Encode()))) @@ -18331,7 +18332,7 @@ func TestWebUserProfile(t *testing.T) { setJWTCookieForReq(req, token) rr = executeRequest(req) checkResponseCode(t, http.StatusOK, rr) - assert.Contains(t, rr.Body.String(), "Your profile has been successfully updated") + assert.Contains(t, rr.Body.String(), util.I18nProfileUpdated) user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) @@ -18348,7 +18349,7 @@ func TestWebUserProfile(t *testing.T) { setJWTCookieForReq(req, token) rr = executeRequest(req) checkResponseCode(t, http.StatusOK, rr) - assert.Contains(t, rr.Body.String(), "Validation error: email") + assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidEmail) // invalid public key form.Set("email", email) form.Set("public_keys[0][public_key]", "invalid") @@ -18358,7 +18359,7 @@ func TestWebUserProfile(t *testing.T) { setJWTCookieForReq(req, token) rr = executeRequest(req) checkResponseCode(t, http.StatusOK, rr) - assert.Contains(t, rr.Body.String(), "Validation error: could not parse key") + assert.Contains(t, rr.Body.String(), util.I18nErrorPubKeyInvalid) // now remove permissions form.Set("public_keys[0][public_key]", testPubKey) form.Del("public_keys[1][public_key]") @@ -18376,7 +18377,7 @@ func TestWebUserProfile(t *testing.T) { setJWTCookieForReq(req, token) rr = executeRequest(req) checkResponseCode(t, http.StatusOK, rr) - assert.Contains(t, rr.Body.String(), "Your profile has been successfully updated") + assert.Contains(t, rr.Body.String(), util.I18nProfileUpdated) user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.True(t, user.Filters.AllowAPIKeyAuth) @@ -18397,7 +18398,7 @@ func TestWebUserProfile(t *testing.T) { setJWTCookieForReq(req, token) rr = executeRequest(req) checkResponseCode(t, http.StatusOK, rr) - assert.Contains(t, rr.Body.String(), "Your profile has been successfully updated") + assert.Contains(t, rr.Body.String(), util.I18nProfileUpdated) user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.True(t, user.Filters.AllowAPIKeyAuth) @@ -18418,7 +18419,7 @@ func TestWebUserProfile(t *testing.T) { setJWTCookieForReq(req, token) rr = executeRequest(req) checkResponseCode(t, http.StatusOK, rr) - assert.Contains(t, rr.Body.String(), "Your profile has been successfully updated") + assert.Contains(t, rr.Body.String(), util.I18nProfileUpdated) user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) assert.True(t, user.Filters.AllowAPIKeyAuth) @@ -24086,7 +24087,7 @@ func TestAdminForgotPassword(t *testing.T) { req.Header.Set("Content-Type", "application/x-www-form-urlencoded") rr = executeRequest(req) assert.Equal(t, http.StatusOK, rr.Code) - assert.Contains(t, rr.Body.String(), "Unable to send confirmation code via email") + assert.Contains(t, rr.Body.String(), "Error sending confirmation code via email") smtpCfg = smtp.Config{} err = smtpCfg.Initialize(configDir, true) @@ -24166,7 +24167,7 @@ func TestUserForgotPassword(t *testing.T) { req.Header.Set("Content-Type", "application/x-www-form-urlencoded") rr = executeRequest(req) assert.Equal(t, http.StatusOK, rr.Code) - assert.Contains(t, rr.Body.String(), "username is mandatory") + assert.Contains(t, rr.Body.String(), util.I18nErrorUsernameRequired) // user cannot reset the password form.Set("username", user.Username) req, err = http.NewRequest(http.MethodPost, webClientForgotPwdPath, bytes.NewBuffer([]byte(form.Encode()))) @@ -24175,7 +24176,7 @@ func TestUserForgotPassword(t *testing.T) { req.Header.Set("Content-Type", "application/x-www-form-urlencoded") rr = executeRequest(req) assert.Equal(t, http.StatusOK, rr.Code) - assert.Contains(t, rr.Body.String(), "you are not allowed to reset your password") + assert.Contains(t, rr.Body.String(), util.I18nErrorPwdResetForbidded) user.Filters.WebClient = []string{sdk.WebClientAPIKeyAuthChangeDisabled} user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) @@ -24206,7 +24207,7 @@ func TestUserForgotPassword(t *testing.T) { req.Header.Set("Content-Type", "application/x-www-form-urlencoded") rr = executeRequest(req) assert.Equal(t, http.StatusOK, rr.Code) - assert.Contains(t, rr.Body.String(), "please set a password") + assert.Contains(t, rr.Body.String(), util.I18nErrorChangePwdGeneric) // passwords mismatch form.Set("password", altAdminPassword) req, err = http.NewRequest(http.MethodPost, webClientResetPwdPath, bytes.NewBuffer([]byte(form.Encode()))) @@ -24215,7 +24216,7 @@ func TestUserForgotPassword(t *testing.T) { req.Header.Set("Content-Type", "application/x-www-form-urlencoded") rr = executeRequest(req) assert.Equal(t, http.StatusOK, rr.Code) - assert.Contains(t, rr.Body.String(), "The two password fields do not match") + assert.Contains(t, rr.Body.String(), util.I18nErrorChangePwdNoMatch) // no code form.Set("confirm_password", altAdminPassword) req, err = http.NewRequest(http.MethodPost, webClientResetPwdPath, bytes.NewBuffer([]byte(form.Encode()))) @@ -24224,7 +24225,7 @@ func TestUserForgotPassword(t *testing.T) { req.Header.Set("Content-Type", "application/x-www-form-urlencoded") rr = executeRequest(req) assert.Equal(t, http.StatusOK, rr.Code) - assert.Contains(t, rr.Body.String(), "please set a confirmation code") + assert.Contains(t, rr.Body.String(), util.I18nErrorChangePwdGeneric) // ok form.Set("code", lastResetCode) req, err = http.NewRequest(http.MethodPost, webClientResetPwdPath, bytes.NewBuffer([]byte(form.Encode()))) @@ -24276,7 +24277,7 @@ func TestUserForgotPassword(t *testing.T) { req.Header.Set("Content-Type", "application/x-www-form-urlencoded") rr = executeRequest(req) assert.Equal(t, http.StatusOK, rr.Code) - assert.Contains(t, rr.Body.String(), "Unable to associate the confirmation code with an existing user") + assert.Contains(t, rr.Body.String(), util.I18nErrorChangePwdGeneric) } func TestAPIForgotPassword(t *testing.T) { diff --git a/internal/httpd/internal_test.go b/internal/httpd/internal_test.go index 31d0093f..7af3971f 100644 --- a/internal/httpd/internal_test.go +++ b/internal/httpd/internal_test.go @@ -25,6 +25,7 @@ import ( "fmt" "html/template" "io" + "io/fs" "net/http" "net/http/httptest" "net/url" @@ -1215,7 +1216,7 @@ func TestCreateShareCookieError(t *testing.T) { req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) server.handleClientShareLoginPost(rr, req) assert.Equal(t, http.StatusOK, rr.Code, rr.Body.String()) - assert.Contains(t, rr.Body.String(), common.ErrInternalFailure.Error()) + assert.Contains(t, rr.Body.String(), util.I18nError500Message) err = dataprovider.DeleteUser(username, "", "", "") assert.NoError(t, err) @@ -1316,7 +1317,7 @@ func TestCreateTokenError(t *testing.T) { rr = httptest.NewRecorder() server.handleWebClientChangePwdPost(rr, req) assert.Equal(t, http.StatusOK, rr.Code, rr.Body.String()) - assert.Contains(t, rr.Body.String(), "invalid URL escape") + assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidForm) req, _ = http.NewRequest(http.MethodPost, webClientProfilePath+"?a=a%C3%AO%GB", bytes.NewBuffer([]byte(form.Encode()))) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") @@ -1349,14 +1350,14 @@ func TestCreateTokenError(t *testing.T) { rr = httptest.NewRecorder() server.handleWebClientTwoFactorPost(rr, req) assert.Equal(t, http.StatusOK, rr.Code, rr.Body.String()) - assert.Contains(t, rr.Body.String(), "invalid URL escape") + assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidForm) req, _ = http.NewRequest(http.MethodPost, webClientTwoFactorRecoveryPath+"?a=a%C3%AO%GD", bytes.NewBuffer([]byte(form.Encode()))) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") rr = httptest.NewRecorder() server.handleWebClientTwoFactorRecoveryPost(rr, req) assert.Equal(t, http.StatusOK, rr.Code, rr.Body.String()) - assert.Contains(t, rr.Body.String(), "invalid URL escape") + assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidForm) req, _ = http.NewRequest(http.MethodPost, webAdminForgotPwdPath+"?a=a%C3%A1%GD", bytes.NewBuffer([]byte(form.Encode()))) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") @@ -1370,7 +1371,7 @@ func TestCreateTokenError(t *testing.T) { rr = httptest.NewRecorder() server.handleWebClientForgotPwdPost(rr, req) assert.Equal(t, http.StatusOK, rr.Code, rr.Body.String()) - assert.Contains(t, rr.Body.String(), "invalid URL escape") + assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidForm) req, _ = http.NewRequest(http.MethodPost, webAdminResetPwdPath+"?a=a%C3%AO%JD", bytes.NewBuffer([]byte(form.Encode()))) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") @@ -1391,7 +1392,7 @@ func TestCreateTokenError(t *testing.T) { rr = httptest.NewRecorder() server.handleWebClientPasswordResetPost(rr, req) assert.Equal(t, http.StatusOK, rr.Code, rr.Body.String()) - assert.Contains(t, rr.Body.String(), "invalid URL escape") + assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidForm) req, _ = http.NewRequest(http.MethodPost, webChangeClientPwdPath+"?a=a%K3%AO%GA", bytes.NewBuffer([]byte(form.Encode()))) _, err = getShareFromPostFields(req) @@ -2552,7 +2553,7 @@ func TestChangeUserPwd(t *testing.T) { } err = doChangeUserPassword(req, "a", "b", "b") if assert.Error(t, err) { - assert.Contains(t, err.Error(), "invalid token claims") + assert.Contains(t, err.Error(), errInvalidTokenClaims.Error()) } } @@ -2579,70 +2580,70 @@ func TestWebUserInvalidClaims(t *testing.T) { req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"])) server.handleClientGetFiles(rr, req) assert.Equal(t, http.StatusForbidden, rr.Code) - assert.Contains(t, rr.Body.String(), "Invalid token claims") + assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidToken) rr = httptest.NewRecorder() req, _ = http.NewRequest(http.MethodGet, webClientDirsPath, nil) req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"])) server.handleClientGetDirContents(rr, req) assert.Equal(t, http.StatusForbidden, rr.Code) - assert.Contains(t, rr.Body.String(), "invalid token claims") + assert.Contains(t, rr.Body.String(), util.I18nErrorDirList403) rr = httptest.NewRecorder() req, _ = http.NewRequest(http.MethodGet, webClientDownloadZipPath, nil) req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"])) server.handleWebClientDownloadZip(rr, req) assert.Equal(t, http.StatusForbidden, rr.Code) - assert.Contains(t, rr.Body.String(), "Invalid token claims") + assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidToken) rr = httptest.NewRecorder() req, _ = http.NewRequest(http.MethodGet, webClientEditFilePath, nil) req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"])) server.handleClientEditFile(rr, req) assert.Equal(t, http.StatusForbidden, rr.Code) - assert.Contains(t, rr.Body.String(), "Invalid token claims") + assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidToken) rr = httptest.NewRecorder() req, _ = http.NewRequest(http.MethodGet, webClientSharePath, nil) req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"])) server.handleClientAddShareGet(rr, req) assert.Equal(t, http.StatusForbidden, rr.Code) - assert.Contains(t, rr.Body.String(), "Invalid token claims") + assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidToken) rr = httptest.NewRecorder() req, _ = http.NewRequest(http.MethodGet, webClientSharePath, nil) req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"])) server.handleClientUpdateShareGet(rr, req) assert.Equal(t, http.StatusForbidden, rr.Code) - assert.Contains(t, rr.Body.String(), "Invalid token claims") + assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidToken) rr = httptest.NewRecorder() req, _ = http.NewRequest(http.MethodPost, webClientSharePath, nil) req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"])) server.handleClientAddSharePost(rr, req) assert.Equal(t, http.StatusForbidden, rr.Code) - assert.Contains(t, rr.Body.String(), "Invalid token claims") + assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidToken) rr = httptest.NewRecorder() req, _ = http.NewRequest(http.MethodPost, webClientSharePath+"/id", nil) req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"])) server.handleClientUpdateSharePost(rr, req) assert.Equal(t, http.StatusForbidden, rr.Code) - assert.Contains(t, rr.Body.String(), "Invalid token claims") + assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidToken) rr = httptest.NewRecorder() req, _ = http.NewRequest(http.MethodGet, webClientSharesPath, nil) req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"])) server.handleClientGetShares(rr, req) assert.Equal(t, http.StatusForbidden, rr.Code) - assert.Contains(t, rr.Body.String(), "Invalid token claims") + assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidToken) rr = httptest.NewRecorder() req, _ = http.NewRequest(http.MethodGet, webClientViewPDFPath, nil) req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"])) server.handleClientGetPDF(rr, req) assert.Equal(t, http.StatusForbidden, rr.Code) - assert.Contains(t, rr.Body.String(), "Invalid token claims") + assert.Contains(t, rr.Body.String(), util.I18nErrorInvalidToken) } func TestInvalidClaims(t *testing.T) { @@ -3511,6 +3512,34 @@ func TestShareRedirectURL(t *testing.T) { assert.Empty(t, res) } +func TestI18NMessages(t *testing.T) { + msg := i18nListDirMsg(http.StatusForbidden) + require.Equal(t, util.I18nErrorDirList403, msg) + msg = i18nListDirMsg(http.StatusInternalServerError) + require.Equal(t, util.I18nErrorDirListGeneric, msg) + msg = i18nFsMsg(http.StatusForbidden) + require.Equal(t, util.I18nError403Message, msg) + msg = i18nFsMsg(http.StatusInternalServerError) + require.Equal(t, util.I18nErrorFsGeneric, msg) +} + +func TestI18NErrors(t *testing.T) { + err := util.NewValidationError("error text") + errI18n := util.NewI18nError(err, util.I18nError500Message) + assert.ErrorIs(t, errI18n, util.ErrValidation) + assert.Equal(t, err.Error(), errI18n.Error()) + assert.Equal(t, util.I18nError500Message, getI18NErrorString(errI18n, "")) + err2 := util.NewI18nError(fs.ErrNotExist, util.I18nError500Message) + assert.ErrorIs(t, err2, &util.I18nError{}) + assert.ErrorIs(t, err2, fs.ErrNotExist) + assert.NotErrorIs(t, err2, fs.ErrExist) + assert.Equal(t, util.I18nError403Message, getI18NErrorString(fs.ErrClosed, util.I18nError403Message)) + errorString := getI18NErrorString(nil, util.I18nError500Message) + assert.Equal(t, util.I18nError500Message, errorString) + errI18nWrap := util.NewI18nError(errI18n, util.I18nError404Message) + assert.Equal(t, util.I18nError500Message, errI18nWrap.I18nMessage) +} + func isSharedProviderSupported() bool { // SQLite shares the implementation with other SQL-based provider but it makes no sense // to use it outside test cases diff --git a/internal/httpd/middleware.go b/internal/httpd/middleware.go index e3029a31..5cba7b67 100644 --- a/internal/httpd/middleware.go +++ b/internal/httpd/middleware.go @@ -204,7 +204,7 @@ func (s *httpdServer) checkHTTPUserPerm(perm string) func(next http.Handler) htt // for web client perms are negated and not granted if tokenClaims.hasPerm(perm) { if isWebRequest(r) { - s.renderClientForbiddenPage(w, r, "You don't have permission for this action") + s.renderClientForbiddenPage(w, r, errors.New("you don't have permission for this action")) } else { sendAPIResponse(w, r, nil, http.StatusText(http.StatusForbidden), http.StatusForbidden) } @@ -231,17 +231,24 @@ func (s *httpdServer) checkAuthRequirements(next http.Handler) http.Handler { tokenClaims := jwtTokenClaims{} tokenClaims.Decode(claims) if tokenClaims.MustSetTwoFactorAuth || tokenClaims.MustChangePassword { - var message string + var err error if tokenClaims.MustSetTwoFactorAuth { - message = fmt.Sprintf("Two-factor authentication requirements not met, please configure two-factor authentication for the following protocols: %v", - strings.Join(tokenClaims.RequiredTwoFactorProtocols, ", ")) + err = util.NewI18nError( + util.NewGenericError( + fmt.Sprintf("Two-factor authentication requirements not met, please configure two-factor authentication for the following protocols: %v", + strings.Join(tokenClaims.RequiredTwoFactorProtocols, ", "))), + util.I18nError2FARequired, + ) } else { - message = "Password change required. Please set a new password to continue to use your account" + err = util.NewI18nError( + util.NewGenericError("Password change required. Please set a new password to continue to use your account"), + util.I18nErrorChangePwdRequired, + ) } if isWebRequest(r) { - s.renderClientForbiddenPage(w, r, message) + s.renderClientForbiddenPage(w, r, err) } else { - sendAPIResponse(w, r, nil, message, http.StatusForbidden) + sendAPIResponse(w, r, err, "", http.StatusForbidden) } return } @@ -254,7 +261,10 @@ func (s *httpdServer) requireBuiltinLogin(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if isLoggedInWithOIDC(r) { if isWebClientRequest(r) { - s.renderClientForbiddenPage(w, r, "This feature is not available if you are logged in with OpenID") + s.renderClientForbiddenPage(w, r, util.NewI18nError( + util.NewGenericError("This feature is not available if you are logged in with OpenID"), + util.I18nErrorNoOIDCFeature, + )) } else { s.renderForbiddenPage(w, r, "This feature is not available if you are logged in with OpenID") } diff --git a/internal/httpd/oidc.go b/internal/httpd/oidc.go index 5d4b55fa..0bceae81 100644 --- a/internal/httpd/oidc.go +++ b/internal/httpd/oidc.go @@ -587,8 +587,8 @@ func (s *httpdServer) handleOIDCRedirect(w http.ResponseWriter, r *http.Request) authReq, err := oidcMgr.getPendingAuth(state) if err != nil { logger.Debug(logSender, "", "oidc authentication state did not match") - s.renderClientMessagePage(w, r, "Invalid authentication request", "Authentication state did not match", - http.StatusBadRequest, nil, "") + s.renderClientMessagePage(w, r, util.I18nInvalidAuthReqTitle, http.StatusBadRequest, + util.NewI18nError(err, util.I18nInvalidAuth), "") return } oidcMgr.removePendingAuth(state) diff --git a/internal/httpd/oidc_test.go b/internal/httpd/oidc_test.go index adde9906..27b5b6f1 100644 --- a/internal/httpd/oidc_test.go +++ b/internal/httpd/oidc_test.go @@ -148,7 +148,7 @@ func TestOIDCLoginLogout(t *testing.T) { assert.NoError(t, err) server.router.ServeHTTP(rr, r) assert.Equal(t, http.StatusBadRequest, rr.Code) - assert.Contains(t, rr.Body.String(), "Authentication state did not match") + assert.Contains(t, rr.Body.String(), util.I18nInvalidAuth) expiredAuthReq := oidcPendingAuth{ State: xid.New().String(), @@ -162,7 +162,7 @@ func TestOIDCLoginLogout(t *testing.T) { assert.NoError(t, err) server.router.ServeHTTP(rr, r) assert.Equal(t, http.StatusBadRequest, rr.Code) - assert.Contains(t, rr.Body.String(), "Authentication state did not match") + assert.Contains(t, rr.Body.String(), util.I18nInvalidAuth) oidcMgr.removePendingAuth(expiredAuthReq.State) server.binding.OIDC.oauth2Config = &mockOAuth2Config{ diff --git a/internal/httpd/server.go b/internal/httpd/server.go index ce6bfce1..525602fd 100644 --- a/internal/httpd/server.go +++ b/internal/httpd/server.go @@ -163,8 +163,8 @@ func (s *httpdServer) refreshCookie(next http.Handler) http.Handler { func (s *httpdServer) renderClientLoginPage(w http.ResponseWriter, r *http.Request, error, ip string) { data := loginPage{ commonBasePage: getCommonBasePage(r), + Title: util.I18nLoginTitle, CurrentURL: webClientLoginPath, - Version: version.Get().Version, Error: error, CSRFToken: createCSRFToken(ip), Branding: s.binding.Branding.WebClient, @@ -198,17 +198,17 @@ func (s *httpdServer) handleWebClientLogout(w http.ResponseWriter, r *http.Reque func (s *httpdServer) handleWebClientChangePwdPost(w http.ResponseWriter, r *http.Request) { r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) if err := r.ParseForm(); err != nil { - s.renderClientChangePasswordPage(w, r, err.Error()) + s.renderClientChangePasswordPage(w, r, util.I18nErrorInvalidForm) return } if err := verifyCSRFToken(r.Form.Get(csrfFormToken), util.GetIPFromRemoteAddress(r.RemoteAddr)); err != nil { - s.renderClientForbiddenPage(w, r, err.Error()) + s.renderClientForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF)) return } err := doChangeUserPassword(r, strings.TrimSpace(r.Form.Get("current_password")), strings.TrimSpace(r.Form.Get("new_password1")), strings.TrimSpace(r.Form.Get("new_password2"))) if err != nil { - s.renderClientChangePasswordPage(w, r, err.Error()) + s.renderClientChangePasswordPage(w, r, getI18NErrorString(err, util.I18nErrorChangePwdGeneric)) return } s.handleWebClientLogout(w, r) @@ -228,7 +228,7 @@ func (s *httpdServer) handleWebClientLoginPost(w http.ResponseWriter, r *http.Re ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr) if err := r.ParseForm(); err != nil { - s.renderClientLoginPage(w, r, err.Error(), ipAddr) + s.renderClientLoginPage(w, r, util.I18nErrorInvalidForm, ipAddr) return } protocol := common.ProtocolHTTP @@ -237,33 +237,33 @@ func (s *httpdServer) handleWebClientLoginPost(w http.ResponseWriter, r *http.Re if username == "" || password == "" { updateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}}, dataprovider.LoginMethodPassword, ipAddr, common.ErrNoCredentials) - s.renderClientLoginPage(w, r, "Invalid credentials", ipAddr) + s.renderClientLoginPage(w, r, util.I18nErrorInvalidCredentials, ipAddr) return } if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil { updateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}}, dataprovider.LoginMethodPassword, ipAddr, err) - s.renderClientLoginPage(w, r, err.Error(), ipAddr) + s.renderClientLoginPage(w, r, util.I18nErrorInvalidCSRF, ipAddr) return } if err := common.Config.ExecutePostConnectHook(ipAddr, protocol); err != nil { updateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}}, dataprovider.LoginMethodPassword, ipAddr, err) - s.renderClientLoginPage(w, r, fmt.Sprintf("access denied: %v", err), ipAddr) + s.renderClientLoginPage(w, r, util.I18nError403Message, ipAddr) return } user, err := dataprovider.CheckUserAndPass(username, password, ipAddr, protocol) if err != nil { updateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, err) - s.renderClientLoginPage(w, r, dataprovider.ErrInvalidCredentials.Error(), ipAddr) + s.renderClientLoginPage(w, r, util.I18nErrorInvalidCredentials, ipAddr) return } connectionID := fmt.Sprintf("%v_%v", protocol, xid.New().String()) if err := checkHTTPClientUser(&user, r, connectionID, true); err != nil { updateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, err) - s.renderClientLoginPage(w, r, err.Error(), ipAddr) + s.renderClientLoginPage(w, r, getI18NErrorString(err, util.I18nError403Message), ipAddr) return } @@ -272,7 +272,7 @@ func (s *httpdServer) handleWebClientLoginPost(w http.ResponseWriter, r *http.Re if err != nil { logger.Warn(logSender, connectionID, "unable to check fs root: %v", err) updateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, common.ErrInternalFailure) - s.renderClientLoginPage(w, r, err.Error(), ipAddr) + s.renderClientLoginPage(w, r, getI18NErrorString(err, util.I18nErrorFsGeneric), ipAddr) return } s.loginUser(w, r, &user, connectionID, ipAddr, false, s.renderClientLoginPage) @@ -284,28 +284,28 @@ func (s *httpdServer) handleWebClientPasswordResetPost(w http.ResponseWriter, r ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr) err := r.ParseForm() if err != nil { - s.renderClientResetPwdPage(w, r, err.Error(), ipAddr) + s.renderClientResetPwdPage(w, r, util.I18nErrorInvalidForm, ipAddr) return } if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil { - s.renderClientForbiddenPage(w, r, err.Error()) + s.renderClientForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF)) return } newPassword := strings.TrimSpace(r.Form.Get("password")) confirmPassword := strings.TrimSpace(r.Form.Get("confirm_password")) if newPassword != confirmPassword { - s.renderClientResetPwdPage(w, r, "The two password fields do not match", ipAddr) + s.renderClientResetPwdPage(w, r, util.I18nErrorChangePwdNoMatch, ipAddr) return } _, user, err := handleResetPassword(r, strings.TrimSpace(r.Form.Get("code")), newPassword, false) if err != nil { - s.renderClientResetPwdPage(w, r, err.Error(), ipAddr) + s.renderClientResetPwdPage(w, r, getI18NErrorString(err, util.I18nErrorChangePwdGeneric), ipAddr) return } connectionID := fmt.Sprintf("%v_%v", getProtocolFromRequest(r), xid.New().String()) if err := checkHTTPClientUser(user, r, connectionID, true); err != nil { - s.renderClientResetPwdPage(w, r, fmt.Sprintf("Password reset successfully but unable to login: %v", err.Error()), ipAddr) + s.renderClientResetPwdPage(w, r, getI18NErrorString(err, util.I18nErrorDirList403), ipAddr) return } @@ -313,7 +313,7 @@ func (s *httpdServer) handleWebClientPasswordResetPost(w http.ResponseWriter, r err = user.CheckFsRoot(connectionID) if err != nil { logger.Warn(logSender, connectionID, "unable to check fs root: %v", err) - s.renderClientResetPwdPage(w, r, fmt.Sprintf("Password reset successfully but unable to login: %s", err.Error()), ipAddr) + s.renderClientResetPwdPage(w, r, util.I18nErrorLoginAfterReset, ipAddr) return } s.loginUser(w, r, user, connectionID, ipAddr, false, s.renderClientResetPwdPage) @@ -328,17 +328,17 @@ func (s *httpdServer) handleWebClientTwoFactorRecoveryPost(w http.ResponseWriter } ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr) if err := r.ParseForm(); err != nil { - s.renderClientTwoFactorRecoveryPage(w, r, err.Error(), ipAddr) + s.renderClientTwoFactorRecoveryPage(w, r, util.I18nErrorInvalidForm, ipAddr) return } username := claims.Username recoveryCode := strings.TrimSpace(r.Form.Get("recovery_code")) if username == "" || recoveryCode == "" { - s.renderClientTwoFactorRecoveryPage(w, r, "Invalid credentials", ipAddr) + s.renderClientTwoFactorRecoveryPage(w, r, util.I18nErrorInvalidCredentials, ipAddr) return } if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil { - s.renderClientTwoFactorRecoveryPage(w, r, err.Error(), ipAddr) + s.renderClientTwoFactorRecoveryPage(w, r, util.I18nErrorInvalidCSRF, ipAddr) return } user, userMerged, err := dataprovider.GetUserVariants(username, "") @@ -346,7 +346,7 @@ func (s *httpdServer) handleWebClientTwoFactorRecoveryPost(w http.ResponseWriter if errors.Is(err, util.ErrNotFound) { handleDefenderEventLoginFailed(ipAddr, err) //nolint:errcheck } - s.renderClientTwoFactorRecoveryPage(w, r, "Invalid credentials", ipAddr) + s.renderClientTwoFactorRecoveryPage(w, r, util.I18nErrorInvalidCredentials, ipAddr) return } if !userMerged.Filters.TOTPConfig.Enabled || !util.Contains(userMerged.Filters.TOTPConfig.Protocols, common.ProtocolHTTP) { @@ -360,7 +360,7 @@ func (s *httpdServer) handleWebClientTwoFactorRecoveryPost(w http.ResponseWriter } if code.Secret.GetPayload() == recoveryCode { if code.Used { - s.renderClientTwoFactorRecoveryPage(w, r, "This recovery code was already used", ipAddr) + s.renderClientTwoFactorRecoveryPage(w, r, util.I18nErrorInvalidCredentials, ipAddr) return } user.Filters.RecoveryCodes[idx].Used = true @@ -377,7 +377,7 @@ func (s *httpdServer) handleWebClientTwoFactorRecoveryPost(w http.ResponseWriter } } handleDefenderEventLoginFailed(ipAddr, dataprovider.ErrInvalidCredentials) //nolint:errcheck - s.renderClientTwoFactorRecoveryPage(w, r, "Invalid recovery code", ipAddr) + s.renderClientTwoFactorRecoveryPage(w, r, util.I18nErrorInvalidCredentials, ipAddr) } func (s *httpdServer) handleWebClientTwoFactorPost(w http.ResponseWriter, r *http.Request) { @@ -389,7 +389,7 @@ func (s *httpdServer) handleWebClientTwoFactorPost(w http.ResponseWriter, r *htt } ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr) if err := r.ParseForm(); err != nil { - s.renderClientTwoFactorPage(w, r, err.Error(), ipAddr) + s.renderClientTwoFactorPage(w, r, util.I18nErrorInvalidForm, ipAddr) return } username := claims.Username @@ -397,25 +397,25 @@ func (s *httpdServer) handleWebClientTwoFactorPost(w http.ResponseWriter, r *htt if username == "" || passcode == "" { updateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}}, dataprovider.LoginMethodPassword, ipAddr, common.ErrNoCredentials) - s.renderClientTwoFactorPage(w, r, "Invalid credentials", ipAddr) + s.renderClientTwoFactorPage(w, r, util.I18nErrorInvalidCredentials, ipAddr) return } if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil { updateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}}, dataprovider.LoginMethodPassword, ipAddr, err) - s.renderClientTwoFactorPage(w, r, err.Error(), ipAddr) + s.renderClientTwoFactorPage(w, r, util.I18nErrorInvalidCSRF, ipAddr) return } user, err := dataprovider.GetUserWithGroupSettings(username, "") if err != nil { updateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}}, dataprovider.LoginMethodPassword, ipAddr, err) - s.renderClientTwoFactorPage(w, r, "Invalid credentials", ipAddr) + s.renderClientTwoFactorPage(w, r, util.I18nErrorInvalidCredentials, ipAddr) return } if !user.Filters.TOTPConfig.Enabled || !util.Contains(user.Filters.TOTPConfig.Protocols, common.ProtocolHTTP) { updateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, common.ErrInternalFailure) - s.renderClientTwoFactorPage(w, r, "Two factory authentication is not enabled", ipAddr) + s.renderClientTwoFactorPage(w, r, util.I18n2FADisabled, ipAddr) return } err = user.Filters.TOTPConfig.Secret.Decrypt() @@ -428,7 +428,7 @@ func (s *httpdServer) handleWebClientTwoFactorPost(w http.ResponseWriter, r *htt user.Filters.TOTPConfig.Secret.GetPayload()) if !match || err != nil { updateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, dataprovider.ErrInvalidCredentials) - s.renderClientTwoFactorPage(w, r, "Invalid authentication code", ipAddr) + s.renderClientTwoFactorPage(w, r, util.I18nErrorInvalidCredentials, ipAddr) return } connectionID := fmt.Sprintf("%s_%s", getProtocolFromRequest(r), xid.New().String()) @@ -575,8 +575,8 @@ func (s *httpdServer) handleWebAdminLoginPost(w http.ResponseWriter, r *http.Req func (s *httpdServer) renderAdminLoginPage(w http.ResponseWriter, r *http.Request, error, ip string) { data := loginPage{ commonBasePage: getCommonBasePage(r), + Title: util.I18nLoginTitle, CurrentURL: webAdminLoginPath, - Version: version.Get().Version, Error: error, CSRFToken: createCSRFToken(ip), Branding: s.binding.Branding.WebAdmin, @@ -734,7 +734,7 @@ func (s *httpdServer) loginUser( if err != nil { logger.Warn(logSender, connectionID, "unable to set user login cookie %v", err) updateLoginMetrics(user, dataprovider.LoginMethodPassword, ipAddr, common.ErrInternalFailure) - errorFunc(w, r, err.Error(), ipAddr) + errorFunc(w, r, util.I18nError500Message, ipAddr) return } if isSecondFactorAuth { @@ -1095,11 +1095,14 @@ func (s *httpdServer) checkConnection(next http.Handler) http.Handler { if err := common.Connections.IsNewConnectionAllowed(ipAddr, common.ProtocolHTTP); err != nil { logger.Log(logger.LevelDebug, common.ProtocolHTTP, "", "connection not allowed from ip %q: %v", ipAddr, err) - s.sendForbiddenResponse(w, r, err.Error()) + s.sendForbiddenResponse(w, r, util.NewI18nError(err, util.I18nErrorConnectionForbidden)) return } if common.IsBanned(ipAddr, common.ProtocolHTTP) { - s.sendForbiddenResponse(w, r, "your IP address is blocked") + s.sendForbiddenResponse(w, r, util.NewI18nError( + util.NewGenericError("your IP address is blocked"), + util.I18nErrorIPForbidden), + ) return } if delay, err := common.LimitRate(common.ProtocolHTTP, ipAddr); err != nil { @@ -1118,8 +1121,8 @@ func (s *httpdServer) sendTooManyRequestResponse(w http.ResponseWriter, r *http. if (s.enableWebAdmin || s.enableWebClient) && isWebRequest(r) { r = s.updateContextFromCookie(r) if s.enableWebClient && (isWebClientRequest(r) || !s.enableWebAdmin) { - s.renderClientMessagePage(w, r, http.StatusText(http.StatusTooManyRequests), "Rate limit exceeded", - http.StatusTooManyRequests, err, "") + s.renderClientMessagePage(w, r, util.I18nError429Title, http.StatusTooManyRequests, + util.NewI18nError(errors.New(http.StatusText(http.StatusTooManyRequests)), util.I18nError429Message), "") return } s.renderMessagePage(w, r, http.StatusText(http.StatusTooManyRequests), "Rate limit exceeded", http.StatusTooManyRequests, @@ -1129,17 +1132,17 @@ func (s *httpdServer) sendTooManyRequestResponse(w http.ResponseWriter, r *http. sendAPIResponse(w, r, err, http.StatusText(http.StatusTooManyRequests), http.StatusTooManyRequests) } -func (s *httpdServer) sendForbiddenResponse(w http.ResponseWriter, r *http.Request, message string) { +func (s *httpdServer) sendForbiddenResponse(w http.ResponseWriter, r *http.Request, err error) { if (s.enableWebAdmin || s.enableWebClient) && isWebRequest(r) { r = s.updateContextFromCookie(r) if s.enableWebClient && (isWebClientRequest(r) || !s.enableWebAdmin) { - s.renderClientForbiddenPage(w, r, message) + s.renderClientForbiddenPage(w, r, err) return } - s.renderForbiddenPage(w, r, message) + s.renderForbiddenPage(w, r, err.Error()) return } - sendAPIResponse(w, r, errors.New(message), message, http.StatusForbidden) + sendAPIResponse(w, r, err, "", http.StatusForbidden) } func (s *httpdServer) badHostHandler(w http.ResponseWriter, r *http.Request) { @@ -1150,7 +1153,10 @@ func (s *httpdServer) badHostHandler(w http.ResponseWriter, r *http.Request) { break } } - s.sendForbiddenResponse(w, r, fmt.Sprintf("The host %q is not allowed", host)) + s.sendForbiddenResponse(w, r, util.NewI18nError( + util.NewGenericError(fmt.Sprintf("The host %q is not allowed", host)), + util.I18nErrorConnectionForbidden, + )) } func (s *httpdServer) notFoundHandler(w http.ResponseWriter, r *http.Request) { diff --git a/internal/httpd/web.go b/internal/httpd/web.go index 03abe3e7..050f1d30 100644 --- a/internal/httpd/web.go +++ b/internal/httpd/web.go @@ -15,10 +15,15 @@ package httpd import ( + "errors" + "fmt" "net/http" "strings" "github.com/unrolled/secure" + + "github.com/drakkan/sftpgo/v2/internal/util" + "github.com/drakkan/sftpgo/v2/internal/version" ) const ( @@ -44,21 +49,26 @@ const ( templateCommonBase = "base.html" ) +var ( + errInvalidTokenClaims = errors.New("invalid token claims") +) + type commonBasePage struct { CSPNonce string StaticURL string + Version string } type loginPage struct { commonBasePage CurrentURL string - Version string Error string CSRFToken string AltLoginURL string AltLoginName string ForgotPwdURL string OpenIDLoginURL string + Title string Branding UIBranding FormDisabled bool } @@ -66,7 +76,6 @@ type loginPage struct { type twoFactorPage struct { commonBasePage CurrentURL string - Version string Error string CSRFToken string RecoveryURL string @@ -110,8 +119,32 @@ func hasPrefixAndSuffix(key, prefix, suffix string) bool { } func getCommonBasePage(r *http.Request) commonBasePage { + v := version.Get() return commonBasePage{ CSPNonce: secure.CSPNonce(r.Context()), StaticURL: webStaticFilesPath, + Version: fmt.Sprintf("v%v-%v", v.Version, v.CommitHash), } } + +func i18nListDirMsg(status int) string { + if status == http.StatusForbidden { + return util.I18nErrorDirList403 + } + return util.I18nErrorDirListGeneric +} + +func i18nFsMsg(status int) string { + if status == http.StatusForbidden { + return util.I18nError403Message + } + return util.I18nErrorFsGeneric +} + +func getI18NErrorString(err error, fallback string) string { + var errI18n *util.I18nError + if errors.As(err, &errI18n) { + return errI18n.I18nMessage + } + return fallback +} diff --git a/internal/httpd/webadmin.go b/internal/httpd/webadmin.go index dbbabe83..7e984724 100644 --- a/internal/httpd/webadmin.go +++ b/internal/httpd/webadmin.go @@ -826,7 +826,6 @@ func (s *httpdServer) renderTwoFactorPage(w http.ResponseWriter, r *http.Request commonBasePage: getCommonBasePage(r), Title: pageTwoFactorTitle, CurrentURL: webAdminTwoFactorPath, - Version: version.Get().Version, Error: error, CSRFToken: createCSRFToken(ip), RecoveryURL: webAdminTwoFactorRecoveryPath, @@ -840,7 +839,6 @@ func (s *httpdServer) renderTwoFactorRecoveryPage(w http.ResponseWriter, r *http commonBasePage: getCommonBasePage(r), Title: pageTwoFactorRecoveryTitle, CurrentURL: webAdminTwoFactorRecoveryPath, - Version: version.Get().Version, Error: error, CSRFToken: createCSRFToken(ip), Branding: s.binding.Branding.WebAdmin, diff --git a/internal/httpd/webclient.go b/internal/httpd/webclient.go index 4489ed8a..601a83bb 100644 --- a/internal/httpd/webclient.go +++ b/internal/httpd/webclient.go @@ -41,7 +41,6 @@ import ( "github.com/drakkan/sftpgo/v2/internal/mfa" "github.com/drakkan/sftpgo/v2/internal/smtp" "github.com/drakkan/sftpgo/v2/internal/util" - "github.com/drakkan/sftpgo/v2/internal/version" "github.com/drakkan/sftpgo/v2/internal/vfs" ) @@ -64,17 +63,6 @@ const ( templateShareLogin = "sharelogin.html" templateShareDownload = "sharedownload.html" templateUploadToShare = "shareupload.html" - pageClientFilesTitle = "Files" - pageClientSharesTitle = "Shares" - pageClientProfileTitle = "My Profile" - pageClientChangePwdTitle = "Change password" - pageClient2FATitle = "Two-factor auth" - pageClientEditFileTitle = "Edit file" - pageClientForgotPwdTitle = "Forgot password" - pageClientResetPwdTitle = "Reset password" - pageExtShareTitle = "Shared files" - pageUploadToShareTitle = "Upload to share" - pageDownloadFromShareTitle = "Download shared file" ) // condResult is the result of an HTTP request precondition check. @@ -111,11 +99,6 @@ type baseClientPage struct { LoginURL string EditURL string MFAURL string - MFATitle string - FilesTitle string - SharesTitle string - ProfileTitle string - Version string CSRFToken string LoggedUser *dataprovider.User Branding UIBranding @@ -166,9 +149,9 @@ type filesPage struct { type shareLoginPage struct { commonBasePage CurrentURL string - Version string Error string CSRFToken string + Title string Branding UIBranding } @@ -411,11 +394,11 @@ func getFileObjectURL(baseDir, name, baseWebPath string) string { return fmt.Sprintf("%v?path=%v&_=%v", baseWebPath, url.QueryEscape(path.Join(baseDir, name)), time.Now().UTC().Unix()) } -func getFileObjectModTime(t time.Time) string { +func getFileObjectModTime(t time.Time) int64 { if isZeroTime(t) { - return "" + return 0 } - return t.Format("2006-01-02 15:04") + return t.UnixMilli() } func loadClientTemplates(templatesPath string) { @@ -546,7 +529,6 @@ func (s *httpdServer) getBaseClientPageData(title, currentURL string, r *http.Re if currentURL != "" { csrfToken = createCSRFToken(util.GetIPFromRemoteAddress(r.RemoteAddr)) } - v := version.Get() data := baseClientPage{ commonBasePage: getCommonBasePage(r), @@ -561,11 +543,6 @@ func (s *httpdServer) getBaseClientPageData(title, currentURL string, r *http.Re LogoutURL: webClientLogoutPath, EditURL: webClientEditFilePath, MFAURL: webClientMFAPath, - MFATitle: pageClient2FATitle, - FilesTitle: pageClientFilesTitle, - SharesTitle: pageClientSharesTitle, - ProfileTitle: pageClientProfileTitle, - Version: fmt.Sprintf("%v-%v", v.Version, v.CommitHash), CSRFToken: csrfToken, LoggedUser: getUserFromToken(r), Branding: s.binding.Branding.WebClient, @@ -583,7 +560,7 @@ func (s *httpdServer) renderClientForgotPwdPage(w http.ResponseWriter, r *http.R Error: error, CSRFToken: createCSRFToken(ip), LoginURL: webClientLoginPath, - Title: pageClientForgotPwdTitle, + Title: util.I18nForgotPwdTitle, Branding: s.binding.Branding.WebClient, } renderClientTemplate(w, templateForgotPassword, data) @@ -596,7 +573,7 @@ func (s *httpdServer) renderClientResetPwdPage(w http.ResponseWriter, r *http.Re Error: error, CSRFToken: createCSRFToken(ip), LoginURL: webClientLoginPath, - Title: pageClientResetPwdTitle, + Title: util.I18nResetPwdTitle, Branding: s.binding.Branding.WebClient, } renderClientTemplate(w, templateResetPassword, data) @@ -605,8 +582,8 @@ func (s *httpdServer) renderClientResetPwdPage(w http.ResponseWriter, r *http.Re func (s *httpdServer) renderShareLoginPage(w http.ResponseWriter, r *http.Request, error, ip string) { data := shareLoginPage{ commonBasePage: getCommonBasePage(r), + Title: util.I18nShareLoginTitle, CurrentURL: r.RequestURI, - Version: version.Get().Version, Error: error, CSRFToken: createCSRFToken(ip), Branding: s.binding.Branding.WebClient, @@ -621,18 +598,14 @@ func renderClientTemplate(w http.ResponseWriter, tmplName string, data any) { } } -func (s *httpdServer) renderClientMessagePage(w http.ResponseWriter, r *http.Request, title, body string, statusCode int, err error, message string) { - var errorString strings.Builder - if body != "" { - errorString.WriteString(body) - errorString.WriteString(" ") - } +func (s *httpdServer) renderClientMessagePage(w http.ResponseWriter, r *http.Request, title string, statusCode int, err error, message string) { + var errString string if err != nil { - errorString.WriteString(err.Error()) + errString = getI18NErrorString(err, util.I18nError500Message) } data := clientMessagePage{ baseClientPage: s.getBaseClientPageData(title, "", r), - Error: errorString.String(), + Error: errString, Success: message, } w.WriteHeader(statusCode) @@ -640,28 +613,31 @@ func (s *httpdServer) renderClientMessagePage(w http.ResponseWriter, r *http.Req } func (s *httpdServer) renderClientInternalServerErrorPage(w http.ResponseWriter, r *http.Request, err error) { - s.renderClientMessagePage(w, r, page500Title, page500Body, http.StatusInternalServerError, err, "") + s.renderClientMessagePage(w, r, util.I18nError500Title, http.StatusInternalServerError, + util.NewI18nError(err, util.I18nError500Message), "") } func (s *httpdServer) renderClientBadRequestPage(w http.ResponseWriter, r *http.Request, err error) { - s.renderClientMessagePage(w, r, page400Title, "", http.StatusBadRequest, err, "") + s.renderClientMessagePage(w, r, util.I18nError400Title, http.StatusBadRequest, + util.NewI18nError(err, util.I18nError400Message), "") } -func (s *httpdServer) renderClientForbiddenPage(w http.ResponseWriter, r *http.Request, body string) { - s.renderClientMessagePage(w, r, page403Title, "", http.StatusForbidden, nil, body) +func (s *httpdServer) renderClientForbiddenPage(w http.ResponseWriter, r *http.Request, err error) { + s.renderClientMessagePage(w, r, util.I18nError403Title, http.StatusForbidden, + util.NewI18nError(err, util.I18nError403Message), "") } func (s *httpdServer) renderClientNotFoundPage(w http.ResponseWriter, r *http.Request, err error) { - s.renderClientMessagePage(w, r, page404Title, page404Body, http.StatusNotFound, err, "") + s.renderClientMessagePage(w, r, util.I18nError400Title, http.StatusNotFound, + util.NewI18nError(err, util.I18nError400Message), "") } -func (s *httpdServer) renderClientTwoFactorPage(w http.ResponseWriter, r *http.Request, error, ip string) { +func (s *httpdServer) renderClientTwoFactorPage(w http.ResponseWriter, r *http.Request, errorString, ip string) { data := twoFactorPage{ commonBasePage: getCommonBasePage(r), Title: pageTwoFactorTitle, CurrentURL: webClientTwoFactorPath, - Version: version.Get().Version, - Error: error, + Error: errorString, CSRFToken: createCSRFToken(ip), RecoveryURL: webClientTwoFactorRecoveryPath, Branding: s.binding.Branding.WebClient, @@ -669,25 +645,24 @@ func (s *httpdServer) renderClientTwoFactorPage(w http.ResponseWriter, r *http.R if next := r.URL.Query().Get("next"); strings.HasPrefix(next, webClientFilesPath) { data.CurrentURL += "?next=" + url.QueryEscape(next) } - renderClientTemplate(w, templateTwoFactor, data) + renderClientTemplate(w, templateClientTwoFactor, data) } -func (s *httpdServer) renderClientTwoFactorRecoveryPage(w http.ResponseWriter, r *http.Request, error, ip string) { +func (s *httpdServer) renderClientTwoFactorRecoveryPage(w http.ResponseWriter, r *http.Request, errorString, ip string) { data := twoFactorPage{ commonBasePage: getCommonBasePage(r), Title: pageTwoFactorRecoveryTitle, CurrentURL: webClientTwoFactorRecoveryPath, - Version: version.Get().Version, - Error: error, + Error: errorString, CSRFToken: createCSRFToken(ip), Branding: s.binding.Branding.WebClient, } - renderClientTemplate(w, templateTwoFactorRecovery, data) + renderClientTemplate(w, templateClientTwoFactorRecovery, data) } func (s *httpdServer) renderClientMFAPage(w http.ResponseWriter, r *http.Request) { data := clientMFAPage{ - baseClientPage: s.getBaseClientPageData(pageMFATitle, webClientMFAPath, r), + baseClientPage: s.getBaseClientPageData(util.I18n2FATitle, webClientMFAPath, r), TOTPConfigs: mfa.GetAvailableTOTPConfigNames(), GenerateTOTPURL: webClientTOTPGeneratePath, ValidateTOTPURL: webClientTOTPValidatePath, @@ -697,7 +672,7 @@ func (s *httpdServer) renderClientMFAPage(w http.ResponseWriter, r *http.Request } user, err := dataprovider.UserExists(data.LoggedUser.Username, "") if err != nil { - s.renderInternalServerErrorPage(w, r, err) + s.renderClientInternalServerErrorPage(w, r, err) return } data.TOTPConfig = user.Filters.TOTPConfig @@ -705,8 +680,12 @@ func (s *httpdServer) renderClientMFAPage(w http.ResponseWriter, r *http.Request } func (s *httpdServer) renderEditFilePage(w http.ResponseWriter, r *http.Request, fileName, fileData string, readOnly bool) { + title := util.I18nViewFileTitle + if !readOnly { + title = util.I18nEditFileTitle + } data := editFilePage{ - baseClientPage: s.getBaseClientPageData(pageClientEditFileTitle, webClientEditFilePath, r), + baseClientPage: s.getBaseClientPageData(title, webClientEditFilePath, r), Path: fileName, Name: path.Base(fileName), CurrentDir: path.Dir(fileName), @@ -719,17 +698,17 @@ func (s *httpdServer) renderEditFilePage(w http.ResponseWriter, r *http.Request, } func (s *httpdServer) renderAddUpdateSharePage(w http.ResponseWriter, r *http.Request, share *dataprovider.Share, - error string, isAdd bool) { + errorString string, isAdd bool) { currentURL := webClientSharePath - title := "Add a new share" + title := util.I18nShareAddTitle if !isAdd { currentURL = fmt.Sprintf("%v/%v", webClientSharePath, url.PathEscape(share.ShareID)) - title = "Update share" + title = util.I18nShareUpdateTitle } data := clientSharePage{ baseClientPage: s.getBaseClientPageData(title, currentURL, r), Share: share, - Error: error, + Error: errorString, IsAdd: isAdd, } @@ -761,7 +740,7 @@ func (s *httpdServer) renderSharedFilesPage(w http.ResponseWriter, r *http.Reque share dataprovider.Share, ) { currentURL := path.Join(webClientPubSharesPath, share.ShareID, "browse") - baseData := s.getBaseClientPageData(pageExtShareTitle, currentURL, r) + baseData := s.getBaseClientPageData(util.I18nSharedFilesTitle, currentURL, r) baseData.FilesURL = currentURL baseSharePath := path.Join(webClientPubSharesPath, share.ShareID) @@ -790,7 +769,7 @@ func (s *httpdServer) renderSharedFilesPage(w http.ResponseWriter, r *http.Reque func (s *httpdServer) renderShareDownloadPage(w http.ResponseWriter, r *http.Request, downloadLink string) { data := shareDownloadPage{ - baseClientPage: s.getBaseClientPageData(pageDownloadFromShareTitle, "", r), + baseClientPage: s.getBaseClientPageData(util.I18nShareDownloadTitle, "", r), DownloadLink: downloadLink, } renderClientTemplate(w, templateShareDownload, data) @@ -799,7 +778,7 @@ func (s *httpdServer) renderShareDownloadPage(w http.ResponseWriter, r *http.Req func (s *httpdServer) renderUploadToSharePage(w http.ResponseWriter, r *http.Request, share dataprovider.Share) { currentURL := path.Join(webClientPubSharesPath, share.ShareID, "upload") data := shareUploadPage{ - baseClientPage: s.getBaseClientPageData(pageUploadToShareTitle, currentURL, r), + baseClientPage: s.getBaseClientPageData(util.I18nShareUploadTitle, currentURL, r), Share: &share, UploadBasePath: path.Join(webClientPubSharesPath, share.ShareID), } @@ -808,7 +787,7 @@ func (s *httpdServer) renderUploadToSharePage(w http.ResponseWriter, r *http.Req func (s *httpdServer) renderFilesPage(w http.ResponseWriter, r *http.Request, dirName, error string, user *dataprovider.User) { data := filesPage{ - baseClientPage: s.getBaseClientPageData(pageClientFilesTitle, webClientFilesPath, r), + baseClientPage: s.getBaseClientPageData(util.I18nFilesTitle, webClientFilesPath, r), Error: error, CurrentDir: url.QueryEscape(dirName), DownloadURL: webClientDownloadZipPath, @@ -831,7 +810,7 @@ func (s *httpdServer) renderFilesPage(w http.ResponseWriter, r *http.Request, di func (s *httpdServer) renderClientProfilePage(w http.ResponseWriter, r *http.Request, error string) { data := clientProfilePage{ - baseClientPage: s.getBaseClientPageData(pageClientProfileTitle, webClientProfilePath, r), + baseClientPage: s.getBaseClientPageData(util.I18nProfileTitle, webClientProfilePath, r), Error: error, } user, userMerged, err := dataprovider.GetUserVariants(data.LoggedUser.Username, "") @@ -849,7 +828,7 @@ func (s *httpdServer) renderClientProfilePage(w http.ResponseWriter, r *http.Req func (s *httpdServer) renderClientChangePasswordPage(w http.ResponseWriter, r *http.Request, error string) { data := changeClientPasswordPage{ - baseClientPage: s.getBaseClientPageData(pageClientChangePwdTitle, webChangeClientPwdPath, r), + baseClientPage: s.getBaseClientPageData(util.I18nChangePwdTitle, webChangeClientPwdPath, r), Error: error, } @@ -860,22 +839,23 @@ func (s *httpdServer) handleWebClientDownloadZip(w http.ResponseWriter, r *http. r.Body = http.MaxBytesReader(w, r.Body, maxMultipartMem) claims, err := getTokenClaims(r) if err != nil || claims.Username == "" { - s.renderClientMessagePage(w, r, "Invalid token claims", "", http.StatusForbidden, nil, "") + s.renderClientForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken)) return } if err := r.ParseForm(); err != nil { - s.renderClientMessagePage(w, r, "Invalid request", err.Error(), getRespStatus(err), nil, "") + s.renderClientBadRequestPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm)) return } ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr) if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil { - s.renderClientForbiddenPage(w, r, err.Error()) + s.renderClientForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF)) return } user, err := dataprovider.GetUserWithGroupSettings(claims.Username, "") if err != nil { - s.renderClientMessagePage(w, r, "Unable to retrieve your user", "", getRespStatus(err), nil, "") + s.renderClientMessagePage(w, r, util.I18nError500Title, getRespStatus(err), + util.NewI18nError(err, util.I18nErrorGetUser), "") return } @@ -883,7 +863,7 @@ func (s *httpdServer) handleWebClientDownloadZip(w http.ResponseWriter, r *http. protocol := getProtocolFromRequest(r) connectionID := fmt.Sprintf("%v_%v", protocol, connID) if err := checkHTTPClientUser(&user, r, connectionID, false); err != nil { - s.renderClientForbiddenPage(w, r, err.Error()) + s.renderClientForbiddenPage(w, r, err) return } connection := &Connection{ @@ -892,7 +872,8 @@ func (s *httpdServer) handleWebClientDownloadZip(w http.ResponseWriter, r *http. request: r, } if err = common.Connections.Add(connection); err != nil { - s.renderClientMessagePage(w, r, "Unable to add connection", "", http.StatusTooManyRequests, err, "") + s.renderClientMessagePage(w, r, util.I18nError429Title, http.StatusTooManyRequests, + util.NewI18nError(err, util.I18nError429Message), "") return } defer common.Connections.Remove(connection.GetID()) @@ -902,7 +883,7 @@ func (s *httpdServer) handleWebClientDownloadZip(w http.ResponseWriter, r *http. var filesList []string err = json.Unmarshal([]byte(files), &filesList) if err != nil { - s.renderClientMessagePage(w, r, "Unable to get files list", "", http.StatusInternalServerError, err, "") + s.renderClientBadRequestPage(w, r, err) return } @@ -914,7 +895,7 @@ func (s *httpdServer) handleWebClientDownloadZip(w http.ResponseWriter, r *http. func (s *httpdServer) handleClientSharePartialDownload(w http.ResponseWriter, r *http.Request) { r.Body = http.MaxBytesReader(w, r.Body, maxMultipartMem) if err := r.ParseForm(); err != nil { - s.renderClientMessagePage(w, r, "Invalid request", err.Error(), getRespStatus(err), nil, "") + s.renderClientBadRequestPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidForm)) return } validScopes := []dataprovider.ShareScope{dataprovider.ShareScopeRead, dataprovider.ShareScopeReadWrite} @@ -923,32 +904,33 @@ func (s *httpdServer) handleClientSharePartialDownload(w http.ResponseWriter, r return } if err := validateBrowsableShare(share, connection); err != nil { - s.renderClientMessagePage(w, r, "Unable to validate share", "", getRespStatus(err), err, "") + s.renderClientMessagePage(w, r, util.I18nShareAccessErrorTitle, getRespStatus(err), err, "") return } name, err := getBrowsableSharedPath(share, r) if err != nil { - s.renderClientMessagePage(w, r, "Invalid share path", "", getRespStatus(err), err, "") + s.renderClientMessagePage(w, r, util.I18nShareAccessErrorTitle, getRespStatus(err), err, "") return } if err = common.Connections.Add(connection); err != nil { - s.renderClientMessagePage(w, r, "Unable to add connection", "", http.StatusTooManyRequests, err, "") + s.renderClientMessagePage(w, r, util.I18nError429Title, http.StatusTooManyRequests, + util.NewI18nError(err, util.I18nError429Message), "") return } defer common.Connections.Remove(connection.GetID()) transferQuota := connection.GetTransferQuota() if !transferQuota.HasDownloadSpace() { - err = connection.GetReadQuotaExceededError() + err = util.NewI18nError(connection.GetReadQuotaExceededError(), util.I18nErrorQuotaRead) connection.Log(logger.LevelInfo, "denying share read due to quota limits") - s.renderClientMessagePage(w, r, "Denying share read due to quota limits", "", getMappedStatusCode(err), err, "") + s.renderClientMessagePage(w, r, util.I18nShareAccessErrorTitle, getMappedStatusCode(err), err, "") return } files := r.Form.Get("files") var filesList []string err = json.Unmarshal([]byte(files), &filesList) if err != nil { - s.renderClientMessagePage(w, r, "Unable to get files list", "", http.StatusInternalServerError, err, "") + s.renderClientBadRequestPage(w, r, err) return } @@ -966,23 +948,23 @@ func (s *httpdServer) handleShareGetDirContents(w http.ResponseWriter, r *http.R return } if err := validateBrowsableShare(share, connection); err != nil { - s.renderClientMessagePage(w, r, "Unable to validate share", "", getRespStatus(err), err, "") + sendAPIResponse(w, r, err, getI18NErrorString(err, util.I18nError500Message), getRespStatus(err)) return } name, err := getBrowsableSharedPath(share, r) if err != nil { - s.renderClientMessagePage(w, r, "Invalid share path", "", getRespStatus(err), err, "") + sendAPIResponse(w, r, err, getI18NErrorString(err, util.I18nError500Message), getRespStatus(err)) return } if err = common.Connections.Add(connection); err != nil { - s.renderClientMessagePage(w, r, "Unable to add connection", "", http.StatusTooManyRequests, err, "") + sendAPIResponse(w, r, err, getI18NErrorString(err, util.I18nError429Message), http.StatusTooManyRequests) return } defer common.Connections.Remove(connection.GetID()) contents, err := connection.ReadDir(name) if err != nil { - sendAPIResponse(w, r, err, "Unable to get directory contents", getMappedStatusCode(err)) + sendAPIResponse(w, r, err, getI18NErrorString(err, util.I18nErrorDirListGeneric), getMappedStatusCode(err)) return } results := make([]map[string]any, 0, len(contents)) @@ -1031,17 +1013,17 @@ func (s *httpdServer) handleShareGetFiles(w http.ResponseWriter, r *http.Request return } if err := validateBrowsableShare(share, connection); err != nil { - s.renderClientMessagePage(w, r, "Unable to validate share", "", getRespStatus(err), err, "") + s.renderClientMessagePage(w, r, util.I18nShareAccessErrorTitle, getRespStatus(err), err, "") return } name, err := getBrowsableSharedPath(share, r) if err != nil { - s.renderClientMessagePage(w, r, "Invalid share path", "", getRespStatus(err), err, "") + s.renderClientMessagePage(w, r, util.I18nShareAccessErrorTitle, getRespStatus(err), err, "") return } if err = common.Connections.Add(connection); err != nil { - s.renderClientMessagePage(w, r, "Unable to add connection", "", http.StatusTooManyRequests, err, "") + s.renderSharedFilesPage(w, r, path.Dir(share.GetRelativePath(name)), util.I18nError429Message, share) return } defer common.Connections.Remove(connection.GetID()) @@ -1053,7 +1035,7 @@ func (s *httpdServer) handleShareGetFiles(w http.ResponseWriter, r *http.Request info, err = connection.Stat(name, 1) } if err != nil { - s.renderSharedFilesPage(w, r, path.Dir(share.GetRelativePath(name)), err.Error(), share) + s.renderSharedFilesPage(w, r, path.Dir(share.GetRelativePath(name)), i18nFsMsg(getRespStatus(err)), share) return } if info.IsDir() { @@ -1064,7 +1046,7 @@ func (s *httpdServer) handleShareGetFiles(w http.ResponseWriter, r *http.Request if status, err := downloadFile(w, r, connection, name, info, false, &share); err != nil { dataprovider.UpdateShareLastUse(&share, -1) //nolint:errcheck if status > 0 { - s.renderSharedFilesPage(w, r, path.Dir(share.GetRelativePath(name)), err.Error(), share) + s.renderSharedFilesPage(w, r, path.Dir(share.GetRelativePath(name)), i18nFsMsg(getRespStatus(err)), share) } } } @@ -1095,29 +1077,31 @@ func (s *httpdServer) handleShareGetPDF(w http.ResponseWriter, r *http.Request) return } if err := validateBrowsableShare(share, connection); err != nil { - s.renderClientMessagePage(w, r, "Unable to validate share", "", getRespStatus(err), err, "") + s.renderClientMessagePage(w, r, util.I18nShareAccessErrorTitle, getRespStatus(err), err, "") return } name, err := getBrowsableSharedPath(share, r) if err != nil { - s.renderClientMessagePage(w, r, "Invalid share path", "", getRespStatus(err), err, "") + s.renderClientMessagePage(w, r, util.I18nShareAccessErrorTitle, getRespStatus(err), err, "") return } if err = common.Connections.Add(connection); err != nil { - s.renderClientMessagePage(w, r, "Unable to add connection", "", http.StatusTooManyRequests, err, "") + s.renderClientMessagePage(w, r, util.I18nError429Title, http.StatusTooManyRequests, + util.NewI18nError(err, util.I18nError429Message), "") return } defer common.Connections.Remove(connection.GetID()) info, err := connection.Stat(name, 1) if err != nil { - s.renderClientMessagePage(w, r, "Unable to get file", "", getRespStatus(err), err, "") + status := getRespStatus(err) + s.renderClientMessagePage(w, r, util.I18nShareAccessErrorTitle, status, + util.NewI18nError(err, i18nFsMsg(status)), "") return } if info.IsDir() { - s.renderClientMessagePage(w, r, "Invalid file", fmt.Sprintf("%q is not a file", name), - http.StatusBadRequest, nil, "") + s.renderClientBadRequestPage(w, r, util.NewI18nError(fmt.Errorf("%q is not a file", name), util.I18nErrorPDFMessage)) return } connection.User.CheckFsRoot(connection.ID) //nolint:errcheck @@ -1134,21 +1118,21 @@ func (s *httpdServer) handleClientGetDirContents(w http.ResponseWriter, r *http. r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) claims, err := getTokenClaims(r) if err != nil || claims.Username == "" { - sendAPIResponse(w, r, nil, "invalid token claims", http.StatusForbidden) + sendAPIResponse(w, r, nil, util.I18nErrorDirList403, http.StatusForbidden) return } user, err := dataprovider.GetUserWithGroupSettings(claims.Username, "") if err != nil { - sendAPIResponse(w, r, nil, "Unable to retrieve your user", getRespStatus(err)) + sendAPIResponse(w, r, nil, util.I18nErrorDirListUser, getRespStatus(err)) return } connID := xid.New().String() protocol := getProtocolFromRequest(r) - connectionID := fmt.Sprintf("%v_%v", protocol, connID) + connectionID := fmt.Sprintf("%s_%s", protocol, connID) if err := checkHTTPClientUser(&user, r, connectionID, false); err != nil { - sendAPIResponse(w, r, err, http.StatusText(http.StatusForbidden), http.StatusForbidden) + sendAPIResponse(w, r, err, getI18NErrorString(err, util.I18nErrorDirList403), http.StatusForbidden) return } connection := &Connection{ @@ -1157,7 +1141,7 @@ func (s *httpdServer) handleClientGetDirContents(w http.ResponseWriter, r *http. request: r, } if err = common.Connections.Add(connection); err != nil { - s.renderClientMessagePage(w, r, "Unable to add connection", "", http.StatusTooManyRequests, err, "") + sendAPIResponse(w, r, err, util.I18nErrorDirList429, http.StatusTooManyRequests) return } defer common.Connections.Remove(connection.GetID()) @@ -1165,7 +1149,8 @@ func (s *httpdServer) handleClientGetDirContents(w http.ResponseWriter, r *http. name := connection.User.GetCleanedPath(r.URL.Query().Get("path")) contents, err := connection.ReadDir(name) if err != nil { - sendAPIResponse(w, r, err, "Unable to get directory contents", getMappedStatusCode(err)) + statusCode := getMappedStatusCode(err) + sendAPIResponse(w, r, err, i18nListDirMsg(statusCode), statusCode) return } @@ -1205,13 +1190,14 @@ func (s *httpdServer) handleClientGetFiles(w http.ResponseWriter, r *http.Reques r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) claims, err := getTokenClaims(r) if err != nil || claims.Username == "" { - s.renderClientForbiddenPage(w, r, "Invalid token claims") + s.renderClientForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken)) return } user, err := dataprovider.GetUserWithGroupSettings(claims.Username, "") if err != nil { - s.renderClientMessagePage(w, r, "Unable to retrieve your user", "", getRespStatus(err), nil, "") + s.renderClientMessagePage(w, r, util.I18nError500Title, getRespStatus(err), + util.NewI18nError(err, util.I18nErrorGetUser), "") return } @@ -1219,7 +1205,7 @@ func (s *httpdServer) handleClientGetFiles(w http.ResponseWriter, r *http.Reques protocol := getProtocolFromRequest(r) connectionID := fmt.Sprintf("%v_%v", protocol, connID) if err := checkHTTPClientUser(&user, r, connectionID, false); err != nil { - s.renderClientForbiddenPage(w, r, err.Error()) + s.renderClientForbiddenPage(w, r, err) return } connection := &Connection{ @@ -1228,7 +1214,8 @@ func (s *httpdServer) handleClientGetFiles(w http.ResponseWriter, r *http.Reques request: r, } if err = common.Connections.Add(connection); err != nil { - s.renderClientMessagePage(w, r, "Unable to add connection", "", http.StatusTooManyRequests, err, "") + s.renderClientMessagePage(w, r, util.I18nError429Title, http.StatusTooManyRequests, + util.NewI18nError(err, util.I18nError429Message), "") return } defer common.Connections.Remove(connection.GetID()) @@ -1241,7 +1228,7 @@ func (s *httpdServer) handleClientGetFiles(w http.ResponseWriter, r *http.Reques info, err = connection.Stat(name, 0) } if err != nil { - s.renderFilesPage(w, r, path.Dir(name), fmt.Sprintf("unable to stat file %q: %v", name, err), &user) + s.renderFilesPage(w, r, path.Dir(name), i18nFsMsg(getRespStatus(err)), &user) return } if info.IsDir() { @@ -1251,10 +1238,11 @@ func (s *httpdServer) handleClientGetFiles(w http.ResponseWriter, r *http.Reques if status, err := downloadFile(w, r, connection, name, info, false, nil); err != nil && status != 0 { if status > 0 { if status == http.StatusRequestedRangeNotSatisfiable { - s.renderClientMessagePage(w, r, http.StatusText(status), "", status, err, "") + s.renderClientMessagePage(w, r, util.I18nError416Title, status, + util.NewI18nError(err, util.I18nError416Message), "") return } - s.renderFilesPage(w, r, path.Dir(name), err.Error(), &user) + s.renderFilesPage(w, r, path.Dir(name), i18nFsMsg(status), &user) } } } @@ -1263,13 +1251,14 @@ func (s *httpdServer) handleClientEditFile(w http.ResponseWriter, r *http.Reques r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) claims, err := getTokenClaims(r) if err != nil || claims.Username == "" { - s.renderClientForbiddenPage(w, r, "Invalid token claims") + s.renderClientForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken)) return } user, err := dataprovider.GetUserWithGroupSettings(claims.Username, "") if err != nil { - s.renderClientMessagePage(w, r, "Unable to retrieve your user", "", getRespStatus(err), nil, "") + s.renderClientMessagePage(w, r, util.I18nError500Title, getRespStatus(err), + util.NewI18nError(err, util.I18nErrorGetUser), "") return } @@ -1277,7 +1266,7 @@ func (s *httpdServer) handleClientEditFile(w http.ResponseWriter, r *http.Reques protocol := getProtocolFromRequest(r) connectionID := fmt.Sprintf("%v_%v", protocol, connID) if err := checkHTTPClientUser(&user, r, connectionID, false); err != nil { - s.renderClientForbiddenPage(w, r, err.Error()) + s.renderClientForbiddenPage(w, r, err) return } connection := &Connection{ @@ -1286,7 +1275,8 @@ func (s *httpdServer) handleClientEditFile(w http.ResponseWriter, r *http.Reques request: r, } if err = common.Connections.Add(connection); err != nil { - s.renderClientMessagePage(w, r, "Unable to add connection", "", http.StatusTooManyRequests, err, "") + s.renderClientMessagePage(w, r, util.I18nError429Title, http.StatusTooManyRequests, + util.NewI18nError(err, util.I18nError429Message), "") return } defer common.Connections.Remove(connection.GetID()) @@ -1294,26 +1284,33 @@ func (s *httpdServer) handleClientEditFile(w http.ResponseWriter, r *http.Reques name := connection.User.GetCleanedPath(r.URL.Query().Get("path")) info, err := connection.Stat(name, 0) if err != nil { - s.renderClientMessagePage(w, r, fmt.Sprintf("Unable to stat file %q", name), "", - getRespStatus(err), nil, "") + status := getRespStatus(err) + s.renderClientMessagePage(w, r, util.I18nErrorEditorTitle, status, util.NewI18nError(err, i18nFsMsg(status)), "") return } if info.IsDir() { - s.renderClientMessagePage(w, r, fmt.Sprintf("The path %q does not point to a file", name), "", - http.StatusBadRequest, nil, "") + s.renderClientMessagePage(w, r, util.I18nErrorEditorTitle, http.StatusBadRequest, + util.NewI18nError( + util.NewValidationError(fmt.Sprintf("The path %q does not point to a file", name)), + util.I18nErrorEditDir, + ), "") return } if info.Size() > httpdMaxEditFileSize { - s.renderClientMessagePage(w, r, fmt.Sprintf("The file size %v for %q exceeds the maximum allowed size", - util.ByteCountIEC(info.Size()), name), "", http.StatusBadRequest, nil, "") + s.renderClientMessagePage(w, r, util.I18nErrorEditorTitle, http.StatusBadRequest, + util.NewI18nError( + util.NewValidationError(fmt.Sprintf("The file size %v for %q exceeds the maximum allowed size", + util.ByteCountIEC(info.Size()), name)), + util.I18nErrorEditSize, + ), "") return } connection.User.CheckFsRoot(connection.ID) //nolint:errcheck reader, err := connection.getFileReader(name, 0, r.Method) if err != nil { - s.renderClientMessagePage(w, r, fmt.Sprintf("Unable to get a reader for the file %q", name), "", - getRespStatus(err), nil, "") + s.renderClientMessagePage(w, r, util.I18nErrorEditorTitle, getRespStatus(err), + util.NewI18nError(err, util.I18nError500Message), "") return } defer reader.Close() @@ -1321,8 +1318,8 @@ func (s *httpdServer) handleClientEditFile(w http.ResponseWriter, r *http.Reques var b bytes.Buffer _, err = io.Copy(&b, reader) if err != nil { - s.renderClientMessagePage(w, r, fmt.Sprintf("Unable to read the file %q", name), "", http.StatusInternalServerError, - nil, "") + s.renderClientMessagePage(w, r, util.I18nErrorEditorTitle, getRespStatus(err), + util.NewI18nError(err, util.I18nError500Message), "") return } @@ -1333,19 +1330,20 @@ func (s *httpdServer) handleClientAddShareGet(w http.ResponseWriter, r *http.Req r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) claims, err := getTokenClaims(r) if err != nil || claims.Username == "" { - s.renderClientForbiddenPage(w, r, "Invalid token claims") + s.renderClientForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken)) return } user, err := dataprovider.GetUserWithGroupSettings(claims.Username, "") if err != nil { - s.renderClientMessagePage(w, r, "Unable to retrieve your user", "", getRespStatus(err), nil, "") + s.renderClientMessagePage(w, r, util.I18nError500Title, getRespStatus(err), + util.NewI18nError(err, util.I18nErrorGetUser), "") return } share := &dataprovider.Share{Scope: dataprovider.ShareScopeRead} if user.Filters.DefaultSharesExpiration > 0 { share.ExpiresAt = util.GetTimeAsMsSinceEpoch(time.Now().Add(24 * time.Hour * time.Duration(user.Filters.DefaultSharesExpiration))) - } else { - share.ExpiresAt = util.GetTimeAsMsSinceEpoch(time.Now().Add(24 * time.Hour * 7)) + } else if user.Filters.MaxSharesExpiration > 0 { + share.ExpiresAt = util.GetTimeAsMsSinceEpoch(time.Now().Add(24 * time.Hour * time.Duration(user.Filters.MaxSharesExpiration))) } dirName := "/" if _, ok := r.URL.Query()["path"]; ok { @@ -1357,7 +1355,7 @@ func (s *httpdServer) handleClientAddShareGet(w http.ResponseWriter, r *http.Req var filesList []string err := json.Unmarshal([]byte(files), &filesList) if err != nil { - s.renderClientMessagePage(w, r, "Invalid share list", "", http.StatusBadRequest, err, "") + s.renderClientBadRequestPage(w, r, err) return } for _, f := range filesList { @@ -1374,7 +1372,7 @@ func (s *httpdServer) handleClientUpdateShareGet(w http.ResponseWriter, r *http. r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) claims, err := getTokenClaims(r) if err != nil || claims.Username == "" { - s.renderClientForbiddenPage(w, r, "Invalid token claims") + s.renderClientForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken)) return } shareID := getURLParam(r, "id") @@ -1393,17 +1391,17 @@ func (s *httpdServer) handleClientAddSharePost(w http.ResponseWriter, r *http.Re r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) claims, err := getTokenClaims(r) if err != nil || claims.Username == "" { - s.renderClientForbiddenPage(w, r, "Invalid token claims") + s.renderClientForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken)) return } share, err := getShareFromPostFields(r) if err != nil { - s.renderAddUpdateSharePage(w, r, share, err.Error(), true) + s.renderAddUpdateSharePage(w, r, share, getI18NErrorString(err, util.I18nError500Message), true) return } ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr) if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil { - s.renderClientForbiddenPage(w, r, err.Error()) + s.renderClientForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF)) return } share.ID = 0 @@ -1412,24 +1410,24 @@ func (s *httpdServer) handleClientAddSharePost(w http.ResponseWriter, r *http.Re share.Username = claims.Username if share.Password == "" { if util.Contains(claims.Permissions, sdk.WebClientShareNoPasswordDisabled) { - s.renderClientForbiddenPage(w, r, "You are not authorized to share files/folders without a password") + s.renderAddUpdateSharePage(w, r, share, util.I18nErrorShareNoPwd, true) return } } user, err := dataprovider.GetUserWithGroupSettings(claims.Username, "") if err != nil { - s.renderAddUpdateSharePage(w, r, share, "Unable to retrieve your user", true) + s.renderAddUpdateSharePage(w, r, share, util.I18nErrorGetUser, true) return } if err := user.CheckMaxShareExpiration(util.GetTimeFromMsecSinceEpoch(share.ExpiresAt)); err != nil { - s.renderAddUpdateSharePage(w, r, share, err.Error(), true) + s.renderAddUpdateSharePage(w, r, share, util.I18nErrorShareExpirationOutOfRange, true) return } err = dataprovider.AddShare(share, claims.Username, ipAddr, claims.Role) if err == nil { http.Redirect(w, r, webClientSharesPath, http.StatusSeeOther) } else { - s.renderAddUpdateSharePage(w, r, share, err.Error(), true) + s.renderAddUpdateSharePage(w, r, share, getI18NErrorString(err, util.I18nErrorShareGeneric), true) } } @@ -1437,7 +1435,7 @@ func (s *httpdServer) handleClientUpdateSharePost(w http.ResponseWriter, r *http r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) claims, err := getTokenClaims(r) if err != nil || claims.Username == "" { - s.renderClientForbiddenPage(w, r, "Invalid token claims") + s.renderClientForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken)) return } shareID := getURLParam(r, "id") @@ -1451,12 +1449,12 @@ func (s *httpdServer) handleClientUpdateSharePost(w http.ResponseWriter, r *http } updatedShare, err := getShareFromPostFields(r) if err != nil { - s.renderAddUpdateSharePage(w, r, updatedShare, err.Error(), false) + s.renderAddUpdateSharePage(w, r, updatedShare, getI18NErrorString(err, util.I18nError500Message), false) return } ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr) if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil { - s.renderClientForbiddenPage(w, r, err.Error()) + s.renderClientForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF)) return } updatedShare.ShareID = shareID @@ -1466,24 +1464,24 @@ func (s *httpdServer) handleClientUpdateSharePost(w http.ResponseWriter, r *http } if updatedShare.Password == "" { if util.Contains(claims.Permissions, sdk.WebClientShareNoPasswordDisabled) { - s.renderClientForbiddenPage(w, r, "You are not authorized to share files/folders without a password") + s.renderAddUpdateSharePage(w, r, updatedShare, util.I18nErrorShareNoPwd, false) return } } user, err := dataprovider.GetUserWithGroupSettings(claims.Username, "") if err != nil { - s.renderAddUpdateSharePage(w, r, updatedShare, "Unable to retrieve your user", false) + s.renderAddUpdateSharePage(w, r, updatedShare, util.I18nErrorGetUser, false) return } if err := user.CheckMaxShareExpiration(util.GetTimeFromMsecSinceEpoch(updatedShare.ExpiresAt)); err != nil { - s.renderAddUpdateSharePage(w, r, updatedShare, err.Error(), false) + s.renderAddUpdateSharePage(w, r, updatedShare, util.I18nErrorShareExpirationOutOfRange, false) return } err = dataprovider.UpdateShare(updatedShare, claims.Username, ipAddr, claims.Role) if err == nil { http.Redirect(w, r, webClientSharesPath, http.StatusSeeOther) } else { - s.renderAddUpdateSharePage(w, r, updatedShare, err.Error(), false) + s.renderAddUpdateSharePage(w, r, updatedShare, getI18NErrorString(err, util.I18nErrorShareGeneric), false) } } @@ -1491,7 +1489,7 @@ func (s *httpdServer) handleClientGetShares(w http.ResponseWriter, r *http.Reque r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) claims, err := getTokenClaims(r) if err != nil || claims.Username == "" { - s.renderClientForbiddenPage(w, r, "Invalid token claims") + s.renderClientForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken)) return } limit := defaultQueryLimit @@ -1506,7 +1504,7 @@ func (s *httpdServer) handleClientGetShares(w http.ResponseWriter, r *http.Reque for { sh, err := dataprovider.GetShares(limit, len(shares), dataprovider.OrderASC, claims.Username) if err != nil { - s.renderInternalServerErrorPage(w, r, err) + s.renderClientInternalServerErrorPage(w, r, err) return } shares = append(shares, sh...) @@ -1515,7 +1513,7 @@ func (s *httpdServer) handleClientGetShares(w http.ResponseWriter, r *http.Reque } } data := clientSharesPage{ - baseClientPage: s.getBaseClientPageData(pageClientSharesTitle, webClientSharesPath, r), + baseClientPage: s.getBaseClientPageData(util.I18nSharesTitle, webClientSharesPath, r), Shares: shares, BasePublicSharesURL: webClientPubSharesPath, } @@ -1536,26 +1534,29 @@ func (s *httpdServer) handleWebClientProfilePost(w http.ResponseWriter, r *http. r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) err := r.ParseForm() if err != nil { - s.renderClientProfilePage(w, r, err.Error()) + s.renderClientProfilePage(w, r, util.I18nErrorInvalidForm) return } ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr) if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil { - s.renderClientForbiddenPage(w, r, err.Error()) + s.renderClientForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF)) return } claims, err := getTokenClaims(r) if err != nil || claims.Username == "" { - s.renderClientForbiddenPage(w, r, "Invalid token claims") + s.renderClientForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken)) return } user, userMerged, err := dataprovider.GetUserVariants(claims.Username, "") if err != nil { - s.renderClientProfilePage(w, r, err.Error()) + s.renderClientProfilePage(w, r, util.I18nErrorGetUser) return } if !userMerged.CanManagePublicKeys() && !userMerged.CanChangeAPIKeyAuth() && !userMerged.CanChangeInfo() { - s.renderClientForbiddenPage(w, r, "You are not allowed to change anything") + s.renderClientForbiddenPage(w, r, util.NewI18nError( + errors.New("you are not allowed to change anything"), + util.I18nErrorNoPermissions, + )) return } if userMerged.CanManagePublicKeys() { @@ -1575,11 +1576,10 @@ func (s *httpdServer) handleWebClientProfilePost(w http.ResponseWriter, r *http. } err = dataprovider.UpdateUser(&user, dataprovider.ActionExecutorSelf, ipAddr, user.Role) if err != nil { - s.renderClientProfilePage(w, r, err.Error()) + s.renderClientProfilePage(w, r, getI18NErrorString(err, util.I18nError500Message)) return } - s.renderClientMessagePage(w, r, "Profile updated", "", http.StatusOK, nil, - "Your profile has been successfully updated") + s.renderClientMessagePage(w, r, util.I18nProfileTitle, http.StatusOK, nil, util.I18nProfileUpdated) } func (s *httpdServer) handleWebClientMFA(w http.ResponseWriter, r *http.Request) { @@ -1600,7 +1600,7 @@ func (s *httpdServer) handleWebClientTwoFactorRecovery(w http.ResponseWriter, r func getShareFromPostFields(r *http.Request) (*dataprovider.Share, error) { share := &dataprovider.Share{} if err := r.ParseForm(); err != nil { - return share, err + return share, util.NewI18nError(err, util.I18nErrorInvalidForm) } for k := range r.Form { if hasPrefixAndSuffix(k, "paths[", "][path]") { @@ -1619,12 +1619,12 @@ func getShareFromPostFields(r *http.Request) (*dataprovider.Share, error) { share.AllowFrom = getSliceFromDelimitedValues(r.Form.Get("allowed_ip"), ",") scope, err := strconv.Atoi(r.Form.Get("scope")) if err != nil { - return share, err + return share, util.NewI18nError(err, util.I18nErrorShareScope) } share.Scope = dataprovider.ShareScope(scope) maxTokens, err := strconv.Atoi(r.Form.Get("max_tokens")) if err != nil { - return share, err + return share, util.NewI18nError(err, util.I18nErrorShareMaxTokens) } share.MaxTokens = maxTokens expirationDateMillis := int64(0) @@ -1632,7 +1632,7 @@ func getShareFromPostFields(r *http.Request) (*dataprovider.Share, error) { if expirationDateString != "" { expirationDate, err := time.Parse(webDateTimeFormat, expirationDateString) if err != nil { - return share, err + return share, util.NewI18nError(err, util.I18nErrorShareExpiration) } expirationDateMillis = util.GetTimeAsMsSinceEpoch(expirationDate) } @@ -1655,21 +1655,17 @@ func (s *httpdServer) handleWebClientForgotPwdPost(w http.ResponseWriter, r *htt ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr) err := r.ParseForm() if err != nil { - s.renderClientForgotPwdPage(w, r, err.Error(), ipAddr) + s.renderClientForgotPwdPage(w, r, util.I18nErrorInvalidForm, ipAddr) return } if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil { - s.renderClientForbiddenPage(w, r, err.Error()) + s.renderClientForbiddenPage(w, r, util.NewI18nError(err, util.I18nErrorInvalidCSRF)) return } username := strings.TrimSpace(r.Form.Get("username")) err = handleForgotPassword(r, username, false) if err != nil { - if e, ok := err.(*util.ValidationError); ok { - s.renderClientForgotPwdPage(w, r, e.GetErrorString(), ipAddr) - return - } - s.renderClientForgotPwdPage(w, r, err.Error(), ipAddr) + s.renderClientForgotPwdPage(w, r, getI18NErrorString(err, util.I18nErrorPwdResetGeneric), ipAddr) return } http.Redirect(w, r, webClientResetPwdPath, http.StatusFound) @@ -1705,18 +1701,19 @@ func (s *httpdServer) handleClientGetPDF(w http.ResponseWriter, r *http.Request) r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize) claims, err := getTokenClaims(r) if err != nil || claims.Username == "" { - s.renderClientForbiddenPage(w, r, "Invalid token claims") + s.renderClientForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken)) return } name := r.URL.Query().Get("path") if name == "" { - s.renderClientBadRequestPage(w, r, errors.New("no file specified")) + s.renderClientBadRequestPage(w, r, util.NewI18nError(errors.New("no file specified"), util.I18nError400Message)) return } name = util.CleanPath(name) user, err := dataprovider.GetUserWithGroupSettings(claims.Username, "") if err != nil { - s.renderClientMessagePage(w, r, "Unable to retrieve your user", "", getRespStatus(err), nil, "") + s.renderClientMessagePage(w, r, util.I18nError500Title, getRespStatus(err), + util.NewI18nError(err, util.I18nErrorGetUser), "") return } @@ -1724,7 +1721,7 @@ func (s *httpdServer) handleClientGetPDF(w http.ResponseWriter, r *http.Request) protocol := getProtocolFromRequest(r) connectionID := fmt.Sprintf("%v_%v", protocol, connID) if err := checkHTTPClientUser(&user, r, connectionID, false); err != nil { - s.renderClientForbiddenPage(w, r, err.Error()) + s.renderClientForbiddenPage(w, r, err) return } connection := &Connection{ @@ -1733,19 +1730,20 @@ func (s *httpdServer) handleClientGetPDF(w http.ResponseWriter, r *http.Request) request: r, } if err = common.Connections.Add(connection); err != nil { - s.renderClientMessagePage(w, r, "Unable to add connection", "", http.StatusTooManyRequests, err, "") + s.renderClientMessagePage(w, r, util.I18nError429Title, http.StatusTooManyRequests, + util.NewI18nError(err, util.I18nError429Message), "") return } defer common.Connections.Remove(connection.GetID()) info, err := connection.Stat(name, 0) if err != nil { - s.renderClientMessagePage(w, r, "Unable to get file", "", getRespStatus(err), err, "") + status := getRespStatus(err) + s.renderClientMessagePage(w, r, util.I18nErrorPDFTitle, status, util.NewI18nError(err, i18nFsMsg(status)), "") return } if info.IsDir() { - s.renderClientMessagePage(w, r, "Invalid file", fmt.Sprintf("%q is not a file", name), - http.StatusBadRequest, nil, "") + s.renderClientBadRequestPage(w, r, util.NewI18nError(fmt.Errorf("%q is not a file", name), util.I18nErrorPDFMessage)) return } connection.User.CheckFsRoot(connection.ID) //nolint:errcheck @@ -1758,8 +1756,8 @@ func (s *httpdServer) handleClientGetPDF(w http.ResponseWriter, r *http.Request) func (s *httpdServer) ensurePDF(w http.ResponseWriter, r *http.Request, name string, connection *Connection) error { reader, err := connection.getFileReader(name, 0, r.Method) if err != nil { - s.renderClientMessagePage(w, r, fmt.Sprintf("Unable to get a reader for the file %q", name), "", - getRespStatus(err), err, "") + s.renderClientMessagePage(w, r, util.I18nErrorPDFTitle, + getRespStatus(err), util.NewI18nError(err, util.I18nError500Message), "") return err } defer reader.Close() @@ -1767,14 +1765,15 @@ func (s *httpdServer) ensurePDF(w http.ResponseWriter, r *http.Request, name str var b bytes.Buffer _, err = io.CopyN(&b, reader, 128) if err != nil { - s.renderClientMessagePage(w, r, "Invalid PDF file", fmt.Sprintf("Unable to validate the file %q as PDF", name), - http.StatusBadRequest, nil, "") + s.renderClientMessagePage(w, r, util.I18nErrorPDFTitle, getRespStatus(err), + util.NewI18nError(err, util.I18nErrorPDFMessage), "") return err } if ctype := http.DetectContentType(b.Bytes()); ctype != "application/pdf" { connection.Log(logger.LevelDebug, "detected %q content type, expected PDF, file %q", ctype, name) - s.renderClientBadRequestPage(w, r, fmt.Errorf("the file %q does not look like a PDF", name)) - return errors.New("invalid PDF") + err := fmt.Errorf("the file %q does not look like a PDF", name) + s.renderClientBadRequestPage(w, r, util.NewI18nError(err, util.I18nErrorPDFMessage)) + return err } return nil } @@ -1788,22 +1787,22 @@ func (s *httpdServer) handleClientShareLoginPost(w http.ResponseWriter, r *http. r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize) ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr) if err := r.ParseForm(); err != nil { - s.renderShareLoginPage(w, r, err.Error(), ipAddr) + s.renderShareLoginPage(w, r, util.I18nErrorInvalidForm, ipAddr) return } if err := verifyCSRFToken(r.Form.Get(csrfFormToken), ipAddr); err != nil { - s.renderShareLoginPage(w, r, err.Error(), ipAddr) + s.renderShareLoginPage(w, r, util.I18nErrorInvalidCSRF, ipAddr) return } shareID := getURLParam(r, "id") share, err := dataprovider.ShareExists(shareID, "") if err != nil { - s.renderShareLoginPage(w, r, dataprovider.ErrInvalidCredentials.Error(), ipAddr) + s.renderShareLoginPage(w, r, util.I18nErrorInvalidCredentials, ipAddr) return } match, err := share.CheckCredentials(strings.TrimSpace(r.Form.Get("share_password"))) if !match || err != nil { - s.renderShareLoginPage(w, r, dataprovider.ErrInvalidCredentials.Error(), ipAddr) + s.renderShareLoginPage(w, r, util.I18nErrorInvalidCredentials, ipAddr) return } c := jwtTokenClaims{ @@ -1811,7 +1810,7 @@ func (s *httpdServer) handleClientShareLoginPost(w http.ResponseWriter, r *http. } err = c.createAndSetCookie(w, r, s.tokenAuth, tokenAudienceWebShare, ipAddr) if err != nil { - s.renderShareLoginPage(w, r, common.ErrInternalFailure.Error(), ipAddr) + s.renderShareLoginPage(w, r, util.I18nError500Message, ipAddr) return } next := path.Clean(r.URL.Query().Get("next")) @@ -1821,8 +1820,7 @@ func (s *httpdServer) handleClientShareLoginPost(w http.ResponseWriter, r *http. http.Redirect(w, r, redirectTo, http.StatusFound) return } - s.renderClientMessagePage(w, r, "Share Login OK", "Share login successful, you can now use your link", - http.StatusOK, nil, "") + s.renderClientMessagePage(w, r, util.I18nSharedFilesTitle, http.StatusOK, nil, util.I18nShareLoginOK) } func (s *httpdServer) handleClientSharedFile(w http.ResponseWriter, r *http.Request) { diff --git a/internal/util/i18n.go b/internal/util/i18n.go new file mode 100644 index 00000000..77ca7481 --- /dev/null +++ b/internal/util/i18n.go @@ -0,0 +1,171 @@ +// Copyright (C) 2023 Nicola Murino +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, version 3. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package util + +import ( + "errors" +) + +// localization id for the Web frontend +const ( + I18nLoginTitle = "title.login" + I18nShareLoginTitle = "title.share_login" + I18nFilesTitle = "title.files" + I18nSharesTitle = "title.shares" + I18nShareAddTitle = "title.add_share" + I18nShareUpdateTitle = "title.update_share" + I18nProfileTitle = "title.profile" + I18nChangePwdTitle = "title.change_password" + I18n2FATitle = "title.two_factor_auth" + I18nEditFileTitle = "title.edit_file" + I18nViewFileTitle = "title.view_file" + I18nForgotPwdTitle = "title.recovery_password" + I18nResetPwdTitle = "title.reset_password" + I18nSharedFilesTitle = "title.shared_files" + I18nShareUploadTitle = "title.upload_to_share" + I18nShareDownloadTitle = "title.download_shared_file" + I18nShareAccessErrorTitle = "title.share_access_error" + I18nInvalidAuthReqTitle = "title.invalid_auth_request" + I18nError403Title = "title.error403" + I18nError400Title = "title.error400" + I18nError416Title = "title.error416" + I18nError429Title = "title.error429" + I18nError500Title = "title.error500" + I18nErrorPDFTitle = "title.errorPDF" + I18nErrorEditorTitle = "title.error_editor" + I18nInvalidAuth = "general.invalid_auth_request" + I18nError429Message = "general.error429" + I18nError400Message = "general.error400" + I18nError403Message = "general.error403" + I18nError404Message = "general.error404" + I18nError416Message = "general.error416" + I18nError500Message = "general.error500" + I18nErrorPDFMessage = "general.errorPDF" + I18nErrorInvalidToken = "general.invalid_token" + I18nErrorInvalidForm = "general.invalid_form" + I18nErrorInvalidCredentials = "general.invalid_credentials" + I18nErrorInvalidCSRF = "general.invalid_csrf" + I18nErrorFsGeneric = "fs.err_generic" + I18nErrorDirListGeneric = "fs.dir_list.err_generic" + I18nErrorDirList403 = "fs.dir_list.err_403" + I18nErrorDirList429 = "fs.dir_list.err_429" + I18nErrorDirListUser = "fs.dir_list.err_user" + I18nErrorFsValidation = "fs.err_validation" + I18nErrorChangePwdRequiredFields = "change_pwd.required_fields" + I18nErrorChangePwdNoMatch = "change_pwd.no_match" + I18nErrorChangePwdGeneric = "change_pwd.generic" + I18nErrorChangePwdNoDifferent = "change_pwd.no_different" + I18nErrorChangePwdCurrentNoMatch = "change_pwd.current_no_match" + I18nErrorChangePwdRequired = "change_pwd.required" + I18nErrorUsernameRequired = "general.username_required" + I18nErrorGetUser = "general.err_user" + I18nErrorPwdResetForbidded = "login.reset_pwd_forbidden" + I18nErrorPwdResetNoEmail = "login.reset_pwd_no_email" + I18nErrorPwdResetSendEmail = "login.reset_pwd_send_email_err" + I18nErrorPwdResetGeneric = "login.reset_pwd_err_generic" + I18nErrorProtocolForbidden = "general.err_protocol_forbidden" + I18nErrorPwdLoginForbidden = "general.pwd_login_forbidden" + I18nErrorIPForbidden = "general.ip_forbidden" + I18nErrorConnectionForbidden = "general.connection_forbidden" + I18nErrorReservedUsername = "user.username_reserved" + I18nErrorInvalidEmail = "general.email_invalid" + I18nErrorInvalidUser = "user.username_invalid" + I18nErrorHomeRequired = "user.home_required" + I18nErrorHomeInvalid = "user.home_invalid" + I18nErrorPubKeyInvalid = "user.pub_key_invalid" + I18nErrorPrimaryGroup = "user.err_primary_group" + I18nErrorDuplicateGroup = "user.err_duplicate_group" + I18nErrorNoPermission = "user.no_permissions" + I18nErrorNoRootPermission = "user.no_root_permissions" + I18nErrorGenericPermission = "user.err_permissions_generic" + I18nError2FAInvalid = "user.2fa_invalid" + I18nErrorRecoveryCodesInvalid = "user.recovery_codes_invalid" + I18nErrorFolderNameRequired = "general.foldername_required" + I18nErrorFolderMountPathRequired = "user.folder_path_required" + I18nErrorDuplicatedFolders = "user.folder_duplicated" + I18nErrorOverlappedFolders = "user.folder_overlapped" + I18nErrorFolderQuotaSizeInvalid = "user.folder_quota_size_invalid" + I18nErrorFolderQuotaFileInvalid = "user.folder_quota_file_invalid" + I18nErrorFolderQuotaInvalid = "user.folder_quota_invalid" + I18nErrorPasswordComplexity = "general.err_password_complexity" + I18nErrorIPFiltersInvalid = "user.ip_filters_invalid" + I18nErrorSourceBWLimitInvalid = "user.src_bw_limits_invalid" + I18nErrorShareExpirationInvalid = "user.share_expiration_invalid" + I18nErrorFilePatternPathInvalid = "user.file_pattern_path_invalid" + I18nErrorFilePatternDuplicated = "user.file_pattern_duplicated" + I18nErrorFilePatternInvalid = "user.file_pattern_invalid" + I18nErrorDisableActive2FA = "user.disable_active_2fa" + I18nErrorPwdChangeConflict = "user.pwd_change_conflict" + I18nErrorLoginAfterReset = "login.reset_ok_login_error" + I18nErrorShareScope = "share.scope_invalid" + I18nErrorShareMaxTokens = "share.max_tokens_invalid" + I18nErrorShareExpiration = "share.expiration_invalid" + I18nErrorShareNoPwd = "share.err_no_password" + I18nErrorShareExpirationOutOfRange = "share.expiration_out_of_range" + I18nErrorShareGeneric = "share.generic" + I18nErrorNameRequired = "general.name_required" + I18nErrorSharePathRequired = "share.path_required" + I18nErrorShareWriteScope = "share.path_write_scope" + I18nErrorShareNestedPaths = "share.nested_paths" + I18nErrorShareExpirationPast = "share.expiration_past" + I18nErrorInvalidIPMask = "general.allowed_ip_mask_invalid" + I18nErrorShareUsage = "share.usage_exceed" + I18nErrorShareExpired = "share.expired" + I18nErrorLoginFromIPDenied = "login.ip_not_allowed" + I18nError2FARequired = "login.two_factor_required" + I18nErrorNoOIDCFeature = "general.no_oidc_feature" + I18nErrorNoPermissions = "general.no_permissions" + I18nErrorShareBrowsePaths = "share.browsable_multiple_paths" + I18nErrorShareBrowseNoDir = "share.browsable_non_dir" + I18nErrorPathInvalid = "general.path_invalid" + I18nErrorQuotaRead = "general.err_quota_read" + I18nErrorEditDir = "general.error_edit_dir" + I18nErrorEditSize = "general.error_edit_size" + I18nProfileUpdated = "general.profile_updated" + I18nShareLoginOK = "general.share_ok" + I18n2FADisabled = "2fa.disabled" +) + +// NewI18nError returns a I18nError wrappring the provided error +func NewI18nError(err error, message string) *I18nError { + var errI18n *I18nError + if errors.As(err, &errI18n) { + return errI18n + } + return &I18nError{ + err: err, + I18nMessage: message, + } +} + +// I18nError is an error wrapper that add a message to use for localization. +type I18nError struct { + err error + I18nMessage string +} + +// Error returns the wrapped error string. +func (e *I18nError) Error() string { + return e.err.Error() +} + +// Is reports if target matches +func (e *I18nError) Is(target error) bool { + if errors.Is(e.err, target) { + return true + } + _, ok := target.(*I18nError) + return ok +} diff --git a/internal/util/util.go b/internal/util/util.go index 153a519d..2d993aae 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -343,7 +343,7 @@ func GetIntFromPointer(val *int64) int64 { // GetTimeFromPointer returns the time value or now func GetTimeFromPointer(val *time.Time) time.Time { if val == nil { - return time.Now() + return time.Unix(0, 0) } return *val } diff --git a/static/locales/en/translation.json b/static/locales/en/translation.json new file mode 100644 index 00000000..28fe8326 --- /dev/null +++ b/static/locales/en/translation.json @@ -0,0 +1,370 @@ +{ + "title": { + "login": "Login", + "share_login": "Share Login", + "profile": "Profile", + "change_password": "Change password", + "files": "Files", + "shares": "Shares", + "add_share": "Add share", + "update_share": "Update share", + "two_factor_auth": "Two-factor authentication", + "two_factor_auth_short": "2FA", + "edit_file": "Edit file", + "view_file": "View file", + "recovery_password": "Password recovery", + "reset_password": "Password reset", + "shared_files": "Shared files", + "upload_to_share": "Upload to share", + "download_shared_file": "Download shared file", + "share_access_error": "Unable to access the share", + "invalid_auth_request": "Invalid authentication request", + "error429": "Too Many Requests", + "error403": "Forbidden", + "error400": "Bad Request", + "error416": "Requested Range Not Satisfiable", + "error500": "Internal Server Error", + "errorPDF": "Unable to show PDF file", + "error_editor": "Cannot open file editor" + }, + "login": { + "username": "Username", + "password": "Password", + "your_username": "Your username", + "forgot_password": "Forgot Password?", + "forgot_password_msg": "Enter your account username below, you will receive a password reset code by email.", + "send_reset_code": "Send Reset Code", + "signin": "Sign in", + "signin_openid": "Sign in with OpenID", + "signout": "Sign out", + "auth_code": "Authentication code", + "two_factor_help": "Open the two-factor authentication app on your device to view your authentication code and verify your identity.", + "two_factor_msg": "Enter a two-factor recovery code", + "recovery_code": "Recovery code", + "recovery_code_msg": "You can enter one of your recovery codes in case you lost access to your mobile device.", + "reset_password": "Reset Password", + "reset_pwd_msg": "Check your email for the confirmation code", + "reset_submit": "Update Password and Sign in", + "confirm_code": "Confirmation code", + "reset_pwd_forbidden": "You are not allowed to reset your password", + "reset_pwd_no_email": "Your account does not have an email address, it is not possible to reset your password by sending an email verification code", + "reset_pwd_send_email_err": "Unable to send confirmation code via email", + "reset_pwd_err_generic": "Unexpected error while resetting password", + "reset_ok_login_error": "The password reset completed successfully but an unexpected error occurred while signing in", + "ip_not_allowed": "Login is not allowed from this IP address", + "two_factor_required": "Two-factor authentication is required, set it up" + }, + "theme": { + "light": "Light", + "dark": "Dark", + "system": "Auto" + }, + "general": { + "invalid_auth_request": "The authentication request does not meet security requirements", + "error400": "The received request is not valid", + "error403": "You do not have the required permissions", + "error404": "The requested resource does not exist", + "error416": "The requested file fragment could not be returned", + "error429": "Rate limit exceeded", + "error500": "The server is unable to fulfill your request", + "errorPDF": "This file does not look like a PDF", + "error_edit_dir": "Cannot edit a directory", + "error_edit_size": "The file size exceeds the maximum allowed size", + "invalid_form": "Invalid form", + "invalid_credentials": "Invalid credentials", + "invalid_csrf": "The form token is not valid", + "invalid_token": "Invalid permissions", + "confirm_logout": "Are you sure you want to sign out?", + "wait": "Please wait...", + "or": "or", + "ok": "OK", + "none": "None", + "cancel": "Cancel", + "submit": "Save", + "back": "Back", + "add": "Add", + "enable": "Enable", + "disable": "Disable", + "close": "Close", + "search": "Search", + "configuration": "Configuration", + "config_saved": "Configuration saved", + "rename": "Rename", + "confirm": "Confirm", + "edit": "Edit", + "delete": "Delete", + "delete_confirm": "Do you want to delete \"{{- name}}\"? This action is irreversible", + "delete_confirm_generic": "Do you want to delete the selected item? This action is irreversible", + "delete_multi_confirm": "Do you want to delete the selected items? This action is irreversible", + "delete_error_generic": "Unable to delete the selected item", + "delete_error_403": "$t(general.delete_error_generic). $t(general.error403)", + "delete_error_404": "$t(general.delete_error_generic). $t(general.error404)", + "loading": "Loading...", + "name": "Name", + "size": "Size", + "last_modified": "Last Modified", + "info": "Info", + "datetime": "{{- val, datetime}}", + "selected_items_one": "{{count}} item selected", + "selected_items_other": "{{count}} items selected", + "name_required": "Name is required", + "name_different": "The new name must be different from the current name", + "html5_media_not_supported": "Your browser does not support HTML5 audio/video.", + "choose_target_folder": "Choose target folder", + "source_name": "Source name", + "target_folder": "Target folder", + "dest_name": "Destination name", + "my_profile": "My profile", + "description": "Description", + "email": "Email", + "api_key_auth": "API key authentication", + "api_key_auth_help": "Allow to impersonate yourself, in REST API, using an API key", + "pub_keys": "Public keys", + "pub_key_placeholder": "Paste your public key here", + "verify": "Verify", + "problems": "Having problems?", + "generate": "Generate", + "view": "View", + "allowed_ip_mask": "Allowed IP/Mask", + "allowed_ip_mask_help": "Comma separated IP/Mask in CIDR format, for example \"192.168.1.0/24,10.8.0.100/32\"", + "allowed_ip_mask_invalid": "Invalid allowed IP/Mask", + "username_required": "The username is required", + "foldername_required": "The folder name is required", + "err_user": "Unable to validate your user", + "err_protocol_forbidden": "HTTP protocol is not allowed for your user", + "pwd_login_forbidden": "The password login method is not allowed for your user", + "ip_forbidden": "Login not allowed from this IP address", + "email_invalid": "The email address is invalid", + "err_password_complexity": "The password provided does not meet the complexity requirements", + "no_oidc_feature": "This feature is not available if you are logged in with OpenID", + "connection_forbidden": "You are not allowed to connect", + "no_permissions": "You are not allowed to change anything", + "path_invalid": "Invalid path", + "err_quota_read": "Read denied due to quota limit", + "profile_updated": "Your profile has been successfully updated", + "share_ok": "Share access successful, you can now use your link", + "qr_code": "QR Code" + }, + "fs": { + "view_file": "View file \"{{- path}}\"", + "edit_file": "Edit file \"{{- path}}\"", + "new_folder": "New Folder", + "select_across_pages": "Select across pages", + "actions": "Actions", + "download": "Download", + "download_ready": "Your download is ready", + "move_copy": "Move or copy", + "share": "Share", + "home": "Home", + "create_folder_msg": "Folder name", + "no_more_subfolders": "No more subfolders in here", + "no_files_folders": "No files or folders", + "invalid_name": "File or folder names cannot contain \"/\"", + "folder_name_required": "Folder name is required", + "deleting": "Delete {{idx}}/{{total}}: {{- name}}", + "copying": "Copy {{idx}}/{{total}}: {{- name}}", + "moving": "Move {{idx}}/{{total}}: {{- name}}", + "uploading": "Upload {{idx}}/{{total}}: {{- name}}", + "err_403": "Permission denied", + "err_429": "Too many concurrent requests", + "err_generic": "Unable to access the requested resource", + "err_validation": "Invalid filesystem configuration", + "dir_list": { + "err_generic": "Failed to get directory listing", + "err_403": "$t(fs.dir_list.err_generic). $t(fs.err_403)", + "err_429": "$t(fs.dir_list.err_generic). $t(fs.err_429)", + "err_user": "$t(fs.dir_list.err_generic). $t(general.err_user)" + }, + "create_dir": { + "err_generic": "Error creating new folder", + "err_403": "$t(fs.create_dir.err_generic). $t(fs.err_403)", + "err_429": "$t(fs.create_dir.err_generic). $t(fs.err_429)" + }, + "save": { + "err_generic": "Error saving file", + "err_403": "$t(fs.create_dir.err_generic). $t(fs.err_403)", + "err_429": "$t(fs.create_dir.err_generic). $t(fs.err_429)" + }, + "delete": { + "err_generic": "Unable to delete \"{{- name}}\"", + "err_403": "$t(fs.delete.err_generic). $t(fs.err_403)", + "err_429": "$t(fs.delete.err_generic). $t(fs.err_429)" + }, + "delete_multi": { + "err_generic_partial": "Not all the selected items have been deleted, please reload the page", + "err_generic": "Unable to delete the selected items", + "err_403_partial": "$t(fs.delete_multi.err_generic_partial). $t(fs.err_403)", + "err_429_partial": "$t(fs.delete_multi.err_generic_partial). $t(fs.err_429)", + "err_403": "$t(fs.delete_multi.err_generic). $t(fs.err_403)", + "err_429": "$t(fs.delete_multi.err_generic). $t(fs.err_429)" + }, + "copy": { + "msg": "Copy", + "err_generic": "Error copying files/directories", + "err_403": "$t(fs.copy.err_generic). $t(fs.err_403)", + "err_429": "$t(fs.copy.err_generic). $t(fs.err_429)" + }, + "move": { + "msg": "Move", + "err_generic": "Error moving files/directories", + "err_403": "$t(fs.move.err_generic). $t(fs.err_403)", + "err_429": "$t(fs.move.err_generic). $t(fs.err_429)" + }, + "rename": { + "new_name": "New name", + "err_generic": "Unable to rename \"{{- name}}\"", + "err_403": "$t(fs.rename.err_generic). $t(fs.err_403)", + "err_429": "$t(fs.rename.err_generic). $t(fs.err_429)" + }, + "upload": { + "text": "Upload Files", + "success": "Files uploaded successfully", + "message": "Drop files here or click to upload.", + "err_generic": "Error uploading files", + "err_403": "$t(fs.upload.err_generic). $t(fs.err_403)", + "err_429": "$t(fs.upload.err_generic). $t(fs.err_429)" + }, + "quota_usage": { + "title": "Quota usage", + "disk": "Disk quota", + "size": "Size: {{- val}}", + "size_percentage": "Size: {{- val}} ({{percentage}}%)", + "files": "Files: {{- num}}", + "files_percentage": "Files: {{- val}} ({{percentage}}%)", + "transfer": "Transfer quota", + "total": "Total: {{- val}}", + "total_percentage": "Total: {{- val}} ({{percentage}}%)", + "uploads": "Uploads: {{- val}}", + "uploads_percentage": "Uploads: {{- val}} ({{percentage}}%)", + "downloads": "Downloads: {{- val}}", + "downloads_percentage": "Downloads: {{- val}} ({{percentage}}%)" + } + }, + "datatable": { + "info": "Showing _START_ to _END_ of _TOTAL_ records", + "info_empty": "Showing no record", + "info_filtered": "(filtered from _MAX_ total records)", + "processing": "Processing..." + }, + "editor": { + "keybinding": "Editor keybindings", + "search": "Open search panel", + "goto": "Jump to line", + "indent_more": "Indent more", + "indent_less": "Indent less" + }, + "2fa": { + "title": "Two-factor authentication using Authenticator apps", + "msg_enabled": "Two-factor authentication is enabled", + "msg_disabled": "Secure Your Account", + "msg_info": "Two-factor authentication adds an extra layer of security to your account. To log in you'll need to provide an additional authentication code.", + "require_for": "Require 2FA for", + "generate": "Generate new secret", + "recovery_codes": "Recovery codes", + "new_recovery_codes": "New recovery codes", + "recovery_codes_msg1": "Recovery codes are a set of one time use codes that can be used in place of the authentication code to login to the web UI. You can use them if you lose access to your phone to login to your account and disable or regenerate two-factor auth configuration.", + "recovery_codes_msg2": "To keep your account secure, don't share or distribute your recovery codes. We recommend saving them with a secure password manager.", + "recovery_codes_msg3": "If you generate new recovery codes, you automatically invalidate old ones.", + "info_title": "Learn about two-factor authentication", + "info1": "SSH protocol (SFTP/SCP/SSH commands) will ask for the passcode if the client uses keyboard interactive authentication.", + "info2": "HTTP protocol means Web UI and REST APIs. Web UI will ask for the passcode using a specific page. For REST API you have to add the passcode using an HTTP header.", + "info3": "FTP has no standard way to support two-factor authentication, if you enable the FTP support, you have to add the TOTP passcode after the password. For example if your password is \"password\" and your one time passcode is \"123456\" you have to use \"password123456\" as password.", + "info4": "WebDAV is not supported since each single request must be authenticated and a passcode cannot be reused.", + "setup_title": "Set up two-factor authentication", + "setup_msg": "Use your preferred Authenticator App (e.g. Microsoft Authenticator, Google Authenticator, Authy, 1Password etc. ) to scan the QR code. It will generate an authentication code for you to enter below.", + "setup_help": "If you have trouble using the QR code, select manual entry on your app, and enter the code:", + "disable_question": "Do you want to disable two-factor authentication?", + "generate_question": "Do you want to generate a new secret and invalidate the previous one? Any registered Authenticator App will stop working", + "disabled": "Two-factor authentication is disabled", + "recovery_codes_gen_err": "Failed to generate new recovery codes", + "recovery_codes_get_err": "Unable to obtain recovery codes", + "auth_code_invalid": "Failed to validate the provided authentication code", + "auth_secret_gen_err": "Failed to generate authentication secret", + "save_err": "Failed to save configuration", + "save_err_proto": "$t(2fa.save_err). Make sure the protocols enabled comply with company policy", + "auth_code_required": "The authentication code is required", + "no_protocol": "Please select at least a protocol" + }, + "share": { + "scope": "Scope", + "scope_read": "Read", + "scope_write": "Write", + "scope_read_write": "Read/Write", + "scope_help": "For scope \"Write\" and \"Read/Write\" you have to define a single path and it must be a directory", + "paths": "Paths", + "path_help": "file or directory path, i.e. /dir or /dir/file.txt", + "password_help": "If set the share will be password-protected", + "expiration": "Expiration", + "expiration_help": "Pick an expiration date", + "max_tokens": "Max tokens", + "max_tokens_help": "Maximum number of times this share can be accessed. 0 means no limit", + "view_manage": "View and manage shares", + "no_share": "No sharing", + "password_protected": "Password protected.", + "expiration_date": "Expiration: {{- val, datetime}}. ", + "last_use": "Last use: {{- val, datetime}}. ", + "usage": "Usage: {{used}}/{{total}}. ", + "used_tokens": "Used token: {{used}}. ", + "scope_invalid": "Invalid scope", + "max_tokens_invalid": "Invalid max tokens", + "expiration_invalid": "Invalid expiration", + "err_no_password": "You are not allowed to share files/folders without password", + "expiration_out_of_range": "Set an expiration date and ensure it complies with company policy, e.g. is not too far in the future", + "generic": "Unexpected error saving share", + "path_required": "At least a path is required", + "path_write_scope": "The write scope requires exactly one path", + "nested_paths": "Paths cannot be nested", + "expiration_past": "The expiration date must be in the future", + "usage_exceed": "Maximum sharing usage exceeded", + "expired": "Sharing has expired", + "browsable_multiple_paths": "A share with multiple paths is not browsable", + "browsable_non_dir": "The share is not a directory so it is not browsable" + }, + "select2": { + "no_results": "No results found", + "searching": "Searching...", + "removeall": "Remove all items", + "remove": "Remove item" + }, + "change_pwd": { + "info": "Enter your current password, for security reasons, and then your new password twice, to verify that you have written it correctly. You will be logged out after changing your password", + "current": "Current password", + "new": "New password", + "confirm": "Confirm password", + "save": "Change my password", + "required_fields": "Please provide the current password and the new one two times", + "no_match": "The two password fields do not match", + "no_different": "The new password must be different from the current one", + "current_no_match": "Current password does not match", + "generic": "Unexpected error while changing password", + "required": "Password change is required. Set a new password to continue using your account" + }, + "user": { + "username_reserved": "The specified username is reserved", + "username_invalid": "The specified username is not valid, the following characters are allowed: a-zA-Z0-9-_.~", + "home_required": "The home directory is mandatory", + "home_invalid": "The home directory must be an absolute path", + "pub_key_invalid": "Invalid public key", + "err_primary_group": "Only one primary group is allowed", + "err_duplicate_group": "Duplicate groups detected", + "no_permissions": "Directories permissions are mandatory", + "no_root_permissions": "Home directory permissions are required", + "err_permissions_generic": "Invalid permissions: Make sure you use valid absolute paths", + "2fa_invalid": "Invalid configuration for two-factor authentication", + "recovery_codes_invalid": "Invalid recovery codes", + "folder_path_required": "The virtual folder mount path is required", + "folder_duplicated": "Duplicated virtual folders detected", + "folder_overlapped": "Overlapping virtual folders detected", + "folder_quota_size_invalid": "The quota as size of virtual folders must be greater than or equal to -1", + "folder_quota_file_invalid": "The quota as files of virtual folders must be greater than or equal to -1", + "folder_quota_invalid": "Quotas as size and as number of files of virtual folders must be both -1 or greater or equal than zero", + "ip_filters_invalid": "Invalid IP filters, make sure they respect CIDR notation, for example 192.168.1.0/24", + "src_bw_limits_invalid": "Invalid per-source bandwidth speed limits", + "share_expiration_invalid": "The expiration for shares must be greater than or equal to the defined default value", + "file_pattern_path_invalid": "File name pattern filter paths must be absolute", + "file_pattern_duplicated": "Duplicated file name pattern filters detected", + "file_pattern_invalid": "Invalid file name pattern filters", + "disable_active_2fa": "Two-factor authentication cannot be disabled for a user with an active configuration", + "pwd_change_conflict": "It is not possible to request a password change and at the same time prevent the password from being changed" + } +} \ No newline at end of file diff --git a/static/locales/it/translation.json b/static/locales/it/translation.json new file mode 100644 index 00000000..bec4b0f9 --- /dev/null +++ b/static/locales/it/translation.json @@ -0,0 +1,370 @@ +{ + "title": { + "login": "Accedi", + "share_login": "Accedi alla condivisione", + "profile": "Profilo", + "change_password": "Cambio password", + "files": "File", + "shares": "Condivisioni", + "add_share": "Aggiungi condivisione", + "update_share": "Modifica condivisione", + "two_factor_auth": "Autenticazione a due fattori", + "two_factor_auth_short": "2FA", + "edit_file": "Modifica file", + "view_file": "Visualizza file", + "recovery_password": "Recupero password", + "reset_password": "Reimpostazione password", + "shared_files": "File condivisi", + "upload_to_share": "Carica nella condivisione", + "download_shared_file": "Scarica file condiviso", + "share_access_error": "Impossibile accedere alla condivisione", + "invalid_auth_request": "Richiesta di autenticazione non valida", + "error429": "Troppe richieste", + "error403": "Non permesso", + "error400": "Richiesta non valida", + "error416": "Impossibile tornare l'intervallo richiesto", + "error500": "Errore interno del server", + "errorPDF": "Impossibile mostrare il file PDF", + "error_editor": "Impossibile aprire l'editor di file" + }, + "login": { + "username": "Nome utente", + "your_username": "Il tuo nome utente", + "password": "Password", + "forgot_password": "Password dimenticata?", + "forgot_password_msg": "Inserisci il nome utente del tuo account qui sotto, riceverai un codice di reimpostazione della password via e-mail.", + "send_reset_code": "Invia codice di ripristino", + "signin": "Accedi", + "signin_openid": "Accedi con OpenID", + "signout": "Esci", + "auth_code": "Codice di autenticazione", + "two_factor_help": "Apri l'app di autenticazione a due fattori sul tuo dispositivo per visualizzare il tuo codice di autenticazione e verificare la tua identità.", + "two_factor_msg": "Inserisci un codice di ripristino per l'autenticazione a due fattori", + "recovery_code": "Codice di ripristino", + "recovery_code_msg": "Puoi inserire uno dei tuoi codici di ripristino nel caso in cui hai perso l'accesso al tuo dispositivo mobile.", + "reset_password": "Reimposta password", + "reset_pwd_msg": "Controlla la tua email per il codice di conferma", + "reset_submit": "Aggiorna password e accedi", + "confirm_code": "Codice di conferma", + "reset_pwd_forbidden": "Non ti è consentito reimpostare la password", + "reset_pwd_no_email": "Il tuo account non ha un indirizzo email, non è possibile reimpostare la password inviando un codice di verifica via email", + "reset_pwd_send_email_err": "Errore nell'invio del codice di conferma via e-mail", + "reset_pwd_err_generic": "Errore imprevisto durante la reimpostazione della password", + "reset_ok_login_error": "La reimpostazione della password è stata completata correttamente ma si è verificato un errore imprevisto durante l'accesso", + "ip_not_allowed": "L'accesso non è consentito da questo indirizzo IP", + "two_factor_required": "È richiesta l'autenticazione a due fattori, configurala" + }, + "theme": { + "light": "Chiaro", + "dark": "Scuro", + "system": "Auto" + }, + "general": { + "invalid_auth_request": "La richiesta di autenticazione non soddisfa i requisiti di sicurezza", + "error400": "La richiesta ricevuta non è valida", + "error403": "Non si dispone delle autorizzazioni richieste", + "error404": "La risorsa richiesta non esiste", + "error416": "Il frammento di file richiesto non può essere restituito", + "error429": "Limite di richieste per unità di tempo superato", + "error500": "Il server non è in grado di soddisfare la tua richiesta", + "errorPDF": "Questo file non sembra un PDF", + "error_edit_dir": "Impossibile modificare una cartella", + "error_edit_size": "La dimensione del file supera la dimensione massima consentita", + "invalid_form": "Modulo non valido", + "invalid_credentials": "Credenziali non valide", + "invalid_csrf": "Il token del modulo non è valido", + "invalid_token": "Permessi non validi", + "confirm_logout": "Sei sicuro di volerti disconnettere?", + "wait": "Attendere prego...", + "or": "o", + "ok": "OK", + "none": "Nessuna", + "cancel": "Annulla", + "submit": "Salva", + "back": "Indietro", + "add": "Aggiungi", + "enable": "Abilita", + "disable": "Disabilita", + "close": "Chiudi", + "search": "Cerca", + "configuration": "Configurazione", + "config_saved": "Configurazione salvata", + "rename": "Rinomina", + "confirm": "Conferma", + "edit": "Modifica", + "delete": "Elimina", + "delete_confirm": "Vuoi eliminare \"{{- name}}\"? Questa azione è irreversibile", + "delete_confirm_generic": "Vuoi eliminare l'elemento selezionato? Questa azione è irreversibile", + "delete_multi_confirm": "Vuoi eliminare gli elementi selezionati? Questa azione è irreversibile", + "delete_error_generic": "Impossibile eliminare l'elemento selezionato", + "delete_error_403": "$t(general.delete_error_generic). $t(general.error403)", + "delete_error_404": "$t(general.delete_error_generic). $t(general.error404)", + "loading": "Caricamento...", + "name": "Nome", + "size": "Dimensione", + "last_modified": "Ultima modifica", + "info": "Info", + "datetime": "{{- val, datetime}}", + "selected_items_one": "{{count}} elemento selezionato", + "selected_items_other": "{{count}} elementi selezionati", + "name_required": "Il nome è obbligatorio", + "name_different": "Il nuovo nome deve essere diverso dal nome attuale", + "html5_media_not_supported": "Il tuo browser non supporta audio/video HTML5.", + "choose_target_folder": "Scegli la cartella di destinazione", + "source_name": "Nome di origine", + "target_folder": "Cartella di destinazione", + "dest_name": "Nome di destinazione", + "my_profile": "Il mio profilo", + "description": "Descrizione", + "email": "Email", + "api_key_auth": "Autenticazione mediante chiave API", + "api_key_auth_help": "Permette di imperosare te stesso, nelle API REST, mediante una chiave API", + "pub_keys": "Chiavi pubbliche", + "pub_key_placeholder": "Incolla qui la tua chiave pubblica", + "verify": "Verifica", + "problems": "Hai problemi?", + "generate": "Genera", + "view": "Visualizza", + "allowed_ip_mask": "IP/Reti permesse", + "allowed_ip_mask_help": "IP/reti separate da virgola in formato CIDR, ad esempio \"192.168.1.0/24,10.8.0.100/32\"", + "allowed_ip_mask_invalid": "IP/reti permesse non valide", + "username_required": "Il nome utente è obbligatorio", + "foldername_required": "Il nome della cartella è obbligatorio", + "err_user": "Errore validazione utente", + "err_protocol_forbidden": "Il protocollo HTTP non è consentito per il tuo utente", + "pwd_login_forbidden": "Il metodo di accesso tramite password non è consentito per il tuo utente", + "ip_forbidden": "Accesso non consentito da questo indirizzo IP", + "email_invalid": "L'indirizzo e-mail non è valido", + "err_password_complexity": "La password fornita non soddisfa i requisiti di complessità", + "no_oidc_feature": "Questa funzionalità non è disponibile se hai effettuato l'accesso con OpenID", + "connection_forbidden": "Non ti è consentito connetterti", + "no_permissions": "Non ti è consentito cambiare nulla", + "path_invalid": "Percorso non valido", + "err_quota_read": "Lettura negata a causa del limite di quota", + "profile_updated": "Il tuo profilo è stato aggiornato con successo", + "share_ok": "Accesso alla condivisione riuscito, ora puoi utilizzare il tuo collegamento", + "qr_code": "Codice QR" + }, + "fs": { + "view_file": "Visualizza file \"{{- path}}\"", + "edit_file": "Modifica file \"{{- path}}\"", + "new_folder": "Nuova cartella", + "select_across_pages": "Seleziona tra le pagine", + "actions": "Azioni", + "download": "Scarica", + "download_ready": "Il tuo download è pronto", + "move_copy": "Sposta o copia", + "share": "Condividi", + "home": "Home", + "create_folder_msg": "Nome cartella", + "no_more_subfolders": "Non ci sono più sottocartelle qui", + "no_files_folders": "Nessun file o cartella", + "invalid_name": "I nomi di file o cartelle non possono contenere \"/\"", + "folder_name_required": "Il nome della cartella è obbligatorio", + "deleting": "Eliminazione {{idx}}/{{total}}: {{- name}}", + "copying": "Copia {{idx}}/{{total}}: {{- name}}", + "moving": "Spostamento {{idx}}/{{total}}: {{- name}}", + "uploading": "Caricamento {{idx}}/{{total}}: {{- name}}", + "err_403": "Non si dispone delle autorizzazioni richieste", + "err_429": "Troppe richieste contemporanee", + "err_generic": "Impossibile accedere alla risorsa richiesta", + "err_validation": "Configurazione del filesystem non valida", + "dir_list": { + "err_generic": "Impossibile ottenere l'elenco della directory", + "err_403": "$t(fs.dir_list.err_generic). $t(fs.err_403)", + "err_429": "$t(fs.dir_list.err_generic). $t(fs.err_429)", + "err_user": "$t(fs.dir_list.err_generic). $t(general.err_user)" + }, + "create_dir": { + "err_generic": "Errore durante la creazione delle nuova cartella", + "err_403": "$t(fs.create_dir.err_generic). $t(fs.err_403)", + "err_429": "$t(fs.create_dir.err_generic). $t(fs.err_429)" + }, + "save": { + "err_generic": "Errore durante il salvataggio del file", + "err_403": "$t(fs.create_dir.err_generic). $t(fs.err_403)", + "err_429": "$t(fs.create_dir.err_generic). $t(fs.err_429)" + }, + "delete": { + "err_generic": "Impossibile eliminare \"{{- name}}\"", + "err_403": "$t(fs.delete.err_generic). $t(fs.err_403)", + "err_429": "$t(fs.delete.err_generic). $t(fs.err_429)" + }, + "delete_multi": { + "err_generic_partial": "Non tutti gli elementi selezionati sono stati eliminati, ricarica la pagina", + "err_generic": "Impossibile eliminare gli elementi selezionati", + "err_403_partial": "$t(fs.delete_multi.err_generic_partial). $t(fs.err_403)", + "err_429_partial": "$t(fs.delete_multi.err_generic_partial). $t(fs.err_429)", + "err_403": "$t(fs.delete_multi.err_generic). $t(fs.err_403)", + "err_429": "$t(fs.delete_multi.err_generic). $t(fs.err_429)" + }, + "copy": { + "msg": "Copia", + "err_generic": "Errore copia file/cartelle", + "err_403": "$t(fs.copy.err_generic). $t(fs.err_403)", + "err_429": "$t(fs.copy.err_generic). $t(fs.err_429)" + }, + "move": { + "msg": "Sposta", + "err_generic": "Errore nello spostamento di file/directory", + "err_403": "$t(fs.move.err_generic). $t(fs.err_403)", + "err_429": "$t(fs.move.err_generic). $t(fs.err_429)" + }, + "rename": { + "new_name": "Nuovo nome", + "err_generic": "Impossibile rinominare \"{{- name}}\"", + "err_403": "$t(fs.rename.err_generic): $t(fs.err_403)", + "err_429": "$t(fs.rename.err_generic): $t(fs.err_429)" + }, + "upload": { + "text": "Carica file", + "success": "File caricati correttamente", + "message": "Rilascia i file qui o fai clic per caricarli.", + "err_generic": "Errore caricamento file", + "err_403": "$t(fs.upload.err_generic). $t(fs.err_403)", + "err_429": "$t(fs.upload.err_generic). $t(fs.err_429)" + }, + "quota_usage": { + "title": "Utilizzo quota", + "disk": "Quota disco", + "size": "Dimensione: {{- val}}", + "size_percentage": "Dimensione: {{- val}} ({{percentage}}%)", + "files": "File: {{- val}}", + "files_percentage": "File: {{- val}} ({{percentage}}%)", + "transfer": "Quota trasferimenti", + "total": "Totale: {{- val}}", + "total_percentage": "Totale: {{- val}} ({{percentage}}%)", + "uploads": "Caricamenti: {{- val}}", + "uploads_percentage": "Caricamenti: {{- val}} ({{percentage}}%)", + "downloads": "Download: {{- val}}", + "downloads_percentage": "Download: {{- val}} ({{percentage}}%)" + } + }, + "datatable": { + "info": "Risultati da _START_ a _END_ di _TOTAL_ elementi", + "info_empty": "Nessun risultato", + "info_filtered": "(filtrati da _MAX_ elementi totali)", + "processing": "Elaborazione..." + }, + "editor": { + "keybinding": "Combinazioni di tasti dell'editor", + "search": "Apre pannello di ricerca", + "goto": "Vai a linea", + "indent_more": "Aumenta indentazione", + "indent_less": "Diminuisci indentazione" + }, + "2fa": { + "title": "Autenticazione a due fattori utilizzando App di autenticazione", + "msg_enabled": "L'autenticazione a due fattori è abilitata", + "msg_disabled": "Metti il tuo account in sicurezza", + "msg_info": "L'autenticazione a due fattori aggiunge un ulteriore livello di sicurezza al tuo account. Per accedere dovrai fornire un ulteriore codice di autenticazione.", + "require_for": "Richiedi 2FA per", + "generate": "Genera nuovo segreto", + "recovery_codes": "Codici di ripristino", + "new_recovery_codes": "Nuovi codici di ripristino", + "recovery_codes_msg1": "I codici di ripristino sono un insieme di codici monouso che possono essere utilizzati al posto del codice di autenticazione per accedere all'interfaccia utente Web. Puoi utilizzarli se perdi l'accesso al tuo telefono per accedere al tuo account e disabilitare o rigenerare la configurazione dell'autenticazione a due fattori.", + "recovery_codes_msg2": "Per mantenere sicuro il tuo account, non condividere o distribuire i tuoi codici di ripristino. Ti consigliamo di salvarli con un gestore di password sicuro.", + "recovery_codes_msg3": "Se generi nuovi codici di ripristino, invalidi automaticamente quelli vecchi.", + "info_title": "Ulteriori informazioni sull'autenticazione a due fattori", + "info1": "Il protocollo SSH (comandi SFTP/SCP/SSH) richiederà il passcode se il client utilizza l'autenticazione interattiva da tastiera.", + "info2": "Per protocollo HTTP si intende l'interfaccia utente Web e le API REST. L'interfaccia utente Web richiederà il passcode utilizzando una pagina specifica. Per l'API REST devi aggiungere il passcode utilizzando un'intestazione HTTP.", + "info3": "FTP non ha un modo standard per supportare l'autenticazione a due fattori, se abiliti il supporto FTP, devi aggiungere il passcode TOTP dopo la password. Ad esempio, se la tua password è \"password\" e il tuo passcode monouso è \"123456\", devi utilizzare \"password123456\" come password.", + "info4": "WebDAV non è supportato poiché ogni singola richiesta deve essere autenticata e un passcode non può essere riutilizzato.", + "setup_title": "Configura l'autenticazione a due fattori", + "setup_msg": "Utilizza la tua app di autenticazione preferita (ad esempio Microsoft Authenticator, Google Authenticator, Authy, 1Password ecc.) per scansionare il codice QR. Verrà generato un codice di autenticazione da inserire di seguito.", + "setup_help": "Se hai problemi con l'utilizzo del codice QR, seleziona l'inserimento manuale sulla tua app e inserisci il codice:", + "disable_question": "Vuoi disattivare l'autenticazione a due fattori?", + "generate_question": "Vuoi generare un nuovo segreto e invalidare quello precedente? Qualsiasi app di autenticazione registrata smetterà di funzionare", + "disabled": "L'autenticazione a due fattori è disabilitata", + "recovery_codes_gen_err": "Impossibile generare nuovi codici di ripristino", + "recovery_codes_get_err": "Impossibile ottenere i codici di ripristino", + "auth_code_invalid": "Impossibile convalidare il codice di autenticazione fornito", + "auth_secret_gen_err": "Impossibile generare il segreto di autenticazione", + "save_err": "Impossibile salvare la configurazione", + "save_err_proto": "$t(2fa.save_err). Assicurati che i protocolli abilitati siano conformi alla politica aziendale", + "auth_code_required": "Il codice di autenticazione è obbligatorio", + "no_protocol": "Seleziona almeno un protocollo" + }, + "share": { + "scope": "Ambito", + "scope_read": "Lettura", + "scope_write": "Scrittura", + "scope_read_write": "Lettura/Scrittura", + "scope_help": "Per gli ambiti \"Scrittura\" e \"Lettura/Scrittura\" devi definire un singolo percorso e deve essere una cartella", + "paths": "Percorsi", + "path_help": "percorso di un file o di una directory, ad esempio /dir o /dir/file.txt", + "password_help": "Se impostata, la condivisione sarà protetta da password", + "expiration": "Scadenza", + "expiration_help": "Scegli una data di scadenza", + "max_tokens": "Token massimi", + "max_tokens_help": "Numero massimo di volte in cui è possibile accedere a questa condivisione. 0 significa nessun limite", + "view_manage": "Visualizza e gestisci condivisioni", + "no_share": "Nessuna condivisione", + "password_protected": "Protetto da password.", + "expiration_date": "Scadenza: {{- val, datetime}}. ", + "last_use": "Ultimo utilizzo: {{- val, datetime}}. ", + "usage": "Utilizzo: {{used}}/{{total}}. ", + "used_tokens": "Token usati: {{used}}. ", + "scope_invalid": "Ambito non valido", + "max_tokens_invalid": "Token massimi non validi", + "expiration_invalid": "Scadenza non valida", + "err_no_password": "Non sei autorizzato a condividere file/cartelle senza password", + "expiration_out_of_range": "Imposta una data di scadenza e assicurati che sia conforme alla politica aziendale, ad es. non è troppo lontan nel futuro", + "generic": "Errore imprevisto durante il salvataggio della condivisione", + "path_required": "È necessario almeno un percorso", + "path_write_scope": "L'ambito di scrittura richiede esattamente un percorso", + "nested_paths": "I percorsi non possono essere contenuti l'uno dentro l'altro", + "expiration_past": "La data di scadenza deve essere futura", + "usage_exceed": "Utilizzo massimo della condivisione superato", + "expired": "La condivisione è scaduta", + "browsable_multiple_paths": "Una condivisione con più percorsi non è navigabile", + "browsable_non_dir": "La condivisione non è una directory quindi non è navigabile" + }, + "select2": { + "no_results": "Nessun risultato trovato", + "searching": "Ricerca...", + "removeall": "Rimuovi tutti gli elementi", + "remove": "Rimuovi elemento" + }, + "change_pwd": { + "info": "Inserisci la password attuale, per ragioni di sicurezza, e poi la nuova password due volte, per verificare di averla scritta correttamente. Verrai disconnesso dopo aver modificato la tua password", + "current": "Password attuale", + "new": "Nuova password", + "confirm": "Conferma nuova password", + "save": "Modifica la mia password", + "required_fields": "Si prega di fornire la password attuale e quella nuova due volte", + "no_match": "I due campi della password non corrispondono", + "no_different": "La nuova password deve essere diversa da quella attuale", + "current_no_match": "La password attuale non corrisponde", + "generic": "Errore imprevisto durante la modifica della password", + "required": "È richiesta la modifica della password. Imposta una nuova password per continuare a utilizzare il tuo account" + }, + "user": { + "username_reserved": "Il nome utente specificato è riservato", + "username_invalid": "Il nome utente specificato non è valido, sono consentiti i seguenti caratteri: a-zA-Z0-9-_.~", + "home_required": "La directory principale è obbligatoria", + "home_invalid": "La directory principale deve essere un path assoluto", + "pub_key_invalid": "Chiave pubblica non valida", + "err_primary_group": "È consentito un solo gruppo primario", + "err_duplicate_group": "Rilevati gruppi duplicati", + "no_permissions": "I permessi per le directory sono obbligatori", + "no_root_permissions": "I permessi per la directory principale sono obbligatori", + "err_permissions_generic": "Permessi non validi: assicurati di utilizzare percorsi assoluti validi", + "2fa_invalid": "Configurazione non valida per l'autenticazione a due fattori", + "recovery_codes_invalid": "Codici di ripristino non validi", + "folde_path_required": "Il percorso di montaggio delle cartelle virtuali è obbligatorio", + "folder_duplicated": "Rilevate cartelle virtuali duplicate", + "folder_overlapped": "Rilevate cartelle virtuali sovrapposte", + "folder_quota_size_invalid": "La quota come dimensione delle cartelle virtuali deve essere maggiore o uguale a -1", + "folder_quota_file_invalid": "La quota come numero di file delle cartelle virtuali deve essere maggiore o uguale a -1", + "folder_quota_invalid": "Le quote come dimensione e numero di file delle cartelle virtuali devono essere entrambe -1 o maggiore o uguale di zero", + "ip_filters_invalid": "Filtri IP non validi, assicurati che rispettino la notazione CIDR, ad esempio 192.168.1.0/24", + "src_bw_limits_invalid": "Limiti di velocità della larghezza di banda per origine non validi", + "share_expiration_invalid": "La scadenza per le condivisioni deve essere maggiore o uguale al valore di default definito", + "file_pattern_path_invalid": "I percorsi dei filtri sui modelli di nome file devono essere assoluti", + "file_pattern_duplicated": "Rilevati filtri su modelli di nome file duplicati", + "file_pattern_invalid": "Filtri su modelli di nome file non validi", + "disable_active_2fa": "L'autenticazione a due fattori non può essere disabilitata per un utente con una configurazione attiva", + "pwd_change_conflict": "Non è possibile richiedere la modifica della password e allo stesso tempo impedire la modifica della password" + } +} \ No newline at end of file diff --git a/static/vendor/i18next/i18next.js b/static/vendor/i18next/i18next.js new file mode 100644 index 00000000..47c7bfae --- /dev/null +++ b/static/vendor/i18next/i18next.js @@ -0,0 +1,2297 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.i18next = factory()); +})(this, (function () { 'use strict'; + + const consoleLogger = { + type: 'logger', + log(args) { + this.output('log', args); + }, + warn(args) { + this.output('warn', args); + }, + error(args) { + this.output('error', args); + }, + output(type, args) { + if (console && console[type]) console[type].apply(console, args); + } + }; + class Logger { + constructor(concreteLogger) { + let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + this.init(concreteLogger, options); + } + init(concreteLogger) { + let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + this.prefix = options.prefix || 'i18next:'; + this.logger = concreteLogger || consoleLogger; + this.options = options; + this.debug = options.debug; + } + log() { + for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { + args[_key] = arguments[_key]; + } + return this.forward(args, 'log', '', true); + } + warn() { + for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { + args[_key2] = arguments[_key2]; + } + return this.forward(args, 'warn', '', true); + } + error() { + for (var _len3 = arguments.length, args = new Array(_len3), _key3 = 0; _key3 < _len3; _key3++) { + args[_key3] = arguments[_key3]; + } + return this.forward(args, 'error', ''); + } + deprecate() { + for (var _len4 = arguments.length, args = new Array(_len4), _key4 = 0; _key4 < _len4; _key4++) { + args[_key4] = arguments[_key4]; + } + return this.forward(args, 'warn', 'WARNING DEPRECATED: ', true); + } + forward(args, lvl, prefix, debugOnly) { + if (debugOnly && !this.debug) return null; + if (typeof args[0] === 'string') args[0] = `${prefix}${this.prefix} ${args[0]}`; + return this.logger[lvl](args); + } + create(moduleName) { + return new Logger(this.logger, { + ...{ + prefix: `${this.prefix}:${moduleName}:` + }, + ...this.options + }); + } + clone(options) { + options = options || this.options; + options.prefix = options.prefix || this.prefix; + return new Logger(this.logger, options); + } + } + var baseLogger = new Logger(); + + class EventEmitter { + constructor() { + this.observers = {}; + } + on(events, listener) { + events.split(' ').forEach(event => { + this.observers[event] = this.observers[event] || []; + this.observers[event].push(listener); + }); + return this; + } + off(event, listener) { + if (!this.observers[event]) return; + if (!listener) { + delete this.observers[event]; + return; + } + this.observers[event] = this.observers[event].filter(l => l !== listener); + } + emit(event) { + for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { + args[_key - 1] = arguments[_key]; + } + if (this.observers[event]) { + const cloned = [].concat(this.observers[event]); + cloned.forEach(observer => { + observer(...args); + }); + } + if (this.observers['*']) { + const cloned = [].concat(this.observers['*']); + cloned.forEach(observer => { + observer.apply(observer, [event, ...args]); + }); + } + } + } + + function defer() { + let res; + let rej; + const promise = new Promise((resolve, reject) => { + res = resolve; + rej = reject; + }); + promise.resolve = res; + promise.reject = rej; + return promise; + } + function makeString(object) { + if (object == null) return ''; + return '' + object; + } + function copy(a, s, t) { + a.forEach(m => { + if (s[m]) t[m] = s[m]; + }); + } + function getLastOfPath(object, path, Empty) { + function cleanKey(key) { + return key && key.indexOf('###') > -1 ? key.replace(/###/g, '.') : key; + } + function canNotTraverseDeeper() { + return !object || typeof object === 'string'; + } + const stack = typeof path !== 'string' ? [].concat(path) : path.split('.'); + while (stack.length > 1) { + if (canNotTraverseDeeper()) return {}; + const key = cleanKey(stack.shift()); + if (!object[key] && Empty) object[key] = new Empty(); + if (Object.prototype.hasOwnProperty.call(object, key)) { + object = object[key]; + } else { + object = {}; + } + } + if (canNotTraverseDeeper()) return {}; + return { + obj: object, + k: cleanKey(stack.shift()) + }; + } + function setPath(object, path, newValue) { + const { + obj, + k + } = getLastOfPath(object, path, Object); + obj[k] = newValue; + } + function pushPath(object, path, newValue, concat) { + const { + obj, + k + } = getLastOfPath(object, path, Object); + obj[k] = obj[k] || []; + if (concat) obj[k] = obj[k].concat(newValue); + if (!concat) obj[k].push(newValue); + } + function getPath(object, path) { + const { + obj, + k + } = getLastOfPath(object, path); + if (!obj) return undefined; + return obj[k]; + } + function getPathWithDefaults(data, defaultData, key) { + const value = getPath(data, key); + if (value !== undefined) { + return value; + } + return getPath(defaultData, key); + } + function deepExtend(target, source, overwrite) { + for (const prop in source) { + if (prop !== '__proto__' && prop !== 'constructor') { + if (prop in target) { + if (typeof target[prop] === 'string' || target[prop] instanceof String || typeof source[prop] === 'string' || source[prop] instanceof String) { + if (overwrite) target[prop] = source[prop]; + } else { + deepExtend(target[prop], source[prop], overwrite); + } + } else { + target[prop] = source[prop]; + } + } + } + return target; + } + function regexEscape(str) { + return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'); + } + var _entityMap = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + '/': '/' + }; + function escape(data) { + if (typeof data === 'string') { + return data.replace(/[&<>"'\/]/g, s => _entityMap[s]); + } + return data; + } + const chars = [' ', ',', '?', '!', ';']; + function looksLikeObjectPath(key, nsSeparator, keySeparator) { + nsSeparator = nsSeparator || ''; + keySeparator = keySeparator || ''; + const possibleChars = chars.filter(c => nsSeparator.indexOf(c) < 0 && keySeparator.indexOf(c) < 0); + if (possibleChars.length === 0) return true; + const r = new RegExp(`(${possibleChars.map(c => c === '?' ? '\\?' : c).join('|')})`); + let matched = !r.test(key); + if (!matched) { + const ki = key.indexOf(keySeparator); + if (ki > 0 && !r.test(key.substring(0, ki))) { + matched = true; + } + } + return matched; + } + function deepFind(obj, path) { + let keySeparator = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : '.'; + if (!obj) return undefined; + if (obj[path]) return obj[path]; + const paths = path.split(keySeparator); + let current = obj; + for (let i = 0; i < paths.length; ++i) { + if (!current) return undefined; + if (typeof current[paths[i]] === 'string' && i + 1 < paths.length) { + return undefined; + } + if (current[paths[i]] === undefined) { + let j = 2; + let p = paths.slice(i, i + j).join(keySeparator); + let mix = current[p]; + while (mix === undefined && paths.length > i + j) { + j++; + p = paths.slice(i, i + j).join(keySeparator); + mix = current[p]; + } + if (mix === undefined) return undefined; + if (mix === null) return null; + if (path.endsWith(p)) { + if (typeof mix === 'string') return mix; + if (p && typeof mix[p] === 'string') return mix[p]; + } + const joinedPath = paths.slice(i + j).join(keySeparator); + if (joinedPath) return deepFind(mix, joinedPath, keySeparator); + return undefined; + } + current = current[paths[i]]; + } + return current; + } + function getCleanedCode(code) { + if (code && code.indexOf('_') > 0) return code.replace('_', '-'); + return code; + } + + class ResourceStore extends EventEmitter { + constructor(data) { + let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : { + ns: ['translation'], + defaultNS: 'translation' + }; + super(); + this.data = data || {}; + this.options = options; + if (this.options.keySeparator === undefined) { + this.options.keySeparator = '.'; + } + if (this.options.ignoreJSONStructure === undefined) { + this.options.ignoreJSONStructure = true; + } + } + addNamespaces(ns) { + if (this.options.ns.indexOf(ns) < 0) { + this.options.ns.push(ns); + } + } + removeNamespaces(ns) { + const index = this.options.ns.indexOf(ns); + if (index > -1) { + this.options.ns.splice(index, 1); + } + } + getResource(lng, ns, key) { + let options = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {}; + const keySeparator = options.keySeparator !== undefined ? options.keySeparator : this.options.keySeparator; + const ignoreJSONStructure = options.ignoreJSONStructure !== undefined ? options.ignoreJSONStructure : this.options.ignoreJSONStructure; + let path = [lng, ns]; + if (key && typeof key !== 'string') path = path.concat(key); + if (key && typeof key === 'string') path = path.concat(keySeparator ? key.split(keySeparator) : key); + if (lng.indexOf('.') > -1) { + path = lng.split('.'); + } + const result = getPath(this.data, path); + if (result || !ignoreJSONStructure || typeof key !== 'string') return result; + return deepFind(this.data && this.data[lng] && this.data[lng][ns], key, keySeparator); + } + addResource(lng, ns, key, value) { + let options = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : { + silent: false + }; + const keySeparator = options.keySeparator !== undefined ? options.keySeparator : this.options.keySeparator; + let path = [lng, ns]; + if (key) path = path.concat(keySeparator ? key.split(keySeparator) : key); + if (lng.indexOf('.') > -1) { + path = lng.split('.'); + value = ns; + ns = path[1]; + } + this.addNamespaces(ns); + setPath(this.data, path, value); + if (!options.silent) this.emit('added', lng, ns, key, value); + } + addResources(lng, ns, resources) { + let options = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : { + silent: false + }; + for (const m in resources) { + if (typeof resources[m] === 'string' || Object.prototype.toString.apply(resources[m]) === '[object Array]') this.addResource(lng, ns, m, resources[m], { + silent: true + }); + } + if (!options.silent) this.emit('added', lng, ns, resources); + } + addResourceBundle(lng, ns, resources, deep, overwrite) { + let options = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : { + silent: false + }; + let path = [lng, ns]; + if (lng.indexOf('.') > -1) { + path = lng.split('.'); + deep = resources; + resources = ns; + ns = path[1]; + } + this.addNamespaces(ns); + let pack = getPath(this.data, path) || {}; + if (deep) { + deepExtend(pack, resources, overwrite); + } else { + pack = { + ...pack, + ...resources + }; + } + setPath(this.data, path, pack); + if (!options.silent) this.emit('added', lng, ns, resources); + } + removeResourceBundle(lng, ns) { + if (this.hasResourceBundle(lng, ns)) { + delete this.data[lng][ns]; + } + this.removeNamespaces(ns); + this.emit('removed', lng, ns); + } + hasResourceBundle(lng, ns) { + return this.getResource(lng, ns) !== undefined; + } + getResourceBundle(lng, ns) { + if (!ns) ns = this.options.defaultNS; + if (this.options.compatibilityAPI === 'v1') return { + ...{}, + ...this.getResource(lng, ns) + }; + return this.getResource(lng, ns); + } + getDataByLanguage(lng) { + return this.data[lng]; + } + hasLanguageSomeTranslations(lng) { + const data = this.getDataByLanguage(lng); + const n = data && Object.keys(data) || []; + return !!n.find(v => data[v] && Object.keys(data[v]).length > 0); + } + toJSON() { + return this.data; + } + } + + var postProcessor = { + processors: {}, + addPostProcessor(module) { + this.processors[module.name] = module; + }, + handle(processors, value, key, options, translator) { + processors.forEach(processor => { + if (this.processors[processor]) value = this.processors[processor].process(value, key, options, translator); + }); + return value; + } + }; + + const checkedLoadedFor = {}; + class Translator extends EventEmitter { + constructor(services) { + let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + super(); + copy(['resourceStore', 'languageUtils', 'pluralResolver', 'interpolator', 'backendConnector', 'i18nFormat', 'utils'], services, this); + this.options = options; + if (this.options.keySeparator === undefined) { + this.options.keySeparator = '.'; + } + this.logger = baseLogger.create('translator'); + } + changeLanguage(lng) { + if (lng) this.language = lng; + } + exists(key) { + let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : { + interpolation: {} + }; + if (key === undefined || key === null) { + return false; + } + const resolved = this.resolve(key, options); + return resolved && resolved.res !== undefined; + } + extractFromKey(key, options) { + let nsSeparator = options.nsSeparator !== undefined ? options.nsSeparator : this.options.nsSeparator; + if (nsSeparator === undefined) nsSeparator = ':'; + const keySeparator = options.keySeparator !== undefined ? options.keySeparator : this.options.keySeparator; + let namespaces = options.ns || this.options.defaultNS || []; + const wouldCheckForNsInKey = nsSeparator && key.indexOf(nsSeparator) > -1; + const seemsNaturalLanguage = !this.options.userDefinedKeySeparator && !options.keySeparator && !this.options.userDefinedNsSeparator && !options.nsSeparator && !looksLikeObjectPath(key, nsSeparator, keySeparator); + if (wouldCheckForNsInKey && !seemsNaturalLanguage) { + const m = key.match(this.interpolator.nestingRegexp); + if (m && m.length > 0) { + return { + key, + namespaces + }; + } + const parts = key.split(nsSeparator); + if (nsSeparator !== keySeparator || nsSeparator === keySeparator && this.options.ns.indexOf(parts[0]) > -1) namespaces = parts.shift(); + key = parts.join(keySeparator); + } + if (typeof namespaces === 'string') namespaces = [namespaces]; + return { + key, + namespaces + }; + } + translate(keys, options, lastKey) { + if (typeof options !== 'object' && this.options.overloadTranslationOptionHandler) { + options = this.options.overloadTranslationOptionHandler(arguments); + } + if (typeof options === 'object') options = { + ...options + }; + if (!options) options = {}; + if (keys === undefined || keys === null) return ''; + if (!Array.isArray(keys)) keys = [String(keys)]; + const returnDetails = options.returnDetails !== undefined ? options.returnDetails : this.options.returnDetails; + const keySeparator = options.keySeparator !== undefined ? options.keySeparator : this.options.keySeparator; + const { + key, + namespaces + } = this.extractFromKey(keys[keys.length - 1], options); + const namespace = namespaces[namespaces.length - 1]; + const lng = options.lng || this.language; + const appendNamespaceToCIMode = options.appendNamespaceToCIMode || this.options.appendNamespaceToCIMode; + if (lng && lng.toLowerCase() === 'cimode') { + if (appendNamespaceToCIMode) { + const nsSeparator = options.nsSeparator || this.options.nsSeparator; + if (returnDetails) { + return { + res: `${namespace}${nsSeparator}${key}`, + usedKey: key, + exactUsedKey: key, + usedLng: lng, + usedNS: namespace, + usedParams: this.getUsedParamsDetails(options) + }; + } + return `${namespace}${nsSeparator}${key}`; + } + if (returnDetails) { + return { + res: key, + usedKey: key, + exactUsedKey: key, + usedLng: lng, + usedNS: namespace, + usedParams: this.getUsedParamsDetails(options) + }; + } + return key; + } + const resolved = this.resolve(keys, options); + let res = resolved && resolved.res; + const resUsedKey = resolved && resolved.usedKey || key; + const resExactUsedKey = resolved && resolved.exactUsedKey || key; + const resType = Object.prototype.toString.apply(res); + const noObject = ['[object Number]', '[object Function]', '[object RegExp]']; + const joinArrays = options.joinArrays !== undefined ? options.joinArrays : this.options.joinArrays; + const handleAsObjectInI18nFormat = !this.i18nFormat || this.i18nFormat.handleAsObject; + const handleAsObject = typeof res !== 'string' && typeof res !== 'boolean' && typeof res !== 'number'; + if (handleAsObjectInI18nFormat && res && handleAsObject && noObject.indexOf(resType) < 0 && !(typeof joinArrays === 'string' && resType === '[object Array]')) { + if (!options.returnObjects && !this.options.returnObjects) { + if (!this.options.returnedObjectHandler) { + this.logger.warn('accessing an object - but returnObjects options is not enabled!'); + } + const r = this.options.returnedObjectHandler ? this.options.returnedObjectHandler(resUsedKey, res, { + ...options, + ns: namespaces + }) : `key '${key} (${this.language})' returned an object instead of string.`; + if (returnDetails) { + resolved.res = r; + resolved.usedParams = this.getUsedParamsDetails(options); + return resolved; + } + return r; + } + if (keySeparator) { + const resTypeIsArray = resType === '[object Array]'; + const copy = resTypeIsArray ? [] : {}; + const newKeyToUse = resTypeIsArray ? resExactUsedKey : resUsedKey; + for (const m in res) { + if (Object.prototype.hasOwnProperty.call(res, m)) { + const deepKey = `${newKeyToUse}${keySeparator}${m}`; + copy[m] = this.translate(deepKey, { + ...options, + ...{ + joinArrays: false, + ns: namespaces + } + }); + if (copy[m] === deepKey) copy[m] = res[m]; + } + } + res = copy; + } + } else if (handleAsObjectInI18nFormat && typeof joinArrays === 'string' && resType === '[object Array]') { + res = res.join(joinArrays); + if (res) res = this.extendTranslation(res, keys, options, lastKey); + } else { + let usedDefault = false; + let usedKey = false; + const needsPluralHandling = options.count !== undefined && typeof options.count !== 'string'; + const hasDefaultValue = Translator.hasDefaultValue(options); + const defaultValueSuffix = needsPluralHandling ? this.pluralResolver.getSuffix(lng, options.count, options) : ''; + const defaultValueSuffixOrdinalFallback = options.ordinal && needsPluralHandling ? this.pluralResolver.getSuffix(lng, options.count, { + ordinal: false + }) : ''; + const defaultValue = options[`defaultValue${defaultValueSuffix}`] || options[`defaultValue${defaultValueSuffixOrdinalFallback}`] || options.defaultValue; + if (!this.isValidLookup(res) && hasDefaultValue) { + usedDefault = true; + res = defaultValue; + } + if (!this.isValidLookup(res)) { + usedKey = true; + res = key; + } + const missingKeyNoValueFallbackToKey = options.missingKeyNoValueFallbackToKey || this.options.missingKeyNoValueFallbackToKey; + const resForMissing = missingKeyNoValueFallbackToKey && usedKey ? undefined : res; + const updateMissing = hasDefaultValue && defaultValue !== res && this.options.updateMissing; + if (usedKey || usedDefault || updateMissing) { + this.logger.log(updateMissing ? 'updateKey' : 'missingKey', lng, namespace, key, updateMissing ? defaultValue : res); + if (keySeparator) { + const fk = this.resolve(key, { + ...options, + keySeparator: false + }); + if (fk && fk.res) this.logger.warn('Seems the loaded translations were in flat JSON format instead of nested. Either set keySeparator: false on init or make sure your translations are published in nested format.'); + } + let lngs = []; + const fallbackLngs = this.languageUtils.getFallbackCodes(this.options.fallbackLng, options.lng || this.language); + if (this.options.saveMissingTo === 'fallback' && fallbackLngs && fallbackLngs[0]) { + for (let i = 0; i < fallbackLngs.length; i++) { + lngs.push(fallbackLngs[i]); + } + } else if (this.options.saveMissingTo === 'all') { + lngs = this.languageUtils.toResolveHierarchy(options.lng || this.language); + } else { + lngs.push(options.lng || this.language); + } + const send = (l, k, specificDefaultValue) => { + const defaultForMissing = hasDefaultValue && specificDefaultValue !== res ? specificDefaultValue : resForMissing; + if (this.options.missingKeyHandler) { + this.options.missingKeyHandler(l, namespace, k, defaultForMissing, updateMissing, options); + } else if (this.backendConnector && this.backendConnector.saveMissing) { + this.backendConnector.saveMissing(l, namespace, k, defaultForMissing, updateMissing, options); + } + this.emit('missingKey', l, namespace, k, res); + }; + if (this.options.saveMissing) { + if (this.options.saveMissingPlurals && needsPluralHandling) { + lngs.forEach(language => { + this.pluralResolver.getSuffixes(language, options).forEach(suffix => { + send([language], key + suffix, options[`defaultValue${suffix}`] || defaultValue); + }); + }); + } else { + send(lngs, key, defaultValue); + } + } + } + res = this.extendTranslation(res, keys, options, resolved, lastKey); + if (usedKey && res === key && this.options.appendNamespaceToMissingKey) res = `${namespace}:${key}`; + if ((usedKey || usedDefault) && this.options.parseMissingKeyHandler) { + if (this.options.compatibilityAPI !== 'v1') { + res = this.options.parseMissingKeyHandler(this.options.appendNamespaceToMissingKey ? `${namespace}:${key}` : key, usedDefault ? res : undefined); + } else { + res = this.options.parseMissingKeyHandler(res); + } + } + } + if (returnDetails) { + resolved.res = res; + resolved.usedParams = this.getUsedParamsDetails(options); + return resolved; + } + return res; + } + extendTranslation(res, key, options, resolved, lastKey) { + var _this = this; + if (this.i18nFormat && this.i18nFormat.parse) { + res = this.i18nFormat.parse(res, { + ...this.options.interpolation.defaultVariables, + ...options + }, options.lng || this.language || resolved.usedLng, resolved.usedNS, resolved.usedKey, { + resolved + }); + } else if (!options.skipInterpolation) { + if (options.interpolation) this.interpolator.init({ + ...options, + ...{ + interpolation: { + ...this.options.interpolation, + ...options.interpolation + } + } + }); + const skipOnVariables = typeof res === 'string' && (options && options.interpolation && options.interpolation.skipOnVariables !== undefined ? options.interpolation.skipOnVariables : this.options.interpolation.skipOnVariables); + let nestBef; + if (skipOnVariables) { + const nb = res.match(this.interpolator.nestingRegexp); + nestBef = nb && nb.length; + } + let data = options.replace && typeof options.replace !== 'string' ? options.replace : options; + if (this.options.interpolation.defaultVariables) data = { + ...this.options.interpolation.defaultVariables, + ...data + }; + res = this.interpolator.interpolate(res, data, options.lng || this.language, options); + if (skipOnVariables) { + const na = res.match(this.interpolator.nestingRegexp); + const nestAft = na && na.length; + if (nestBef < nestAft) options.nest = false; + } + if (!options.lng && this.options.compatibilityAPI !== 'v1' && resolved && resolved.res) options.lng = resolved.usedLng; + if (options.nest !== false) res = this.interpolator.nest(res, function () { + for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { + args[_key] = arguments[_key]; + } + if (lastKey && lastKey[0] === args[0] && !options.context) { + _this.logger.warn(`It seems you are nesting recursively key: ${args[0]} in key: ${key[0]}`); + return null; + } + return _this.translate(...args, key); + }, options); + if (options.interpolation) this.interpolator.reset(); + } + const postProcess = options.postProcess || this.options.postProcess; + const postProcessorNames = typeof postProcess === 'string' ? [postProcess] : postProcess; + if (res !== undefined && res !== null && postProcessorNames && postProcessorNames.length && options.applyPostProcessor !== false) { + res = postProcessor.handle(postProcessorNames, res, key, this.options && this.options.postProcessPassResolved ? { + i18nResolved: { + ...resolved, + usedParams: this.getUsedParamsDetails(options) + }, + ...options + } : options, this); + } + return res; + } + resolve(keys) { + let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + let found; + let usedKey; + let exactUsedKey; + let usedLng; + let usedNS; + if (typeof keys === 'string') keys = [keys]; + keys.forEach(k => { + if (this.isValidLookup(found)) return; + const extracted = this.extractFromKey(k, options); + const key = extracted.key; + usedKey = key; + let namespaces = extracted.namespaces; + if (this.options.fallbackNS) namespaces = namespaces.concat(this.options.fallbackNS); + const needsPluralHandling = options.count !== undefined && typeof options.count !== 'string'; + const needsZeroSuffixLookup = needsPluralHandling && !options.ordinal && options.count === 0 && this.pluralResolver.shouldUseIntlApi(); + const needsContextHandling = options.context !== undefined && (typeof options.context === 'string' || typeof options.context === 'number') && options.context !== ''; + const codes = options.lngs ? options.lngs : this.languageUtils.toResolveHierarchy(options.lng || this.language, options.fallbackLng); + namespaces.forEach(ns => { + if (this.isValidLookup(found)) return; + usedNS = ns; + if (!checkedLoadedFor[`${codes[0]}-${ns}`] && this.utils && this.utils.hasLoadedNamespace && !this.utils.hasLoadedNamespace(usedNS)) { + checkedLoadedFor[`${codes[0]}-${ns}`] = true; + this.logger.warn(`key "${usedKey}" for languages "${codes.join(', ')}" won't get resolved as namespace "${usedNS}" was not yet loaded`, 'This means something IS WRONG in your setup. You access the t function before i18next.init / i18next.loadNamespace / i18next.changeLanguage was done. Wait for the callback or Promise to resolve before accessing it!!!'); + } + codes.forEach(code => { + if (this.isValidLookup(found)) return; + usedLng = code; + const finalKeys = [key]; + if (this.i18nFormat && this.i18nFormat.addLookupKeys) { + this.i18nFormat.addLookupKeys(finalKeys, key, code, ns, options); + } else { + let pluralSuffix; + if (needsPluralHandling) pluralSuffix = this.pluralResolver.getSuffix(code, options.count, options); + const zeroSuffix = `${this.options.pluralSeparator}zero`; + const ordinalPrefix = `${this.options.pluralSeparator}ordinal${this.options.pluralSeparator}`; + if (needsPluralHandling) { + finalKeys.push(key + pluralSuffix); + if (options.ordinal && pluralSuffix.indexOf(ordinalPrefix) === 0) { + finalKeys.push(key + pluralSuffix.replace(ordinalPrefix, this.options.pluralSeparator)); + } + if (needsZeroSuffixLookup) { + finalKeys.push(key + zeroSuffix); + } + } + if (needsContextHandling) { + const contextKey = `${key}${this.options.contextSeparator}${options.context}`; + finalKeys.push(contextKey); + if (needsPluralHandling) { + finalKeys.push(contextKey + pluralSuffix); + if (options.ordinal && pluralSuffix.indexOf(ordinalPrefix) === 0) { + finalKeys.push(contextKey + pluralSuffix.replace(ordinalPrefix, this.options.pluralSeparator)); + } + if (needsZeroSuffixLookup) { + finalKeys.push(contextKey + zeroSuffix); + } + } + } + } + let possibleKey; + while (possibleKey = finalKeys.pop()) { + if (!this.isValidLookup(found)) { + exactUsedKey = possibleKey; + found = this.getResource(code, ns, possibleKey, options); + } + } + }); + }); + }); + return { + res: found, + usedKey, + exactUsedKey, + usedLng, + usedNS + }; + } + isValidLookup(res) { + return res !== undefined && !(!this.options.returnNull && res === null) && !(!this.options.returnEmptyString && res === ''); + } + getResource(code, ns, key) { + let options = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {}; + if (this.i18nFormat && this.i18nFormat.getResource) return this.i18nFormat.getResource(code, ns, key, options); + return this.resourceStore.getResource(code, ns, key, options); + } + getUsedParamsDetails() { + let options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + const optionsKeys = ['defaultValue', 'ordinal', 'context', 'replace', 'lng', 'lngs', 'fallbackLng', 'ns', 'keySeparator', 'nsSeparator', 'returnObjects', 'returnDetails', 'joinArrays', 'postProcess', 'interpolation']; + const useOptionsReplaceForData = options.replace && typeof options.replace !== 'string'; + let data = useOptionsReplaceForData ? options.replace : options; + if (useOptionsReplaceForData && typeof options.count !== 'undefined') { + data.count = options.count; + } + if (this.options.interpolation.defaultVariables) { + data = { + ...this.options.interpolation.defaultVariables, + ...data + }; + } + if (!useOptionsReplaceForData) { + data = { + ...data + }; + for (const key of optionsKeys) { + delete data[key]; + } + } + return data; + } + static hasDefaultValue(options) { + const prefix = 'defaultValue'; + for (const option in options) { + if (Object.prototype.hasOwnProperty.call(options, option) && prefix === option.substring(0, prefix.length) && undefined !== options[option]) { + return true; + } + } + return false; + } + } + + function capitalize(string) { + return string.charAt(0).toUpperCase() + string.slice(1); + } + class LanguageUtil { + constructor(options) { + this.options = options; + this.supportedLngs = this.options.supportedLngs || false; + this.logger = baseLogger.create('languageUtils'); + } + getScriptPartFromCode(code) { + code = getCleanedCode(code); + if (!code || code.indexOf('-') < 0) return null; + const p = code.split('-'); + if (p.length === 2) return null; + p.pop(); + if (p[p.length - 1].toLowerCase() === 'x') return null; + return this.formatLanguageCode(p.join('-')); + } + getLanguagePartFromCode(code) { + code = getCleanedCode(code); + if (!code || code.indexOf('-') < 0) return code; + const p = code.split('-'); + return this.formatLanguageCode(p[0]); + } + formatLanguageCode(code) { + if (typeof code === 'string' && code.indexOf('-') > -1) { + const specialCases = ['hans', 'hant', 'latn', 'cyrl', 'cans', 'mong', 'arab']; + let p = code.split('-'); + if (this.options.lowerCaseLng) { + p = p.map(part => part.toLowerCase()); + } else if (p.length === 2) { + p[0] = p[0].toLowerCase(); + p[1] = p[1].toUpperCase(); + if (specialCases.indexOf(p[1].toLowerCase()) > -1) p[1] = capitalize(p[1].toLowerCase()); + } else if (p.length === 3) { + p[0] = p[0].toLowerCase(); + if (p[1].length === 2) p[1] = p[1].toUpperCase(); + if (p[0] !== 'sgn' && p[2].length === 2) p[2] = p[2].toUpperCase(); + if (specialCases.indexOf(p[1].toLowerCase()) > -1) p[1] = capitalize(p[1].toLowerCase()); + if (specialCases.indexOf(p[2].toLowerCase()) > -1) p[2] = capitalize(p[2].toLowerCase()); + } + return p.join('-'); + } + return this.options.cleanCode || this.options.lowerCaseLng ? code.toLowerCase() : code; + } + isSupportedCode(code) { + if (this.options.load === 'languageOnly' || this.options.nonExplicitSupportedLngs) { + code = this.getLanguagePartFromCode(code); + } + return !this.supportedLngs || !this.supportedLngs.length || this.supportedLngs.indexOf(code) > -1; + } + getBestMatchFromCodes(codes) { + if (!codes) return null; + let found; + codes.forEach(code => { + if (found) return; + const cleanedLng = this.formatLanguageCode(code); + if (!this.options.supportedLngs || this.isSupportedCode(cleanedLng)) found = cleanedLng; + }); + if (!found && this.options.supportedLngs) { + codes.forEach(code => { + if (found) return; + const lngOnly = this.getLanguagePartFromCode(code); + if (this.isSupportedCode(lngOnly)) return found = lngOnly; + found = this.options.supportedLngs.find(supportedLng => { + if (supportedLng === lngOnly) return supportedLng; + if (supportedLng.indexOf('-') < 0 && lngOnly.indexOf('-') < 0) return; + if (supportedLng.indexOf(lngOnly) === 0) return supportedLng; + }); + }); + } + if (!found) found = this.getFallbackCodes(this.options.fallbackLng)[0]; + return found; + } + getFallbackCodes(fallbacks, code) { + if (!fallbacks) return []; + if (typeof fallbacks === 'function') fallbacks = fallbacks(code); + if (typeof fallbacks === 'string') fallbacks = [fallbacks]; + if (Object.prototype.toString.apply(fallbacks) === '[object Array]') return fallbacks; + if (!code) return fallbacks.default || []; + let found = fallbacks[code]; + if (!found) found = fallbacks[this.getScriptPartFromCode(code)]; + if (!found) found = fallbacks[this.formatLanguageCode(code)]; + if (!found) found = fallbacks[this.getLanguagePartFromCode(code)]; + if (!found) found = fallbacks.default; + return found || []; + } + toResolveHierarchy(code, fallbackCode) { + const fallbackCodes = this.getFallbackCodes(fallbackCode || this.options.fallbackLng || [], code); + const codes = []; + const addCode = c => { + if (!c) return; + if (this.isSupportedCode(c)) { + codes.push(c); + } else { + this.logger.warn(`rejecting language code not found in supportedLngs: ${c}`); + } + }; + if (typeof code === 'string' && (code.indexOf('-') > -1 || code.indexOf('_') > -1)) { + if (this.options.load !== 'languageOnly') addCode(this.formatLanguageCode(code)); + if (this.options.load !== 'languageOnly' && this.options.load !== 'currentOnly') addCode(this.getScriptPartFromCode(code)); + if (this.options.load !== 'currentOnly') addCode(this.getLanguagePartFromCode(code)); + } else if (typeof code === 'string') { + addCode(this.formatLanguageCode(code)); + } + fallbackCodes.forEach(fc => { + if (codes.indexOf(fc) < 0) addCode(this.formatLanguageCode(fc)); + }); + return codes; + } + } + + let sets = [{ + lngs: ['ach', 'ak', 'am', 'arn', 'br', 'fil', 'gun', 'ln', 'mfe', 'mg', 'mi', 'oc', 'pt', 'pt-BR', 'tg', 'tl', 'ti', 'tr', 'uz', 'wa'], + nr: [1, 2], + fc: 1 + }, { + lngs: ['af', 'an', 'ast', 'az', 'bg', 'bn', 'ca', 'da', 'de', 'dev', 'el', 'en', 'eo', 'es', 'et', 'eu', 'fi', 'fo', 'fur', 'fy', 'gl', 'gu', 'ha', 'hi', 'hu', 'hy', 'ia', 'it', 'kk', 'kn', 'ku', 'lb', 'mai', 'ml', 'mn', 'mr', 'nah', 'nap', 'nb', 'ne', 'nl', 'nn', 'no', 'nso', 'pa', 'pap', 'pms', 'ps', 'pt-PT', 'rm', 'sco', 'se', 'si', 'so', 'son', 'sq', 'sv', 'sw', 'ta', 'te', 'tk', 'ur', 'yo'], + nr: [1, 2], + fc: 2 + }, { + lngs: ['ay', 'bo', 'cgg', 'fa', 'ht', 'id', 'ja', 'jbo', 'ka', 'km', 'ko', 'ky', 'lo', 'ms', 'sah', 'su', 'th', 'tt', 'ug', 'vi', 'wo', 'zh'], + nr: [1], + fc: 3 + }, { + lngs: ['be', 'bs', 'cnr', 'dz', 'hr', 'ru', 'sr', 'uk'], + nr: [1, 2, 5], + fc: 4 + }, { + lngs: ['ar'], + nr: [0, 1, 2, 3, 11, 100], + fc: 5 + }, { + lngs: ['cs', 'sk'], + nr: [1, 2, 5], + fc: 6 + }, { + lngs: ['csb', 'pl'], + nr: [1, 2, 5], + fc: 7 + }, { + lngs: ['cy'], + nr: [1, 2, 3, 8], + fc: 8 + }, { + lngs: ['fr'], + nr: [1, 2], + fc: 9 + }, { + lngs: ['ga'], + nr: [1, 2, 3, 7, 11], + fc: 10 + }, { + lngs: ['gd'], + nr: [1, 2, 3, 20], + fc: 11 + }, { + lngs: ['is'], + nr: [1, 2], + fc: 12 + }, { + lngs: ['jv'], + nr: [0, 1], + fc: 13 + }, { + lngs: ['kw'], + nr: [1, 2, 3, 4], + fc: 14 + }, { + lngs: ['lt'], + nr: [1, 2, 10], + fc: 15 + }, { + lngs: ['lv'], + nr: [1, 2, 0], + fc: 16 + }, { + lngs: ['mk'], + nr: [1, 2], + fc: 17 + }, { + lngs: ['mnk'], + nr: [0, 1, 2], + fc: 18 + }, { + lngs: ['mt'], + nr: [1, 2, 11, 20], + fc: 19 + }, { + lngs: ['or'], + nr: [2, 1], + fc: 2 + }, { + lngs: ['ro'], + nr: [1, 2, 20], + fc: 20 + }, { + lngs: ['sl'], + nr: [5, 1, 2, 3], + fc: 21 + }, { + lngs: ['he', 'iw'], + nr: [1, 2, 20, 21], + fc: 22 + }]; + let _rulesPluralsTypes = { + 1: function (n) { + return Number(n > 1); + }, + 2: function (n) { + return Number(n != 1); + }, + 3: function (n) { + return 0; + }, + 4: function (n) { + return Number(n % 10 == 1 && n % 100 != 11 ? 0 : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2); + }, + 5: function (n) { + return Number(n == 0 ? 0 : n == 1 ? 1 : n == 2 ? 2 : n % 100 >= 3 && n % 100 <= 10 ? 3 : n % 100 >= 11 ? 4 : 5); + }, + 6: function (n) { + return Number(n == 1 ? 0 : n >= 2 && n <= 4 ? 1 : 2); + }, + 7: function (n) { + return Number(n == 1 ? 0 : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2); + }, + 8: function (n) { + return Number(n == 1 ? 0 : n == 2 ? 1 : n != 8 && n != 11 ? 2 : 3); + }, + 9: function (n) { + return Number(n >= 2); + }, + 10: function (n) { + return Number(n == 1 ? 0 : n == 2 ? 1 : n < 7 ? 2 : n < 11 ? 3 : 4); + }, + 11: function (n) { + return Number(n == 1 || n == 11 ? 0 : n == 2 || n == 12 ? 1 : n > 2 && n < 20 ? 2 : 3); + }, + 12: function (n) { + return Number(n % 10 != 1 || n % 100 == 11); + }, + 13: function (n) { + return Number(n !== 0); + }, + 14: function (n) { + return Number(n == 1 ? 0 : n == 2 ? 1 : n == 3 ? 2 : 3); + }, + 15: function (n) { + return Number(n % 10 == 1 && n % 100 != 11 ? 0 : n % 10 >= 2 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2); + }, + 16: function (n) { + return Number(n % 10 == 1 && n % 100 != 11 ? 0 : n !== 0 ? 1 : 2); + }, + 17: function (n) { + return Number(n == 1 || n % 10 == 1 && n % 100 != 11 ? 0 : 1); + }, + 18: function (n) { + return Number(n == 0 ? 0 : n == 1 ? 1 : 2); + }, + 19: function (n) { + return Number(n == 1 ? 0 : n == 0 || n % 100 > 1 && n % 100 < 11 ? 1 : n % 100 > 10 && n % 100 < 20 ? 2 : 3); + }, + 20: function (n) { + return Number(n == 1 ? 0 : n == 0 || n % 100 > 0 && n % 100 < 20 ? 1 : 2); + }, + 21: function (n) { + return Number(n % 100 == 1 ? 1 : n % 100 == 2 ? 2 : n % 100 == 3 || n % 100 == 4 ? 3 : 0); + }, + 22: function (n) { + return Number(n == 1 ? 0 : n == 2 ? 1 : (n < 0 || n > 10) && n % 10 == 0 ? 2 : 3); + } + }; + const nonIntlVersions = ['v1', 'v2', 'v3']; + const intlVersions = ['v4']; + const suffixesOrder = { + zero: 0, + one: 1, + two: 2, + few: 3, + many: 4, + other: 5 + }; + function createRules() { + const rules = {}; + sets.forEach(set => { + set.lngs.forEach(l => { + rules[l] = { + numbers: set.nr, + plurals: _rulesPluralsTypes[set.fc] + }; + }); + }); + return rules; + } + class PluralResolver { + constructor(languageUtils) { + let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + this.languageUtils = languageUtils; + this.options = options; + this.logger = baseLogger.create('pluralResolver'); + if ((!this.options.compatibilityJSON || intlVersions.includes(this.options.compatibilityJSON)) && (typeof Intl === 'undefined' || !Intl.PluralRules)) { + this.options.compatibilityJSON = 'v3'; + this.logger.error('Your environment seems not to be Intl API compatible, use an Intl.PluralRules polyfill. Will fallback to the compatibilityJSON v3 format handling.'); + } + this.rules = createRules(); + } + addRule(lng, obj) { + this.rules[lng] = obj; + } + getRule(code) { + let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + if (this.shouldUseIntlApi()) { + try { + return new Intl.PluralRules(getCleanedCode(code), { + type: options.ordinal ? 'ordinal' : 'cardinal' + }); + } catch { + return; + } + } + return this.rules[code] || this.rules[this.languageUtils.getLanguagePartFromCode(code)]; + } + needsPlural(code) { + let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + const rule = this.getRule(code, options); + if (this.shouldUseIntlApi()) { + return rule && rule.resolvedOptions().pluralCategories.length > 1; + } + return rule && rule.numbers.length > 1; + } + getPluralFormsOfKey(code, key) { + let options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + return this.getSuffixes(code, options).map(suffix => `${key}${suffix}`); + } + getSuffixes(code) { + let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + const rule = this.getRule(code, options); + if (!rule) { + return []; + } + if (this.shouldUseIntlApi()) { + return rule.resolvedOptions().pluralCategories.sort((pluralCategory1, pluralCategory2) => suffixesOrder[pluralCategory1] - suffixesOrder[pluralCategory2]).map(pluralCategory => `${this.options.prepend}${options.ordinal ? `ordinal${this.options.prepend}` : ''}${pluralCategory}`); + } + return rule.numbers.map(number => this.getSuffix(code, number, options)); + } + getSuffix(code, count) { + let options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + const rule = this.getRule(code, options); + if (rule) { + if (this.shouldUseIntlApi()) { + return `${this.options.prepend}${options.ordinal ? `ordinal${this.options.prepend}` : ''}${rule.select(count)}`; + } + return this.getSuffixRetroCompatible(rule, count); + } + this.logger.warn(`no plural rule found for: ${code}`); + return ''; + } + getSuffixRetroCompatible(rule, count) { + const idx = rule.noAbs ? rule.plurals(count) : rule.plurals(Math.abs(count)); + let suffix = rule.numbers[idx]; + if (this.options.simplifyPluralSuffix && rule.numbers.length === 2 && rule.numbers[0] === 1) { + if (suffix === 2) { + suffix = 'plural'; + } else if (suffix === 1) { + suffix = ''; + } + } + const returnSuffix = () => this.options.prepend && suffix.toString() ? this.options.prepend + suffix.toString() : suffix.toString(); + if (this.options.compatibilityJSON === 'v1') { + if (suffix === 1) return ''; + if (typeof suffix === 'number') return `_plural_${suffix.toString()}`; + return returnSuffix(); + } else if (this.options.compatibilityJSON === 'v2') { + return returnSuffix(); + } else if (this.options.simplifyPluralSuffix && rule.numbers.length === 2 && rule.numbers[0] === 1) { + return returnSuffix(); + } + return this.options.prepend && idx.toString() ? this.options.prepend + idx.toString() : idx.toString(); + } + shouldUseIntlApi() { + return !nonIntlVersions.includes(this.options.compatibilityJSON); + } + } + + function deepFindWithDefaults(data, defaultData, key) { + let keySeparator = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : '.'; + let ignoreJSONStructure = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : true; + let path = getPathWithDefaults(data, defaultData, key); + if (!path && ignoreJSONStructure && typeof key === 'string') { + path = deepFind(data, key, keySeparator); + if (path === undefined) path = deepFind(defaultData, key, keySeparator); + } + return path; + } + class Interpolator { + constructor() { + let options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + this.logger = baseLogger.create('interpolator'); + this.options = options; + this.format = options.interpolation && options.interpolation.format || (value => value); + this.init(options); + } + init() { + let options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + if (!options.interpolation) options.interpolation = { + escapeValue: true + }; + const iOpts = options.interpolation; + this.escape = iOpts.escape !== undefined ? iOpts.escape : escape; + this.escapeValue = iOpts.escapeValue !== undefined ? iOpts.escapeValue : true; + this.useRawValueToEscape = iOpts.useRawValueToEscape !== undefined ? iOpts.useRawValueToEscape : false; + this.prefix = iOpts.prefix ? regexEscape(iOpts.prefix) : iOpts.prefixEscaped || '{{'; + this.suffix = iOpts.suffix ? regexEscape(iOpts.suffix) : iOpts.suffixEscaped || '}}'; + this.formatSeparator = iOpts.formatSeparator ? iOpts.formatSeparator : iOpts.formatSeparator || ','; + this.unescapePrefix = iOpts.unescapeSuffix ? '' : iOpts.unescapePrefix || '-'; + this.unescapeSuffix = this.unescapePrefix ? '' : iOpts.unescapeSuffix || ''; + this.nestingPrefix = iOpts.nestingPrefix ? regexEscape(iOpts.nestingPrefix) : iOpts.nestingPrefixEscaped || regexEscape('$t('); + this.nestingSuffix = iOpts.nestingSuffix ? regexEscape(iOpts.nestingSuffix) : iOpts.nestingSuffixEscaped || regexEscape(')'); + this.nestingOptionsSeparator = iOpts.nestingOptionsSeparator ? iOpts.nestingOptionsSeparator : iOpts.nestingOptionsSeparator || ','; + this.maxReplaces = iOpts.maxReplaces ? iOpts.maxReplaces : 1000; + this.alwaysFormat = iOpts.alwaysFormat !== undefined ? iOpts.alwaysFormat : false; + this.resetRegExp(); + } + reset() { + if (this.options) this.init(this.options); + } + resetRegExp() { + const regexpStr = `${this.prefix}(.+?)${this.suffix}`; + this.regexp = new RegExp(regexpStr, 'g'); + const regexpUnescapeStr = `${this.prefix}${this.unescapePrefix}(.+?)${this.unescapeSuffix}${this.suffix}`; + this.regexpUnescape = new RegExp(regexpUnescapeStr, 'g'); + const nestingRegexpStr = `${this.nestingPrefix}(.+?)${this.nestingSuffix}`; + this.nestingRegexp = new RegExp(nestingRegexpStr, 'g'); + } + interpolate(str, data, lng, options) { + let match; + let value; + let replaces; + const defaultData = this.options && this.options.interpolation && this.options.interpolation.defaultVariables || {}; + function regexSafe(val) { + return val.replace(/\$/g, '$$$$'); + } + const handleFormat = key => { + if (key.indexOf(this.formatSeparator) < 0) { + const path = deepFindWithDefaults(data, defaultData, key, this.options.keySeparator, this.options.ignoreJSONStructure); + return this.alwaysFormat ? this.format(path, undefined, lng, { + ...options, + ...data, + interpolationkey: key + }) : path; + } + const p = key.split(this.formatSeparator); + const k = p.shift().trim(); + const f = p.join(this.formatSeparator).trim(); + return this.format(deepFindWithDefaults(data, defaultData, k, this.options.keySeparator, this.options.ignoreJSONStructure), f, lng, { + ...options, + ...data, + interpolationkey: k + }); + }; + this.resetRegExp(); + const missingInterpolationHandler = options && options.missingInterpolationHandler || this.options.missingInterpolationHandler; + const skipOnVariables = options && options.interpolation && options.interpolation.skipOnVariables !== undefined ? options.interpolation.skipOnVariables : this.options.interpolation.skipOnVariables; + const todos = [{ + regex: this.regexpUnescape, + safeValue: val => regexSafe(val) + }, { + regex: this.regexp, + safeValue: val => this.escapeValue ? regexSafe(this.escape(val)) : regexSafe(val) + }]; + todos.forEach(todo => { + replaces = 0; + while (match = todo.regex.exec(str)) { + const matchedVar = match[1].trim(); + value = handleFormat(matchedVar); + if (value === undefined) { + if (typeof missingInterpolationHandler === 'function') { + const temp = missingInterpolationHandler(str, match, options); + value = typeof temp === 'string' ? temp : ''; + } else if (options && Object.prototype.hasOwnProperty.call(options, matchedVar)) { + value = ''; + } else if (skipOnVariables) { + value = match[0]; + continue; + } else { + this.logger.warn(`missed to pass in variable ${matchedVar} for interpolating ${str}`); + value = ''; + } + } else if (typeof value !== 'string' && !this.useRawValueToEscape) { + value = makeString(value); + } + const safeValue = todo.safeValue(value); + str = str.replace(match[0], safeValue); + if (skipOnVariables) { + todo.regex.lastIndex += value.length; + todo.regex.lastIndex -= match[0].length; + } else { + todo.regex.lastIndex = 0; + } + replaces++; + if (replaces >= this.maxReplaces) { + break; + } + } + }); + return str; + } + nest(str, fc) { + let options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + let match; + let value; + let clonedOptions; + function handleHasOptions(key, inheritedOptions) { + const sep = this.nestingOptionsSeparator; + if (key.indexOf(sep) < 0) return key; + const c = key.split(new RegExp(`${sep}[ ]*{`)); + let optionsString = `{${c[1]}`; + key = c[0]; + optionsString = this.interpolate(optionsString, clonedOptions); + const matchedSingleQuotes = optionsString.match(/'/g); + const matchedDoubleQuotes = optionsString.match(/"/g); + if (matchedSingleQuotes && matchedSingleQuotes.length % 2 === 0 && !matchedDoubleQuotes || matchedDoubleQuotes.length % 2 !== 0) { + optionsString = optionsString.replace(/'/g, '"'); + } + try { + clonedOptions = JSON.parse(optionsString); + if (inheritedOptions) clonedOptions = { + ...inheritedOptions, + ...clonedOptions + }; + } catch (e) { + this.logger.warn(`failed parsing options string in nesting for key ${key}`, e); + return `${key}${sep}${optionsString}`; + } + delete clonedOptions.defaultValue; + return key; + } + while (match = this.nestingRegexp.exec(str)) { + let formatters = []; + clonedOptions = { + ...options + }; + clonedOptions = clonedOptions.replace && typeof clonedOptions.replace !== 'string' ? clonedOptions.replace : clonedOptions; + clonedOptions.applyPostProcessor = false; + delete clonedOptions.defaultValue; + let doReduce = false; + if (match[0].indexOf(this.formatSeparator) !== -1 && !/{.*}/.test(match[1])) { + const r = match[1].split(this.formatSeparator).map(elem => elem.trim()); + match[1] = r.shift(); + formatters = r; + doReduce = true; + } + value = fc(handleHasOptions.call(this, match[1].trim(), clonedOptions), clonedOptions); + if (value && match[0] === str && typeof value !== 'string') return value; + if (typeof value !== 'string') value = makeString(value); + if (!value) { + this.logger.warn(`missed to resolve ${match[1]} for nesting ${str}`); + value = ''; + } + if (doReduce) { + value = formatters.reduce((v, f) => this.format(v, f, options.lng, { + ...options, + interpolationkey: match[1].trim() + }), value.trim()); + } + str = str.replace(match[0], value); + this.regexp.lastIndex = 0; + } + return str; + } + } + + function parseFormatStr(formatStr) { + let formatName = formatStr.toLowerCase().trim(); + const formatOptions = {}; + if (formatStr.indexOf('(') > -1) { + const p = formatStr.split('('); + formatName = p[0].toLowerCase().trim(); + const optStr = p[1].substring(0, p[1].length - 1); + if (formatName === 'currency' && optStr.indexOf(':') < 0) { + if (!formatOptions.currency) formatOptions.currency = optStr.trim(); + } else if (formatName === 'relativetime' && optStr.indexOf(':') < 0) { + if (!formatOptions.range) formatOptions.range = optStr.trim(); + } else { + const opts = optStr.split(';'); + opts.forEach(opt => { + if (!opt) return; + const [key, ...rest] = opt.split(':'); + const val = rest.join(':').trim().replace(/^'+|'+$/g, ''); + if (!formatOptions[key.trim()]) formatOptions[key.trim()] = val; + if (val === 'false') formatOptions[key.trim()] = false; + if (val === 'true') formatOptions[key.trim()] = true; + if (!isNaN(val)) formatOptions[key.trim()] = parseInt(val, 10); + }); + } + } + return { + formatName, + formatOptions + }; + } + function createCachedFormatter(fn) { + const cache = {}; + return function invokeFormatter(val, lng, options) { + const key = lng + JSON.stringify(options); + let formatter = cache[key]; + if (!formatter) { + formatter = fn(getCleanedCode(lng), options); + cache[key] = formatter; + } + return formatter(val); + }; + } + class Formatter { + constructor() { + let options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + this.logger = baseLogger.create('formatter'); + this.options = options; + this.formats = { + number: createCachedFormatter((lng, opt) => { + const formatter = new Intl.NumberFormat(lng, { + ...opt + }); + return val => formatter.format(val); + }), + currency: createCachedFormatter((lng, opt) => { + const formatter = new Intl.NumberFormat(lng, { + ...opt, + style: 'currency' + }); + return val => formatter.format(val); + }), + datetime: createCachedFormatter((lng, opt) => { + const formatter = new Intl.DateTimeFormat(lng, { + ...opt + }); + return val => formatter.format(val); + }), + relativetime: createCachedFormatter((lng, opt) => { + const formatter = new Intl.RelativeTimeFormat(lng, { + ...opt + }); + return val => formatter.format(val, opt.range || 'day'); + }), + list: createCachedFormatter((lng, opt) => { + const formatter = new Intl.ListFormat(lng, { + ...opt + }); + return val => formatter.format(val); + }) + }; + this.init(options); + } + init(services) { + let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : { + interpolation: {} + }; + const iOpts = options.interpolation; + this.formatSeparator = iOpts.formatSeparator ? iOpts.formatSeparator : iOpts.formatSeparator || ','; + } + add(name, fc) { + this.formats[name.toLowerCase().trim()] = fc; + } + addCached(name, fc) { + this.formats[name.toLowerCase().trim()] = createCachedFormatter(fc); + } + format(value, format, lng) { + let options = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {}; + const formats = format.split(this.formatSeparator); + const result = formats.reduce((mem, f) => { + const { + formatName, + formatOptions + } = parseFormatStr(f); + if (this.formats[formatName]) { + let formatted = mem; + try { + const valOptions = options && options.formatParams && options.formatParams[options.interpolationkey] || {}; + const l = valOptions.locale || valOptions.lng || options.locale || options.lng || lng; + formatted = this.formats[formatName](mem, l, { + ...formatOptions, + ...options, + ...valOptions + }); + } catch (error) { + this.logger.warn(error); + } + return formatted; + } else { + this.logger.warn(`there was no format function for ${formatName}`); + } + return mem; + }, value); + return result; + } + } + + function removePending(q, name) { + if (q.pending[name] !== undefined) { + delete q.pending[name]; + q.pendingCount--; + } + } + class Connector extends EventEmitter { + constructor(backend, store, services) { + let options = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {}; + super(); + this.backend = backend; + this.store = store; + this.services = services; + this.languageUtils = services.languageUtils; + this.options = options; + this.logger = baseLogger.create('backendConnector'); + this.waitingReads = []; + this.maxParallelReads = options.maxParallelReads || 10; + this.readingCalls = 0; + this.maxRetries = options.maxRetries >= 0 ? options.maxRetries : 5; + this.retryTimeout = options.retryTimeout >= 1 ? options.retryTimeout : 350; + this.state = {}; + this.queue = []; + if (this.backend && this.backend.init) { + this.backend.init(services, options.backend, options); + } + } + queueLoad(languages, namespaces, options, callback) { + const toLoad = {}; + const pending = {}; + const toLoadLanguages = {}; + const toLoadNamespaces = {}; + languages.forEach(lng => { + let hasAllNamespaces = true; + namespaces.forEach(ns => { + const name = `${lng}|${ns}`; + if (!options.reload && this.store.hasResourceBundle(lng, ns)) { + this.state[name] = 2; + } else if (this.state[name] < 0) ; else if (this.state[name] === 1) { + if (pending[name] === undefined) pending[name] = true; + } else { + this.state[name] = 1; + hasAllNamespaces = false; + if (pending[name] === undefined) pending[name] = true; + if (toLoad[name] === undefined) toLoad[name] = true; + if (toLoadNamespaces[ns] === undefined) toLoadNamespaces[ns] = true; + } + }); + if (!hasAllNamespaces) toLoadLanguages[lng] = true; + }); + if (Object.keys(toLoad).length || Object.keys(pending).length) { + this.queue.push({ + pending, + pendingCount: Object.keys(pending).length, + loaded: {}, + errors: [], + callback + }); + } + return { + toLoad: Object.keys(toLoad), + pending: Object.keys(pending), + toLoadLanguages: Object.keys(toLoadLanguages), + toLoadNamespaces: Object.keys(toLoadNamespaces) + }; + } + loaded(name, err, data) { + const s = name.split('|'); + const lng = s[0]; + const ns = s[1]; + if (err) this.emit('failedLoading', lng, ns, err); + if (data) { + this.store.addResourceBundle(lng, ns, data); + } + this.state[name] = err ? -1 : 2; + const loaded = {}; + this.queue.forEach(q => { + pushPath(q.loaded, [lng], ns); + removePending(q, name); + if (err) q.errors.push(err); + if (q.pendingCount === 0 && !q.done) { + Object.keys(q.loaded).forEach(l => { + if (!loaded[l]) loaded[l] = {}; + const loadedKeys = q.loaded[l]; + if (loadedKeys.length) { + loadedKeys.forEach(n => { + if (loaded[l][n] === undefined) loaded[l][n] = true; + }); + } + }); + q.done = true; + if (q.errors.length) { + q.callback(q.errors); + } else { + q.callback(); + } + } + }); + this.emit('loaded', loaded); + this.queue = this.queue.filter(q => !q.done); + } + read(lng, ns, fcName) { + let tried = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 0; + let wait = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : this.retryTimeout; + let callback = arguments.length > 5 ? arguments[5] : undefined; + if (!lng.length) return callback(null, {}); + if (this.readingCalls >= this.maxParallelReads) { + this.waitingReads.push({ + lng, + ns, + fcName, + tried, + wait, + callback + }); + return; + } + this.readingCalls++; + const resolver = (err, data) => { + this.readingCalls--; + if (this.waitingReads.length > 0) { + const next = this.waitingReads.shift(); + this.read(next.lng, next.ns, next.fcName, next.tried, next.wait, next.callback); + } + if (err && data && tried < this.maxRetries) { + setTimeout(() => { + this.read.call(this, lng, ns, fcName, tried + 1, wait * 2, callback); + }, wait); + return; + } + callback(err, data); + }; + const fc = this.backend[fcName].bind(this.backend); + if (fc.length === 2) { + try { + const r = fc(lng, ns); + if (r && typeof r.then === 'function') { + r.then(data => resolver(null, data)).catch(resolver); + } else { + resolver(null, r); + } + } catch (err) { + resolver(err); + } + return; + } + return fc(lng, ns, resolver); + } + prepareLoading(languages, namespaces) { + let options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + let callback = arguments.length > 3 ? arguments[3] : undefined; + if (!this.backend) { + this.logger.warn('No backend was added via i18next.use. Will not load resources.'); + return callback && callback(); + } + if (typeof languages === 'string') languages = this.languageUtils.toResolveHierarchy(languages); + if (typeof namespaces === 'string') namespaces = [namespaces]; + const toLoad = this.queueLoad(languages, namespaces, options, callback); + if (!toLoad.toLoad.length) { + if (!toLoad.pending.length) callback(); + return null; + } + toLoad.toLoad.forEach(name => { + this.loadOne(name); + }); + } + load(languages, namespaces, callback) { + this.prepareLoading(languages, namespaces, {}, callback); + } + reload(languages, namespaces, callback) { + this.prepareLoading(languages, namespaces, { + reload: true + }, callback); + } + loadOne(name) { + let prefix = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ''; + const s = name.split('|'); + const lng = s[0]; + const ns = s[1]; + this.read(lng, ns, 'read', undefined, undefined, (err, data) => { + if (err) this.logger.warn(`${prefix}loading namespace ${ns} for language ${lng} failed`, err); + if (!err && data) this.logger.log(`${prefix}loaded namespace ${ns} for language ${lng}`, data); + this.loaded(name, err, data); + }); + } + saveMissing(languages, namespace, key, fallbackValue, isUpdate) { + let options = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : {}; + let clb = arguments.length > 6 && arguments[6] !== undefined ? arguments[6] : () => {}; + if (this.services.utils && this.services.utils.hasLoadedNamespace && !this.services.utils.hasLoadedNamespace(namespace)) { + this.logger.warn(`did not save key "${key}" as the namespace "${namespace}" was not yet loaded`, 'This means something IS WRONG in your setup. You access the t function before i18next.init / i18next.loadNamespace / i18next.changeLanguage was done. Wait for the callback or Promise to resolve before accessing it!!!'); + return; + } + if (key === undefined || key === null || key === '') return; + if (this.backend && this.backend.create) { + const opts = { + ...options, + isUpdate + }; + const fc = this.backend.create.bind(this.backend); + if (fc.length < 6) { + try { + let r; + if (fc.length === 5) { + r = fc(languages, namespace, key, fallbackValue, opts); + } else { + r = fc(languages, namespace, key, fallbackValue); + } + if (r && typeof r.then === 'function') { + r.then(data => clb(null, data)).catch(clb); + } else { + clb(null, r); + } + } catch (err) { + clb(err); + } + } else { + fc(languages, namespace, key, fallbackValue, clb, opts); + } + } + if (!languages || !languages[0]) return; + this.store.addResource(languages[0], namespace, key, fallbackValue); + } + } + + function get() { + return { + debug: false, + initImmediate: true, + ns: ['translation'], + defaultNS: ['translation'], + fallbackLng: ['dev'], + fallbackNS: false, + supportedLngs: false, + nonExplicitSupportedLngs: false, + load: 'all', + preload: false, + simplifyPluralSuffix: true, + keySeparator: '.', + nsSeparator: ':', + pluralSeparator: '_', + contextSeparator: '_', + partialBundledLanguages: false, + saveMissing: false, + updateMissing: false, + saveMissingTo: 'fallback', + saveMissingPlurals: true, + missingKeyHandler: false, + missingInterpolationHandler: false, + postProcess: false, + postProcessPassResolved: false, + returnNull: false, + returnEmptyString: true, + returnObjects: false, + joinArrays: false, + returnedObjectHandler: false, + parseMissingKeyHandler: false, + appendNamespaceToMissingKey: false, + appendNamespaceToCIMode: false, + overloadTranslationOptionHandler: function handle(args) { + let ret = {}; + if (typeof args[1] === 'object') ret = args[1]; + if (typeof args[1] === 'string') ret.defaultValue = args[1]; + if (typeof args[2] === 'string') ret.tDescription = args[2]; + if (typeof args[2] === 'object' || typeof args[3] === 'object') { + const options = args[3] || args[2]; + Object.keys(options).forEach(key => { + ret[key] = options[key]; + }); + } + return ret; + }, + interpolation: { + escapeValue: true, + format: (value, format, lng, options) => value, + prefix: '{{', + suffix: '}}', + formatSeparator: ',', + unescapePrefix: '-', + nestingPrefix: '$t(', + nestingSuffix: ')', + nestingOptionsSeparator: ',', + maxReplaces: 1000, + skipOnVariables: true + } + }; + } + function transformOptions(options) { + if (typeof options.ns === 'string') options.ns = [options.ns]; + if (typeof options.fallbackLng === 'string') options.fallbackLng = [options.fallbackLng]; + if (typeof options.fallbackNS === 'string') options.fallbackNS = [options.fallbackNS]; + if (options.supportedLngs && options.supportedLngs.indexOf('cimode') < 0) { + options.supportedLngs = options.supportedLngs.concat(['cimode']); + } + return options; + } + + function noop() {} + function bindMemberFunctions(inst) { + const mems = Object.getOwnPropertyNames(Object.getPrototypeOf(inst)); + mems.forEach(mem => { + if (typeof inst[mem] === 'function') { + inst[mem] = inst[mem].bind(inst); + } + }); + } + class I18n extends EventEmitter { + constructor() { + let options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + let callback = arguments.length > 1 ? arguments[1] : undefined; + super(); + this.options = transformOptions(options); + this.services = {}; + this.logger = baseLogger; + this.modules = { + external: [] + }; + bindMemberFunctions(this); + if (callback && !this.isInitialized && !options.isClone) { + if (!this.options.initImmediate) { + this.init(options, callback); + return this; + } + setTimeout(() => { + this.init(options, callback); + }, 0); + } + } + init() { + var _this = this; + let options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + let callback = arguments.length > 1 ? arguments[1] : undefined; + if (typeof options === 'function') { + callback = options; + options = {}; + } + if (!options.defaultNS && options.defaultNS !== false && options.ns) { + if (typeof options.ns === 'string') { + options.defaultNS = options.ns; + } else if (options.ns.indexOf('translation') < 0) { + options.defaultNS = options.ns[0]; + } + } + const defOpts = get(); + this.options = { + ...defOpts, + ...this.options, + ...transformOptions(options) + }; + if (this.options.compatibilityAPI !== 'v1') { + this.options.interpolation = { + ...defOpts.interpolation, + ...this.options.interpolation + }; + } + if (options.keySeparator !== undefined) { + this.options.userDefinedKeySeparator = options.keySeparator; + } + if (options.nsSeparator !== undefined) { + this.options.userDefinedNsSeparator = options.nsSeparator; + } + function createClassOnDemand(ClassOrObject) { + if (!ClassOrObject) return null; + if (typeof ClassOrObject === 'function') return new ClassOrObject(); + return ClassOrObject; + } + if (!this.options.isClone) { + if (this.modules.logger) { + baseLogger.init(createClassOnDemand(this.modules.logger), this.options); + } else { + baseLogger.init(null, this.options); + } + let formatter; + if (this.modules.formatter) { + formatter = this.modules.formatter; + } else if (typeof Intl !== 'undefined') { + formatter = Formatter; + } + const lu = new LanguageUtil(this.options); + this.store = new ResourceStore(this.options.resources, this.options); + const s = this.services; + s.logger = baseLogger; + s.resourceStore = this.store; + s.languageUtils = lu; + s.pluralResolver = new PluralResolver(lu, { + prepend: this.options.pluralSeparator, + compatibilityJSON: this.options.compatibilityJSON, + simplifyPluralSuffix: this.options.simplifyPluralSuffix + }); + if (formatter && (!this.options.interpolation.format || this.options.interpolation.format === defOpts.interpolation.format)) { + s.formatter = createClassOnDemand(formatter); + s.formatter.init(s, this.options); + this.options.interpolation.format = s.formatter.format.bind(s.formatter); + } + s.interpolator = new Interpolator(this.options); + s.utils = { + hasLoadedNamespace: this.hasLoadedNamespace.bind(this) + }; + s.backendConnector = new Connector(createClassOnDemand(this.modules.backend), s.resourceStore, s, this.options); + s.backendConnector.on('*', function (event) { + for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { + args[_key - 1] = arguments[_key]; + } + _this.emit(event, ...args); + }); + if (this.modules.languageDetector) { + s.languageDetector = createClassOnDemand(this.modules.languageDetector); + if (s.languageDetector.init) s.languageDetector.init(s, this.options.detection, this.options); + } + if (this.modules.i18nFormat) { + s.i18nFormat = createClassOnDemand(this.modules.i18nFormat); + if (s.i18nFormat.init) s.i18nFormat.init(this); + } + this.translator = new Translator(this.services, this.options); + this.translator.on('*', function (event) { + for (var _len2 = arguments.length, args = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) { + args[_key2 - 1] = arguments[_key2]; + } + _this.emit(event, ...args); + }); + this.modules.external.forEach(m => { + if (m.init) m.init(this); + }); + } + this.format = this.options.interpolation.format; + if (!callback) callback = noop; + if (this.options.fallbackLng && !this.services.languageDetector && !this.options.lng) { + const codes = this.services.languageUtils.getFallbackCodes(this.options.fallbackLng); + if (codes.length > 0 && codes[0] !== 'dev') this.options.lng = codes[0]; + } + if (!this.services.languageDetector && !this.options.lng) { + this.logger.warn('init: no languageDetector is used and no lng is defined'); + } + const storeApi = ['getResource', 'hasResourceBundle', 'getResourceBundle', 'getDataByLanguage']; + storeApi.forEach(fcName => { + this[fcName] = function () { + return _this.store[fcName](...arguments); + }; + }); + const storeApiChained = ['addResource', 'addResources', 'addResourceBundle', 'removeResourceBundle']; + storeApiChained.forEach(fcName => { + this[fcName] = function () { + _this.store[fcName](...arguments); + return _this; + }; + }); + const deferred = defer(); + const load = () => { + const finish = (err, t) => { + if (this.isInitialized && !this.initializedStoreOnce) this.logger.warn('init: i18next is already initialized. You should call init just once!'); + this.isInitialized = true; + if (!this.options.isClone) this.logger.log('initialized', this.options); + this.emit('initialized', this.options); + deferred.resolve(t); + callback(err, t); + }; + if (this.languages && this.options.compatibilityAPI !== 'v1' && !this.isInitialized) return finish(null, this.t.bind(this)); + this.changeLanguage(this.options.lng, finish); + }; + if (this.options.resources || !this.options.initImmediate) { + load(); + } else { + setTimeout(load, 0); + } + return deferred; + } + loadResources(language) { + let callback = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : noop; + let usedCallback = callback; + const usedLng = typeof language === 'string' ? language : this.language; + if (typeof language === 'function') usedCallback = language; + if (!this.options.resources || this.options.partialBundledLanguages) { + if (usedLng && usedLng.toLowerCase() === 'cimode' && (!this.options.preload || this.options.preload.length === 0)) return usedCallback(); + const toLoad = []; + const append = lng => { + if (!lng) return; + if (lng === 'cimode') return; + const lngs = this.services.languageUtils.toResolveHierarchy(lng); + lngs.forEach(l => { + if (l === 'cimode') return; + if (toLoad.indexOf(l) < 0) toLoad.push(l); + }); + }; + if (!usedLng) { + const fallbacks = this.services.languageUtils.getFallbackCodes(this.options.fallbackLng); + fallbacks.forEach(l => append(l)); + } else { + append(usedLng); + } + if (this.options.preload) { + this.options.preload.forEach(l => append(l)); + } + this.services.backendConnector.load(toLoad, this.options.ns, e => { + if (!e && !this.resolvedLanguage && this.language) this.setResolvedLanguage(this.language); + usedCallback(e); + }); + } else { + usedCallback(null); + } + } + reloadResources(lngs, ns, callback) { + const deferred = defer(); + if (!lngs) lngs = this.languages; + if (!ns) ns = this.options.ns; + if (!callback) callback = noop; + this.services.backendConnector.reload(lngs, ns, err => { + deferred.resolve(); + callback(err); + }); + return deferred; + } + use(module) { + if (!module) throw new Error('You are passing an undefined module! Please check the object you are passing to i18next.use()'); + if (!module.type) throw new Error('You are passing a wrong module! Please check the object you are passing to i18next.use()'); + if (module.type === 'backend') { + this.modules.backend = module; + } + if (module.type === 'logger' || module.log && module.warn && module.error) { + this.modules.logger = module; + } + if (module.type === 'languageDetector') { + this.modules.languageDetector = module; + } + if (module.type === 'i18nFormat') { + this.modules.i18nFormat = module; + } + if (module.type === 'postProcessor') { + postProcessor.addPostProcessor(module); + } + if (module.type === 'formatter') { + this.modules.formatter = module; + } + if (module.type === '3rdParty') { + this.modules.external.push(module); + } + return this; + } + setResolvedLanguage(l) { + if (!l || !this.languages) return; + if (['cimode', 'dev'].indexOf(l) > -1) return; + for (let li = 0; li < this.languages.length; li++) { + const lngInLngs = this.languages[li]; + if (['cimode', 'dev'].indexOf(lngInLngs) > -1) continue; + if (this.store.hasLanguageSomeTranslations(lngInLngs)) { + this.resolvedLanguage = lngInLngs; + break; + } + } + } + changeLanguage(lng, callback) { + var _this2 = this; + this.isLanguageChangingTo = lng; + const deferred = defer(); + this.emit('languageChanging', lng); + const setLngProps = l => { + this.language = l; + this.languages = this.services.languageUtils.toResolveHierarchy(l); + this.resolvedLanguage = undefined; + this.setResolvedLanguage(l); + }; + const done = (err, l) => { + if (l) { + setLngProps(l); + this.translator.changeLanguage(l); + this.isLanguageChangingTo = undefined; + this.emit('languageChanged', l); + this.logger.log('languageChanged', l); + } else { + this.isLanguageChangingTo = undefined; + } + deferred.resolve(function () { + return _this2.t(...arguments); + }); + if (callback) callback(err, function () { + return _this2.t(...arguments); + }); + }; + const setLng = lngs => { + if (!lng && !lngs && this.services.languageDetector) lngs = []; + const l = typeof lngs === 'string' ? lngs : this.services.languageUtils.getBestMatchFromCodes(lngs); + if (l) { + if (!this.language) { + setLngProps(l); + } + if (!this.translator.language) this.translator.changeLanguage(l); + if (this.services.languageDetector && this.services.languageDetector.cacheUserLanguage) this.services.languageDetector.cacheUserLanguage(l); + } + this.loadResources(l, err => { + done(err, l); + }); + }; + if (!lng && this.services.languageDetector && !this.services.languageDetector.async) { + setLng(this.services.languageDetector.detect()); + } else if (!lng && this.services.languageDetector && this.services.languageDetector.async) { + if (this.services.languageDetector.detect.length === 0) { + this.services.languageDetector.detect().then(setLng); + } else { + this.services.languageDetector.detect(setLng); + } + } else { + setLng(lng); + } + return deferred; + } + getFixedT(lng, ns, keyPrefix) { + var _this3 = this; + const fixedT = function (key, opts) { + let options; + if (typeof opts !== 'object') { + for (var _len3 = arguments.length, rest = new Array(_len3 > 2 ? _len3 - 2 : 0), _key3 = 2; _key3 < _len3; _key3++) { + rest[_key3 - 2] = arguments[_key3]; + } + options = _this3.options.overloadTranslationOptionHandler([key, opts].concat(rest)); + } else { + options = { + ...opts + }; + } + options.lng = options.lng || fixedT.lng; + options.lngs = options.lngs || fixedT.lngs; + options.ns = options.ns || fixedT.ns; + options.keyPrefix = options.keyPrefix || keyPrefix || fixedT.keyPrefix; + const keySeparator = _this3.options.keySeparator || '.'; + let resultKey; + if (options.keyPrefix && Array.isArray(key)) { + resultKey = key.map(k => `${options.keyPrefix}${keySeparator}${k}`); + } else { + resultKey = options.keyPrefix ? `${options.keyPrefix}${keySeparator}${key}` : key; + } + return _this3.t(resultKey, options); + }; + if (typeof lng === 'string') { + fixedT.lng = lng; + } else { + fixedT.lngs = lng; + } + fixedT.ns = ns; + fixedT.keyPrefix = keyPrefix; + return fixedT; + } + t() { + return this.translator && this.translator.translate(...arguments); + } + exists() { + return this.translator && this.translator.exists(...arguments); + } + setDefaultNamespace(ns) { + this.options.defaultNS = ns; + } + hasLoadedNamespace(ns) { + let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + if (!this.isInitialized) { + this.logger.warn('hasLoadedNamespace: i18next was not initialized', this.languages); + return false; + } + if (!this.languages || !this.languages.length) { + this.logger.warn('hasLoadedNamespace: i18n.languages were undefined or empty', this.languages); + return false; + } + const lng = options.lng || this.resolvedLanguage || this.languages[0]; + const fallbackLng = this.options ? this.options.fallbackLng : false; + const lastLng = this.languages[this.languages.length - 1]; + if (lng.toLowerCase() === 'cimode') return true; + const loadNotPending = (l, n) => { + const loadState = this.services.backendConnector.state[`${l}|${n}`]; + return loadState === -1 || loadState === 2; + }; + if (options.precheck) { + const preResult = options.precheck(this, loadNotPending); + if (preResult !== undefined) return preResult; + } + if (this.hasResourceBundle(lng, ns)) return true; + if (!this.services.backendConnector.backend || this.options.resources && !this.options.partialBundledLanguages) return true; + if (loadNotPending(lng, ns) && (!fallbackLng || loadNotPending(lastLng, ns))) return true; + return false; + } + loadNamespaces(ns, callback) { + const deferred = defer(); + if (!this.options.ns) { + if (callback) callback(); + return Promise.resolve(); + } + if (typeof ns === 'string') ns = [ns]; + ns.forEach(n => { + if (this.options.ns.indexOf(n) < 0) this.options.ns.push(n); + }); + this.loadResources(err => { + deferred.resolve(); + if (callback) callback(err); + }); + return deferred; + } + loadLanguages(lngs, callback) { + const deferred = defer(); + if (typeof lngs === 'string') lngs = [lngs]; + const preloaded = this.options.preload || []; + const newLngs = lngs.filter(lng => preloaded.indexOf(lng) < 0); + if (!newLngs.length) { + if (callback) callback(); + return Promise.resolve(); + } + this.options.preload = preloaded.concat(newLngs); + this.loadResources(err => { + deferred.resolve(); + if (callback) callback(err); + }); + return deferred; + } + dir(lng) { + if (!lng) lng = this.resolvedLanguage || (this.languages && this.languages.length > 0 ? this.languages[0] : this.language); + if (!lng) return 'rtl'; + const rtlLngs = ['ar', 'shu', 'sqr', 'ssh', 'xaa', 'yhd', 'yud', 'aao', 'abh', 'abv', 'acm', 'acq', 'acw', 'acx', 'acy', 'adf', 'ads', 'aeb', 'aec', 'afb', 'ajp', 'apc', 'apd', 'arb', 'arq', 'ars', 'ary', 'arz', 'auz', 'avl', 'ayh', 'ayl', 'ayn', 'ayp', 'bbz', 'pga', 'he', 'iw', 'ps', 'pbt', 'pbu', 'pst', 'prp', 'prd', 'ug', 'ur', 'ydd', 'yds', 'yih', 'ji', 'yi', 'hbo', 'men', 'xmn', 'fa', 'jpr', 'peo', 'pes', 'prs', 'dv', 'sam', 'ckb']; + const languageUtils = this.services && this.services.languageUtils || new LanguageUtil(get()); + return rtlLngs.indexOf(languageUtils.getLanguagePartFromCode(lng)) > -1 || lng.toLowerCase().indexOf('-arab') > 1 ? 'rtl' : 'ltr'; + } + static createInstance() { + let options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + let callback = arguments.length > 1 ? arguments[1] : undefined; + return new I18n(options, callback); + } + cloneInstance() { + let options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + let callback = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : noop; + const forkResourceStore = options.forkResourceStore; + if (forkResourceStore) delete options.forkResourceStore; + const mergedOptions = { + ...this.options, + ...options, + ...{ + isClone: true + } + }; + const clone = new I18n(mergedOptions); + if (options.debug !== undefined || options.prefix !== undefined) { + clone.logger = clone.logger.clone(options); + } + const membersToCopy = ['store', 'services', 'language']; + membersToCopy.forEach(m => { + clone[m] = this[m]; + }); + clone.services = { + ...this.services + }; + clone.services.utils = { + hasLoadedNamespace: clone.hasLoadedNamespace.bind(clone) + }; + if (forkResourceStore) { + clone.store = new ResourceStore(this.store.data, mergedOptions); + clone.services.resourceStore = clone.store; + } + clone.translator = new Translator(clone.services, mergedOptions); + clone.translator.on('*', function (event) { + for (var _len4 = arguments.length, args = new Array(_len4 > 1 ? _len4 - 1 : 0), _key4 = 1; _key4 < _len4; _key4++) { + args[_key4 - 1] = arguments[_key4]; + } + clone.emit(event, ...args); + }); + clone.init(mergedOptions, callback); + clone.translator.options = mergedOptions; + clone.translator.backendConnector.services.utils = { + hasLoadedNamespace: clone.hasLoadedNamespace.bind(clone) + }; + return clone; + } + toJSON() { + return { + options: this.options, + store: this.store, + language: this.language, + languages: this.languages, + resolvedLanguage: this.resolvedLanguage + }; + } + } + const instance = I18n.createInstance(); + instance.createInstance = I18n.createInstance; + + return instance; + +})); diff --git a/static/vendor/i18next/i18nextBrowserLanguageDetector.js b/static/vendor/i18next/i18nextBrowserLanguageDetector.js new file mode 100644 index 00000000..b29574f2 --- /dev/null +++ b/static/vendor/i18next/i18nextBrowserLanguageDetector.js @@ -0,0 +1,420 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.i18nextBrowserLanguageDetector = factory()); +})(this, (function () { 'use strict'; + + function _classCallCheck(instance, Constructor) { + if (!(instance instanceof Constructor)) { + throw new TypeError("Cannot call a class as a function"); + } + } + + function _typeof(o) { + "@babel/helpers - typeof"; + + return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { + return typeof o; + } : function (o) { + return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; + }, _typeof(o); + } + + function _toPrimitive(input, hint) { + if (_typeof(input) !== "object" || input === null) return input; + var prim = input[Symbol.toPrimitive]; + if (prim !== undefined) { + var res = prim.call(input, hint || "default"); + if (_typeof(res) !== "object") return res; + throw new TypeError("@@toPrimitive must return a primitive value."); + } + return (hint === "string" ? String : Number)(input); + } + + function _toPropertyKey(arg) { + var key = _toPrimitive(arg, "string"); + return _typeof(key) === "symbol" ? key : String(key); + } + + function _defineProperties(target, props) { + for (var i = 0; i < props.length; i++) { + var descriptor = props[i]; + descriptor.enumerable = descriptor.enumerable || false; + descriptor.configurable = true; + if ("value" in descriptor) descriptor.writable = true; + Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor); + } + } + function _createClass(Constructor, protoProps, staticProps) { + if (protoProps) _defineProperties(Constructor.prototype, protoProps); + if (staticProps) _defineProperties(Constructor, staticProps); + Object.defineProperty(Constructor, "prototype", { + writable: false + }); + return Constructor; + } + + var arr = []; + var each = arr.forEach; + var slice = arr.slice; + function defaults(obj) { + each.call(slice.call(arguments, 1), function (source) { + if (source) { + for (var prop in source) { + if (obj[prop] === undefined) obj[prop] = source[prop]; + } + } + }); + return obj; + } + + // eslint-disable-next-line no-control-regex + var fieldContentRegExp = /^[\u0009\u0020-\u007e\u0080-\u00ff]+$/; + var serializeCookie = function serializeCookie(name, val, options) { + var opt = options || {}; + opt.path = opt.path || '/'; + var value = encodeURIComponent(val); + var str = "".concat(name, "=").concat(value); + if (opt.maxAge > 0) { + var maxAge = opt.maxAge - 0; + if (Number.isNaN(maxAge)) throw new Error('maxAge should be a Number'); + str += "; Max-Age=".concat(Math.floor(maxAge)); + } + if (opt.domain) { + if (!fieldContentRegExp.test(opt.domain)) { + throw new TypeError('option domain is invalid'); + } + str += "; Domain=".concat(opt.domain); + } + if (opt.path) { + if (!fieldContentRegExp.test(opt.path)) { + throw new TypeError('option path is invalid'); + } + str += "; Path=".concat(opt.path); + } + if (opt.expires) { + if (typeof opt.expires.toUTCString !== 'function') { + throw new TypeError('option expires is invalid'); + } + str += "; Expires=".concat(opt.expires.toUTCString()); + } + if (opt.httpOnly) str += '; HttpOnly'; + if (opt.secure) str += '; Secure'; + if (opt.sameSite) { + var sameSite = typeof opt.sameSite === 'string' ? opt.sameSite.toLowerCase() : opt.sameSite; + switch (sameSite) { + case true: + str += '; SameSite=Strict'; + break; + case 'lax': + str += '; SameSite=Lax'; + break; + case 'strict': + str += '; SameSite=Strict'; + break; + case 'none': + str += '; SameSite=None'; + break; + default: + throw new TypeError('option sameSite is invalid'); + } + } + return str; + }; + var cookie = { + create: function create(name, value, minutes, domain) { + var cookieOptions = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : { + path: '/', + sameSite: 'strict' + }; + if (minutes) { + cookieOptions.expires = new Date(); + cookieOptions.expires.setTime(cookieOptions.expires.getTime() + minutes * 60 * 1000); + } + if (domain) cookieOptions.domain = domain; + document.cookie = serializeCookie(name, encodeURIComponent(value), cookieOptions); + }, + read: function read(name) { + var nameEQ = "".concat(name, "="); + var ca = document.cookie.split(';'); + for (var i = 0; i < ca.length; i++) { + var c = ca[i]; + while (c.charAt(0) === ' ') c = c.substring(1, c.length); + if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length); + } + return null; + }, + remove: function remove(name) { + this.create(name, '', -1); + } + }; + var cookie$1 = { + name: 'cookie', + lookup: function lookup(options) { + var found; + if (options.lookupCookie && typeof document !== 'undefined') { + var c = cookie.read(options.lookupCookie); + if (c) found = c; + } + return found; + }, + cacheUserLanguage: function cacheUserLanguage(lng, options) { + if (options.lookupCookie && typeof document !== 'undefined') { + cookie.create(options.lookupCookie, lng, options.cookieMinutes, options.cookieDomain, options.cookieOptions); + } + } + }; + + var querystring = { + name: 'querystring', + lookup: function lookup(options) { + var found; + if (typeof window !== 'undefined') { + var search = window.location.search; + if (!window.location.search && window.location.hash && window.location.hash.indexOf('?') > -1) { + search = window.location.hash.substring(window.location.hash.indexOf('?')); + } + var query = search.substring(1); + var params = query.split('&'); + for (var i = 0; i < params.length; i++) { + var pos = params[i].indexOf('='); + if (pos > 0) { + var key = params[i].substring(0, pos); + if (key === options.lookupQuerystring) { + found = params[i].substring(pos + 1); + } + } + } + } + return found; + } + }; + + var hasLocalStorageSupport = null; + var localStorageAvailable = function localStorageAvailable() { + if (hasLocalStorageSupport !== null) return hasLocalStorageSupport; + try { + hasLocalStorageSupport = window !== 'undefined' && window.localStorage !== null; + var testKey = 'i18next.translate.boo'; + window.localStorage.setItem(testKey, 'foo'); + window.localStorage.removeItem(testKey); + } catch (e) { + hasLocalStorageSupport = false; + } + return hasLocalStorageSupport; + }; + var localStorage = { + name: 'localStorage', + lookup: function lookup(options) { + var found; + if (options.lookupLocalStorage && localStorageAvailable()) { + var lng = window.localStorage.getItem(options.lookupLocalStorage); + if (lng) found = lng; + } + return found; + }, + cacheUserLanguage: function cacheUserLanguage(lng, options) { + if (options.lookupLocalStorage && localStorageAvailable()) { + window.localStorage.setItem(options.lookupLocalStorage, lng); + } + } + }; + + var hasSessionStorageSupport = null; + var sessionStorageAvailable = function sessionStorageAvailable() { + if (hasSessionStorageSupport !== null) return hasSessionStorageSupport; + try { + hasSessionStorageSupport = window !== 'undefined' && window.sessionStorage !== null; + var testKey = 'i18next.translate.boo'; + window.sessionStorage.setItem(testKey, 'foo'); + window.sessionStorage.removeItem(testKey); + } catch (e) { + hasSessionStorageSupport = false; + } + return hasSessionStorageSupport; + }; + var sessionStorage = { + name: 'sessionStorage', + lookup: function lookup(options) { + var found; + if (options.lookupSessionStorage && sessionStorageAvailable()) { + var lng = window.sessionStorage.getItem(options.lookupSessionStorage); + if (lng) found = lng; + } + return found; + }, + cacheUserLanguage: function cacheUserLanguage(lng, options) { + if (options.lookupSessionStorage && sessionStorageAvailable()) { + window.sessionStorage.setItem(options.lookupSessionStorage, lng); + } + } + }; + + var navigator$1 = { + name: 'navigator', + lookup: function lookup(options) { + var found = []; + if (typeof navigator !== 'undefined') { + if (navigator.languages) { + // chrome only; not an array, so can't use .push.apply instead of iterating + for (var i = 0; i < navigator.languages.length; i++) { + found.push(navigator.languages[i]); + } + } + if (navigator.userLanguage) { + found.push(navigator.userLanguage); + } + if (navigator.language) { + found.push(navigator.language); + } + } + return found.length > 0 ? found : undefined; + } + }; + + var htmlTag = { + name: 'htmlTag', + lookup: function lookup(options) { + var found; + var htmlTag = options.htmlTag || (typeof document !== 'undefined' ? document.documentElement : null); + if (htmlTag && typeof htmlTag.getAttribute === 'function') { + found = htmlTag.getAttribute('lang'); + } + return found; + } + }; + + var path = { + name: 'path', + lookup: function lookup(options) { + var found; + if (typeof window !== 'undefined') { + var language = window.location.pathname.match(/\/([a-zA-Z-]*)/g); + if (language instanceof Array) { + if (typeof options.lookupFromPathIndex === 'number') { + if (typeof language[options.lookupFromPathIndex] !== 'string') { + return undefined; + } + found = language[options.lookupFromPathIndex].replace('/', ''); + } else { + found = language[0].replace('/', ''); + } + } + } + return found; + } + }; + + var subdomain = { + name: 'subdomain', + lookup: function lookup(options) { + // If given get the subdomain index else 1 + var lookupFromSubdomainIndex = typeof options.lookupFromSubdomainIndex === 'number' ? options.lookupFromSubdomainIndex + 1 : 1; + // get all matches if window.location. is existing + // first item of match is the match itself and the second is the first group macht which sould be the first subdomain match + // is the hostname no public domain get the or option of localhost + var language = typeof window !== 'undefined' && window.location && window.location.hostname && window.location.hostname.match(/^(\w{2,5})\.(([a-z0-9-]{1,63}\.[a-z]{2,6})|localhost)/i); + + // if there is no match (null) return undefined + if (!language) return undefined; + // return the given group match + return language[lookupFromSubdomainIndex]; + } + }; + + function getDefaults() { + return { + order: ['querystring', 'cookie', 'localStorage', 'sessionStorage', 'navigator', 'htmlTag'], + lookupQuerystring: 'lng', + lookupCookie: 'i18next', + lookupLocalStorage: 'i18nextLng', + lookupSessionStorage: 'i18nextLng', + // cache user language + caches: ['localStorage'], + excludeCacheFor: ['cimode'], + // cookieMinutes: 10, + // cookieDomain: 'myDomain' + + convertDetectedLanguage: function convertDetectedLanguage(l) { + return l; + } + }; + } + var Browser = /*#__PURE__*/function () { + function Browser(services) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + _classCallCheck(this, Browser); + this.type = 'languageDetector'; + this.detectors = {}; + this.init(services, options); + } + _createClass(Browser, [{ + key: "init", + value: function init(services) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + var i18nOptions = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + this.services = services || { + languageUtils: {} + }; // this way the language detector can be used without i18next + this.options = defaults(options, this.options || {}, getDefaults()); + if (typeof this.options.convertDetectedLanguage === 'string' && this.options.convertDetectedLanguage.indexOf('15897') > -1) { + this.options.convertDetectedLanguage = function (l) { + return l.replace('-', '_'); + }; + } + + // backwards compatibility + if (this.options.lookupFromUrlIndex) this.options.lookupFromPathIndex = this.options.lookupFromUrlIndex; + this.i18nOptions = i18nOptions; + this.addDetector(cookie$1); + this.addDetector(querystring); + this.addDetector(localStorage); + this.addDetector(sessionStorage); + this.addDetector(navigator$1); + this.addDetector(htmlTag); + this.addDetector(path); + this.addDetector(subdomain); + } + }, { + key: "addDetector", + value: function addDetector(detector) { + this.detectors[detector.name] = detector; + } + }, { + key: "detect", + value: function detect(detectionOrder) { + var _this = this; + if (!detectionOrder) detectionOrder = this.options.order; + var detected = []; + detectionOrder.forEach(function (detectorName) { + if (_this.detectors[detectorName]) { + var lookup = _this.detectors[detectorName].lookup(_this.options); + if (lookup && typeof lookup === 'string') lookup = [lookup]; + if (lookup) detected = detected.concat(lookup); + } + }); + detected = detected.map(function (d) { + return _this.options.convertDetectedLanguage(d); + }); + if (this.services.languageUtils.getBestMatchFromCodes) return detected; // new i18next v19.5.0 + return detected.length > 0 ? detected[0] : null; // a little backward compatibility + } + }, { + key: "cacheUserLanguage", + value: function cacheUserLanguage(lng, caches) { + var _this2 = this; + if (!caches) caches = this.options.caches; + if (!caches) return; + if (this.options.excludeCacheFor && this.options.excludeCacheFor.indexOf(lng) > -1) return; + caches.forEach(function (cacheName) { + if (_this2.detectors[cacheName]) _this2.detectors[cacheName].cacheUserLanguage(lng, _this2.options); + }); + } + }]); + return Browser; + }(); + Browser.type = 'languageDetector'; + + return Browser; + +})); diff --git a/static/vendor/i18next/i18nextChainedBackend.js b/static/vendor/i18next/i18nextChainedBackend.js new file mode 100644 index 00000000..2b70ed29 --- /dev/null +++ b/static/vendor/i18next/i18nextChainedBackend.js @@ -0,0 +1,266 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.i18nextChainedBackend = factory()); +})(this, (function () { 'use strict'; + + function _classCallCheck(instance, Constructor) { + if (!(instance instanceof Constructor)) { + throw new TypeError("Cannot call a class as a function"); + } + } + + function _typeof(o) { + "@babel/helpers - typeof"; + + return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { + return typeof o; + } : function (o) { + return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; + }, _typeof(o); + } + + function _toPrimitive(input, hint) { + if (_typeof(input) !== "object" || input === null) return input; + var prim = input[Symbol.toPrimitive]; + if (prim !== undefined) { + var res = prim.call(input, hint || "default"); + if (_typeof(res) !== "object") return res; + throw new TypeError("@@toPrimitive must return a primitive value."); + } + return (hint === "string" ? String : Number)(input); + } + + function _toPropertyKey(arg) { + var key = _toPrimitive(arg, "string"); + return _typeof(key) === "symbol" ? key : String(key); + } + + function _defineProperties(target, props) { + for (var i = 0; i < props.length; i++) { + var descriptor = props[i]; + descriptor.enumerable = descriptor.enumerable || false; + descriptor.configurable = true; + if ("value" in descriptor) descriptor.writable = true; + Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor); + } + } + function _createClass(Constructor, protoProps, staticProps) { + if (protoProps) _defineProperties(Constructor.prototype, protoProps); + if (staticProps) _defineProperties(Constructor, staticProps); + Object.defineProperty(Constructor, "prototype", { + writable: false + }); + return Constructor; + } + + var arr = []; + var each = arr.forEach; + var slice = arr.slice; + function defaults(obj) { + each.call(slice.call(arguments, 1), function (source) { + if (source) { + for (var prop in source) { + if (obj[prop] === undefined) obj[prop] = source[prop]; + } + } + }); + return obj; + } + function createClassOnDemand(ClassOrObject) { + if (!ClassOrObject) return null; + if (typeof ClassOrObject === 'function') return new ClassOrObject(); + return ClassOrObject; + } + + function getDefaults() { + return { + handleEmptyResourcesAsFailed: true, + cacheHitMode: 'none' + // reloadInterval: typeof window !== 'undefined' ? false : 60 * 60 * 1000 + // refreshExpirationTime: 60 * 60 * 1000 + }; + } + + function handleCorrectReadFunction(backend, language, namespace, resolver) { + var fc = backend.read.bind(backend); + if (fc.length === 2) { + // no callback + try { + var r = fc(language, namespace); + if (r && typeof r.then === 'function') { + // promise + r.then(function (data) { + return resolver(null, data); + })["catch"](resolver); + } else { + // sync + resolver(null, r); + } + } catch (err) { + resolver(err); + } + return; + } + + // normal with callback + fc(language, namespace, resolver); + } + var Backend = /*#__PURE__*/function () { + function Backend(services) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + var i18nextOptions = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + _classCallCheck(this, Backend); + this.backends = []; + this.type = 'backend'; + this.allOptions = i18nextOptions; + this.init(services, options); + } + _createClass(Backend, [{ + key: "init", + value: function init(services) { + var _this = this; + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + var i18nextOptions = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + this.services = services; + this.options = defaults(options, this.options || {}, getDefaults()); + this.allOptions = i18nextOptions; + this.options.backends && this.options.backends.forEach(function (b, i) { + _this.backends[i] = _this.backends[i] || createClassOnDemand(b); + _this.backends[i].init(services, _this.options.backendOptions && _this.options.backendOptions[i] || {}, i18nextOptions); + }); + if (this.services && this.options.reloadInterval) { + setInterval(function () { + return _this.reload(); + }, this.options.reloadInterval); + } + } + }, { + key: "read", + value: function read(language, namespace, callback) { + var _this2 = this; + var bLen = this.backends.length; + var loadPosition = function loadPosition(pos) { + if (pos >= bLen) return callback(new Error('non of the backend loaded data', true)); // failed pass retry flag + var isLastBackend = pos === bLen - 1; + var lengthCheckAmount = _this2.options.handleEmptyResourcesAsFailed && !isLastBackend ? 0 : -1; + var backend = _this2.backends[pos]; + if (backend.read) { + handleCorrectReadFunction(backend, language, namespace, function (err, data, savedAt) { + if (!err && data && Object.keys(data).length > lengthCheckAmount) { + callback(null, data, pos); + savePosition(pos - 1, data); // save one in front + if (backend.save && _this2.options.cacheHitMode && ['refresh', 'refreshAndUpdateStore'].indexOf(_this2.options.cacheHitMode) > -1) { + if (savedAt && _this2.options.refreshExpirationTime && savedAt + _this2.options.refreshExpirationTime > Date.now()) return; + var nextBackend = _this2.backends[pos + 1]; + if (nextBackend && nextBackend.read) { + handleCorrectReadFunction(nextBackend, language, namespace, function (err, data) { + if (err) return; + if (!data) return; + if (Object.keys(data).length <= lengthCheckAmount) return; + savePosition(pos, data); + if (_this2.options.cacheHitMode !== 'refreshAndUpdateStore') return; + if (_this2.services && _this2.services.resourceStore) { + _this2.services.resourceStore.addResourceBundle(language, namespace, data); + } + }); + } + } + } else { + loadPosition(pos + 1); // try load from next + } + }); + } else { + loadPosition(pos + 1); // try load from next + } + }; + + var savePosition = function savePosition(pos, data) { + if (pos < 0) return; + var backend = _this2.backends[pos]; + if (backend.save) { + backend.save(language, namespace, data); + savePosition(pos - 1, data); + } else { + savePosition(pos - 1, data); + } + }; + loadPosition(0); + } + }, { + key: "create", + value: function create(languages, namespace, key, fallbackValue) { + var clb = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : function () {}; + var opts = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : {}; + this.backends.forEach(function (b) { + if (!b.create) return; + var fc = b.create.bind(b); + if (fc.length < 6) { + // no callback + try { + var r; + if (fc.length === 5) { + // future callback-less api for i18next-locize-backend + r = fc(languages, namespace, key, fallbackValue, opts); + } else { + r = fc(languages, namespace, key, fallbackValue); + } + if (r && typeof r.then === 'function') { + // promise + r.then(function (data) { + return clb(null, data); + })["catch"](clb); + } else { + // sync + clb(null, r); + } + } catch (err) { + clb(err); + } + return; + } + + // normal with callback + fc(languages, namespace, key, fallbackValue, clb /* unused callback */, opts); + }); + } + }, { + key: "reload", + value: function reload() { + var _this3 = this; + var _this$services = this.services, + backendConnector = _this$services.backendConnector, + languageUtils = _this$services.languageUtils, + logger = _this$services.logger; + var currentLanguage = backendConnector.language; + if (currentLanguage && currentLanguage.toLowerCase() === 'cimode') return; // avoid loading resources for cimode + + var toLoad = []; + var append = function append(lng) { + var lngs = languageUtils.toResolveHierarchy(lng); + lngs.forEach(function (l) { + if (toLoad.indexOf(l) < 0) toLoad.push(l); + }); + }; + append(currentLanguage); + if (this.allOptions.preload) this.allOptions.preload.forEach(function (l) { + return append(l); + }); + toLoad.forEach(function (lng) { + _this3.allOptions.ns.forEach(function (ns) { + backendConnector.read(lng, ns, 'read', null, null, function (err, data) { + if (err) logger.warn("loading namespace ".concat(ns, " for language ").concat(lng, " failed"), err); + if (!err && data) logger.log("loaded namespace ".concat(ns, " for language ").concat(lng), data); + backendConnector.loaded("".concat(lng, "|").concat(ns), err, data); + }); + }); + }); + } + }]); + return Backend; + }(); + Backend.type = 'backend'; + + return Backend; + +})); diff --git a/static/vendor/i18next/i18nextHttpBackend.js b/static/vendor/i18next/i18nextHttpBackend.js new file mode 100644 index 00000000..163320a6 --- /dev/null +++ b/static/vendor/i18next/i18nextHttpBackend.js @@ -0,0 +1,414 @@ +(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.i18nextHttpBackend = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i 1 && arguments[1] !== undefined ? arguments[1] : {}; + var allOptions = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + _classCallCheck(this, Backend); + this.services = services; + this.options = options; + this.allOptions = allOptions; + this.type = 'backend'; + this.init(services, options, allOptions); + } + _createClass(Backend, [{ + key: "init", + value: function init(services) { + var _this = this; + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + var allOptions = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + this.services = services; + this.options = (0, _utils.defaults)(options, this.options || {}, getDefaults()); + this.allOptions = allOptions; + if (this.services && this.options.reloadInterval) { + setInterval(function () { + return _this.reload(); + }, this.options.reloadInterval); + } + } + }, { + key: "readMulti", + value: function readMulti(languages, namespaces, callback) { + this._readAny(languages, languages, namespaces, namespaces, callback); + } + }, { + key: "read", + value: function read(language, namespace, callback) { + this._readAny([language], language, [namespace], namespace, callback); + } + }, { + key: "_readAny", + value: function _readAny(languages, loadUrlLanguages, namespaces, loadUrlNamespaces, callback) { + var _this2 = this; + var loadPath = this.options.loadPath; + if (typeof this.options.loadPath === 'function') { + loadPath = this.options.loadPath(languages, namespaces); + } + loadPath = (0, _utils.makePromise)(loadPath); + loadPath.then(function (resolvedLoadPath) { + if (!resolvedLoadPath) return callback(null, {}); + var url = _this2.services.interpolator.interpolate(resolvedLoadPath, { + lng: languages.join('+'), + ns: namespaces.join('+') + }); + _this2.loadUrl(url, callback, loadUrlLanguages, loadUrlNamespaces); + }); + } + }, { + key: "loadUrl", + value: function loadUrl(url, callback, languages, namespaces) { + var _this3 = this; + var lng = typeof languages === 'string' ? [languages] : languages; + var ns = typeof namespaces === 'string' ? [namespaces] : namespaces; + var payload = this.options.parseLoadPayload(lng, ns); + this.options.request(this.options, url, payload, function (err, res) { + if (res && (res.status >= 500 && res.status < 600 || !res.status)) return callback('failed loading ' + url + '; status code: ' + res.status, true); + if (res && res.status >= 400 && res.status < 500) return callback('failed loading ' + url + '; status code: ' + res.status, false); + if (!res && err && err.message && err.message.indexOf('Failed to fetch') > -1) return callback('failed loading ' + url + ': ' + err.message, true); + if (err) return callback(err, false); + var ret, parseErr; + try { + if (typeof res.data === 'string') { + ret = _this3.options.parse(res.data, languages, namespaces); + } else { + ret = res.data; + } + } catch (e) { + parseErr = 'failed parsing ' + url + ' to json'; + } + if (parseErr) return callback(parseErr, false); + callback(null, ret); + }); + } + }, { + key: "create", + value: function create(languages, namespace, key, fallbackValue, callback) { + var _this4 = this; + if (!this.options.addPath) return; + if (typeof languages === 'string') languages = [languages]; + var payload = this.options.parsePayload(namespace, key, fallbackValue); + var finished = 0; + var dataArray = []; + var resArray = []; + languages.forEach(function (lng) { + var addPath = _this4.options.addPath; + if (typeof _this4.options.addPath === 'function') { + addPath = _this4.options.addPath(lng, namespace); + } + var url = _this4.services.interpolator.interpolate(addPath, { + lng: lng, + ns: namespace + }); + _this4.options.request(_this4.options, url, payload, function (data, res) { + finished += 1; + dataArray.push(data); + resArray.push(res); + if (finished === languages.length) { + if (typeof callback === 'function') callback(dataArray, resArray); + } + }); + }); + } + }, { + key: "reload", + value: function reload() { + var _this5 = this; + var _this$services = this.services, + backendConnector = _this$services.backendConnector, + languageUtils = _this$services.languageUtils, + logger = _this$services.logger; + var currentLanguage = backendConnector.language; + if (currentLanguage && currentLanguage.toLowerCase() === 'cimode') return; + var toLoad = []; + var append = function append(lng) { + var lngs = languageUtils.toResolveHierarchy(lng); + lngs.forEach(function (l) { + if (toLoad.indexOf(l) < 0) toLoad.push(l); + }); + }; + append(currentLanguage); + if (this.allOptions.preload) this.allOptions.preload.forEach(function (l) { + return append(l); + }); + toLoad.forEach(function (lng) { + _this5.allOptions.ns.forEach(function (ns) { + backendConnector.read(lng, ns, 'read', null, null, function (err, data) { + if (err) logger.warn("loading namespace ".concat(ns, " for language ").concat(lng, " failed"), err); + if (!err && data) logger.log("loaded namespace ".concat(ns, " for language ").concat(lng), data); + backendConnector.loaded("".concat(lng, "|").concat(ns), err, data); + }); + }); + }); + } + }]); + return Backend; +}(); +Backend.type = 'backend'; +var _default = exports.default = Backend; +module.exports = exports.default; +},{"./request.js":3,"./utils.js":4}],3:[function(require,module,exports){ +(function (global){(function (){ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = void 0; +var _utils = require("./utils.js"); +var fetchNode = _interopRequireWildcard(require("./getFetch.js")); +function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(e) { return e ? t : r; })(e); } +function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != _typeof(e) && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; } +function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } +var fetchApi; +if (typeof fetch === 'function') { + if (typeof global !== 'undefined' && global.fetch) { + fetchApi = global.fetch; + } else if (typeof window !== 'undefined' && window.fetch) { + fetchApi = window.fetch; + } else { + fetchApi = fetch; + } +} +var XmlHttpRequestApi; +if ((0, _utils.hasXMLHttpRequest)()) { + if (typeof global !== 'undefined' && global.XMLHttpRequest) { + XmlHttpRequestApi = global.XMLHttpRequest; + } else if (typeof window !== 'undefined' && window.XMLHttpRequest) { + XmlHttpRequestApi = window.XMLHttpRequest; + } +} +var ActiveXObjectApi; +if (typeof ActiveXObject === 'function') { + if (typeof global !== 'undefined' && global.ActiveXObject) { + ActiveXObjectApi = global.ActiveXObject; + } else if (typeof window !== 'undefined' && window.ActiveXObject) { + ActiveXObjectApi = window.ActiveXObject; + } +} +if (!fetchApi && fetchNode && !XmlHttpRequestApi && !ActiveXObjectApi) fetchApi = fetchNode.default || fetchNode; +if (typeof fetchApi !== 'function') fetchApi = undefined; +var addQueryString = function addQueryString(url, params) { + if (params && _typeof(params) === 'object') { + var queryString = ''; + for (var paramName in params) { + queryString += '&' + encodeURIComponent(paramName) + '=' + encodeURIComponent(params[paramName]); + } + if (!queryString) return url; + url = url + (url.indexOf('?') !== -1 ? '&' : '?') + queryString.slice(1); + } + return url; +}; +var fetchIt = function fetchIt(url, fetchOptions, callback) { + var resolver = function resolver(response) { + if (!response.ok) return callback(response.statusText || 'Error', { + status: response.status + }); + response.text().then(function (data) { + callback(null, { + status: response.status, + data: data + }); + }).catch(callback); + }; + if (typeof fetch === 'function') { + fetch(url, fetchOptions).then(resolver).catch(callback); + } else { + fetchApi(url, fetchOptions).then(resolver).catch(callback); + } +}; +var omitFetchOptions = false; +var requestWithFetch = function requestWithFetch(options, url, payload, callback) { + if (options.queryStringParams) { + url = addQueryString(url, options.queryStringParams); + } + var headers = (0, _utils.defaults)({}, typeof options.customHeaders === 'function' ? options.customHeaders() : options.customHeaders); + if (typeof window === 'undefined' && typeof global !== 'undefined' && typeof global.process !== 'undefined' && global.process.versions && global.process.versions.node) { + headers['User-Agent'] = "i18next-http-backend (node/".concat(global.process.version, "; ").concat(global.process.platform, " ").concat(global.process.arch, ")"); + } + if (payload) headers['Content-Type'] = 'application/json'; + var reqOptions = typeof options.requestOptions === 'function' ? options.requestOptions(payload) : options.requestOptions; + var fetchOptions = (0, _utils.defaults)({ + method: payload ? 'POST' : 'GET', + body: payload ? options.stringify(payload) : undefined, + headers: headers + }, omitFetchOptions ? {} : reqOptions); + try { + fetchIt(url, fetchOptions, callback); + } catch (e) { + if (!reqOptions || Object.keys(reqOptions).length === 0 || !e.message || e.message.indexOf('not implemented') < 0) { + return callback(e); + } + try { + Object.keys(reqOptions).forEach(function (opt) { + delete fetchOptions[opt]; + }); + fetchIt(url, fetchOptions, callback); + omitFetchOptions = true; + } catch (err) { + callback(err); + } + } +}; +var requestWithXmlHttpRequest = function requestWithXmlHttpRequest(options, url, payload, callback) { + if (payload && _typeof(payload) === 'object') { + payload = addQueryString('', payload).slice(1); + } + if (options.queryStringParams) { + url = addQueryString(url, options.queryStringParams); + } + try { + var x; + if (XmlHttpRequestApi) { + x = new XmlHttpRequestApi(); + } else { + x = new ActiveXObjectApi('MSXML2.XMLHTTP.3.0'); + } + x.open(payload ? 'POST' : 'GET', url, 1); + if (!options.crossDomain) { + x.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); + } + x.withCredentials = !!options.withCredentials; + if (payload) { + x.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); + } + if (x.overrideMimeType) { + x.overrideMimeType('application/json'); + } + var h = options.customHeaders; + h = typeof h === 'function' ? h() : h; + if (h) { + for (var i in h) { + x.setRequestHeader(i, h[i]); + } + } + x.onreadystatechange = function () { + x.readyState > 3 && callback(x.status >= 400 ? x.statusText : null, { + status: x.status, + data: x.responseText + }); + }; + x.send(payload); + } catch (e) { + console && console.log(e); + } +}; +var request = function request(options, url, payload, callback) { + if (typeof payload === 'function') { + callback = payload; + payload = undefined; + } + callback = callback || function () {}; + if (fetchApi && url.indexOf('file:') !== 0) { + return requestWithFetch(options, url, payload, callback); + } + if ((0, _utils.hasXMLHttpRequest)() || typeof ActiveXObject === 'function') { + return requestWithXmlHttpRequest(options, url, payload, callback); + } + callback(new Error('No fetch and no xhr implementation found!')); +}; +var _default = exports.default = request; +module.exports = exports.default; +}).call(this)}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{"./getFetch.js":1,"./utils.js":4}],4:[function(require,module,exports){ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.defaults = defaults; +exports.hasXMLHttpRequest = hasXMLHttpRequest; +exports.makePromise = makePromise; +function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } +var arr = []; +var each = arr.forEach; +var slice = arr.slice; +function defaults(obj) { + each.call(slice.call(arguments, 1), function (source) { + if (source) { + for (var prop in source) { + if (obj[prop] === undefined) obj[prop] = source[prop]; + } + } + }); + return obj; +} +function hasXMLHttpRequest() { + return typeof XMLHttpRequest === 'function' || (typeof XMLHttpRequest === "undefined" ? "undefined" : _typeof(XMLHttpRequest)) === 'object'; +} +function isPromise(maybePromise) { + return !!maybePromise && typeof maybePromise.then === 'function'; +} +function makePromise(maybePromise) { + if (isPromise(maybePromise)) { + return maybePromise; + } + return Promise.resolve(maybePromise); +} +},{}],5:[function(require,module,exports){ + +},{}]},{},[2])(2) +}); diff --git a/static/vendor/i18next/i18nextLocalStorageBackend.js b/static/vendor/i18next/i18nextLocalStorageBackend.js new file mode 100644 index 00000000..66c7f226 --- /dev/null +++ b/static/vendor/i18next/i18nextLocalStorageBackend.js @@ -0,0 +1,190 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.i18nextLocalStorageBackend = factory()); +})(this, (function () { 'use strict'; + + function _typeof(o) { + "@babel/helpers - typeof"; + + return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { + return typeof o; + } : function (o) { + return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; + }, _typeof(o); + } + + function _toPrimitive(input, hint) { + if (_typeof(input) !== "object" || input === null) return input; + var prim = input[Symbol.toPrimitive]; + if (prim !== undefined) { + var res = prim.call(input, hint || "default"); + if (_typeof(res) !== "object") return res; + throw new TypeError("@@toPrimitive must return a primitive value."); + } + return (hint === "string" ? String : Number)(input); + } + + function _toPropertyKey(arg) { + var key = _toPrimitive(arg, "string"); + return _typeof(key) === "symbol" ? key : String(key); + } + + function _defineProperty(obj, key, value) { + key = _toPropertyKey(key); + if (key in obj) { + Object.defineProperty(obj, key, { + value: value, + enumerable: true, + configurable: true, + writable: true + }); + } else { + obj[key] = value; + } + return obj; + } + + function _classCallCheck(instance, Constructor) { + if (!(instance instanceof Constructor)) { + throw new TypeError("Cannot call a class as a function"); + } + } + + function _defineProperties(target, props) { + for (var i = 0; i < props.length; i++) { + var descriptor = props[i]; + descriptor.enumerable = descriptor.enumerable || false; + descriptor.configurable = true; + if ("value" in descriptor) descriptor.writable = true; + Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor); + } + } + function _createClass(Constructor, protoProps, staticProps) { + if (protoProps) _defineProperties(Constructor.prototype, protoProps); + if (staticProps) _defineProperties(Constructor, staticProps); + Object.defineProperty(Constructor, "prototype", { + writable: false + }); + return Constructor; + } + + function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } + function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } + /* eslint-disable max-classes-per-file */ + var Storage = /*#__PURE__*/function () { + function Storage(options) { + _classCallCheck(this, Storage); + this.store = options.store; + } + _createClass(Storage, [{ + key: "setItem", + value: function setItem(key, value) { + if (this.store) { + try { + this.store.setItem(key, value); + } catch (e) { + // f.log('failed to set value for key "' + key + '" to localStorage.'); + } + } + } + }, { + key: "getItem", + value: function getItem(key, value) { + if (this.store) { + try { + return this.store.getItem(key, value); + } catch (e) { + // f.log('failed to get value for key "' + key + '" from localStorage.'); + } + } + return undefined; + } + }]); + return Storage; + }(); + function getDefaults() { + var store = null; + try { + store = window.localStorage; + } catch (e) { + if (typeof window !== 'undefined') { + console.log('Failed to load local storage.', e); + } + } + return { + prefix: 'i18next_res_', + expirationTime: 7 * 24 * 60 * 60 * 1000, + defaultVersion: undefined, + versions: {}, + store: store + }; + } + var Cache = /*#__PURE__*/function () { + function Cache(services) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + _classCallCheck(this, Cache); + this.init(services, options); + this.type = 'backend'; + } + _createClass(Cache, [{ + key: "init", + value: function init(services) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + this.services = services; + this.options = _objectSpread(_objectSpread(_objectSpread({}, getDefaults()), this.options), options); + this.storage = new Storage(this.options); + } + }, { + key: "read", + value: function read(language, namespace, callback) { + var nowMS = Date.now(); + if (!this.storage.store) { + return callback(null, null); + } + var local = this.storage.getItem("".concat(this.options.prefix).concat(language, "-").concat(namespace)); + if (local) { + local = JSON.parse(local); + var version = this.getVersion(language); + if ( + // expiration field is mandatory, and should not be expired + local.i18nStamp && local.i18nStamp + this.options.expirationTime > nowMS && + // there should be no language version set, or if it is, it should match the one in translation + version === local.i18nVersion) { + var i18nStamp = local.i18nStamp; + delete local.i18nVersion; + delete local.i18nStamp; + return callback(null, local, i18nStamp); + } + } + return callback(null, null); + } + }, { + key: "save", + value: function save(language, namespace, data) { + if (this.storage.store) { + data.i18nStamp = Date.now(); + + // language version (if set) + var version = this.getVersion(language); + if (version) { + data.i18nVersion = version; + } + + // save + this.storage.setItem("".concat(this.options.prefix).concat(language, "-").concat(namespace), JSON.stringify(data)); + } + } + }, { + key: "getVersion", + value: function getVersion(language) { + return this.options.versions[language] || this.options.defaultVersion; + } + }]); + return Cache; + }(); + Cache.type = 'backend'; + + return Cache; + +})); diff --git a/static/vendor/i18next/jquery-i18next.js b/static/vendor/i18next/jquery-i18next.js new file mode 100644 index 00000000..e8118cb5 --- /dev/null +++ b/static/vendor/i18next/jquery-i18next.js @@ -0,0 +1,128 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global.jqueryI18next = factory()); +}(this, (function () { 'use strict'; + +var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; + +var defaults = { + tName: 't', + i18nName: 'i18n', + handleName: 'localize', + selectorAttr: 'data-i18n', + targetAttr: 'i18n-target', + optionsAttr: 'i18n-options', + useOptionsAttr: false, + parseDefaultValueFromContent: true +}; + +function init(i18next, $) { + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + + + options = _extends({}, defaults, options); + + function parse(ele, key, opts) { + if (key.length === 0) return; + + var attr = 'text'; + + if (key.indexOf('[') === 0) { + var parts = key.split(']'); + key = parts[1]; + attr = parts[0].substr(1, parts[0].length - 1); + } + + if (key.indexOf(';') === key.length - 1) { + key = key.substr(0, key.length - 2); + } + + function extendDefault(o, val) { + if (!options.parseDefaultValueFromContent) return o; + return _extends({}, o, { defaultValue: val }); + } + + if (attr === 'html') { + ele.html(i18next.t(key, extendDefault(opts, ele.html()))); + } else if (attr === 'text') { + ele.text(i18next.t(key, extendDefault(opts, ele.text()))); + } else if (attr === 'prepend') { + ele.prepend(i18next.t(key, extendDefault(opts, ele.html()))); + } else if (attr === 'append') { + ele.append(i18next.t(key, extendDefault(opts, ele.html()))); + } else if (attr.indexOf('data-') === 0) { + var dataAttr = attr.substr('data-'.length); + var translated = i18next.t(key, extendDefault(opts, ele.data(dataAttr))); + + // we change into the data cache + ele.data(dataAttr, translated); + // we change into the dom + ele.attr(attr, translated); + } else { + ele.attr(attr, i18next.t(key, extendDefault(opts, ele.attr(attr)))); + } + } + + function localize(ele, opts) { + var key = ele.attr(options.selectorAttr); + if (!key && typeof key !== 'undefined' && key !== false) key = ele.text() || ele.val(); + if (!key) return; + + var target = ele, + targetSelector = ele.data(options.targetAttr); + + if (targetSelector) target = ele.find(targetSelector) || ele; + + if (!opts && options.useOptionsAttr === true) opts = ele.data(options.optionsAttr); + + opts = opts || {}; + + if (key.indexOf(';') >= 0) { + var keys = key.split(';'); + + $.each(keys, function (m, k) { + // .trim(): Trim the comma-separated parameters on the data-i18n attribute. + if (k !== '') parse(target, k.trim(), opts); + }); + } else { + parse(target, key, opts); + } + + if (options.useOptionsAttr === true) { + var clone = {}; + clone = _extends({ clone: clone }, opts); + + delete clone.lng; + ele.data(options.optionsAttr, clone); + } + } + + function handle(opts) { + return this.each(function () { + // localize element itself + localize($(this), opts); + + // localize children + var elements = $(this).find('[' + options.selectorAttr + ']'); + elements.each(function () { + localize($(this), opts); + }); + }); + }; + + // $.t $.i18n shortcut + $[options.tName] = i18next.t.bind(i18next); + $[options.i18nName] = i18next; + + // selector function $(mySelector).localize(opts); + $.fn[options.handleName] = handle; +} + +var index = { + init: init +}; + +return index; + +}))); \ No newline at end of file diff --git a/templates/common/base.html b/templates/common/base.html index 4aaeddef..e46f5937 100644 --- a/templates/common/base.html +++ b/templates/common/base.html @@ -14,14 +14,14 @@ therefore cannot be used in derivative works/products without an explicit grant from the SFTPGo Team (support@sftpgo.com). --> {{- define "errmsg"}} -
- +
+
- {{.}} +
+
@@ -69,9 +67,9 @@ explicit grant from the SFTPGo Team (support@sftpgo.com). KTApp.hidePageLoading(); if (!has_errors) { ModalAlert.fire({ - text: `File/s uploaded successfully`, + text: $.t('fs.upload.success'), icon: "success", - confirmButtonText: "OK", + confirmButtonText: $.t('general.ok'), customClass: { confirmButton: 'btn btn-primary' } @@ -90,13 +88,17 @@ explicit grant from the SFTPGo Team (support@sftpgo.com). try { lastModified = f.lastModified; } catch (e) { - console.log("unable to get last modified time from file: " + e.message); + console.error("unable to get last modified time from file: " + e.message); lastModified = ""; } let uploadTxt = f.name; if (files.length > 1){ - uploadTxt = `Upload ${index+1}/${files.length}: ${uploadTxt}`; + uploadTxt = $.t('fs.uploading', { + idx: index + 1, + total: files.length, + name: uploadTxt + }); } $('#loading_message').text(uploadTxt); @@ -123,18 +125,23 @@ explicit grant from the SFTPGo Team (support@sftpgo.com). success++; uploadFile(); }).catch(function (error) { - let errorMessage = "Error uploading files"; + let errorMessage; if (error && error.response) { - if (error.response.data.message) { - errorMessage = error.response.data.message; - } - if (error.response.data.error) { - errorMessage += ": " + error.response.data.error; + switch (error.response.status) { + case 403: + errorMessage = "fs.upload.err_403"; + break; + case 429: + errorMessage = "fs.upload.err_429"; + break; } } + if (!errorMessage){ + errorMessage = "fs.upload.err_generic"; + } index++; has_errors = true; - $('#errorTxt').text(errorMessage); + setI18NData($('#errorTxt'), errorMessage); $('#errorMsg').removeClass("d-none"); uploadFile(); }); diff --git a/templates/webclient/twofactor-recovery.html b/templates/webclient/twofactor-recovery.html index 09025e48..4db0c29b 100644 --- a/templates/webclient/twofactor-recovery.html +++ b/templates/webclient/twofactor-recovery.html @@ -15,8 +15,6 @@ explicit grant from the SFTPGo Team (support@sftpgo.com). --> {{- template "baselogin" .}} -{{- define "title"}}{{.Title}}{{- end}} - {{- define "content"}}
@@ -33,13 +31,14 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
{{template "errmsg" .Error}}
- +
@@ -53,7 +52,9 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
- You can enter one of your recovery codes in case you lost access to your mobile device. + + You can enter one of your recovery codes in case you lost access to your mobile device. +
diff --git a/templates/webclient/twofactor.html b/templates/webclient/twofactor.html index fdbdc9f4..0a9696bc 100644 --- a/templates/webclient/twofactor.html +++ b/templates/webclient/twofactor.html @@ -15,8 +15,6 @@ explicit grant from the SFTPGo Team (support@sftpgo.com). --> {{- template "baselogin" .}} -{{- define "title"}}{{.Title}}{{- end}} - {{- define "content"}}
@@ -33,13 +31,14 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
{{template "errmsg" .Error}}
- +
@@ -54,11 +53,13 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
- Open the two-factor authentication app on your device to view your authentication code and verify your identity. + + Open the two-factor authentication app on your device to view your authentication code and verify your identity. +
-

Having problems?

-

Enter a two-factor recovery code

+

Having problems?

+

Enter a two-factor recovery code

diff --git a/templates/webclient/viewpdf.html b/templates/webclient/viewpdf.html index 486605a8..db3d4b5f 100644 --- a/templates/webclient/viewpdf.html +++ b/templates/webclient/viewpdf.html @@ -18,7 +18,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com). - {{.Title}} + {{range .Branding.ExtraCSS}}