diff --git a/.env.dist b/.env.dist index 6a4ad2b..aa703e8 100644 --- a/.env.dist +++ b/.env.dist @@ -33,4 +33,3 @@ TZ=UTC #GOKAPI_DISABLE_CORS_CHECK=false #GOKAPI_LOG_STDOUT=false #GOKAPI_ENABLE_HOTLINK_VIDEOS=false -#DOCKER_NONROOT=false diff --git a/build/go-generate/minifyStaticContent.go b/build/go-generate/minifyStaticContent.go index 770e747..245ed46 100644 --- a/build/go-generate/minifyStaticContent.go +++ b/build/go-generate/minifyStaticContent.go @@ -137,6 +137,6 @@ func fileExists(filename string) bool { // Auto-generated content below, do not modify // Version codes can be changed in updateVersionNumbers.go -const jsAdminVersion = 12 +const jsAdminVersion = 13 const jsE2EVersion = 8 const cssMainVersion = 5 diff --git a/build/go-generate/updateVersionNumbers.go b/build/go-generate/updateVersionNumbers.go index f63e028..a16a016 100644 --- a/build/go-generate/updateVersionNumbers.go +++ b/build/go-generate/updateVersionNumbers.go @@ -11,7 +11,7 @@ import ( "strings" ) -const versionJsAdmin = 12 +const versionJsAdmin = 13 const versionJsDropzone = 5 const versionJsE2EAdmin = 8 const versionCssMain = 5 diff --git a/cmd/gokapi/Main.go b/cmd/gokapi/Main.go index 158004b..6244a3b 100644 --- a/cmd/gokapi/Main.go +++ b/cmd/gokapi/Main.go @@ -67,6 +67,7 @@ func main() { initCloudConfig(passedFlags) go storage.CleanUp(true) logging.LogStartup() + showDeprecationWarnings() go webserver.Start() c := make(chan os.Signal) @@ -107,6 +108,17 @@ func showVersion(passedFlags flagparser.MainFlags) { osExit(0) } +func showDeprecationWarnings() { + for _, dep := range configuration.Environment.ActiveDeprecations { + fmt.Println() + fmt.Println("WARNING, deprecated feature: " + dep.Name) + fmt.Println(dep.Description) + fmt.Println("See " + dep.DocUrl + " for more information.") + fmt.Println() + logging.LogDeprecation(dep) + } +} + func parseBuildSettings(infos []debug.BuildSetting) { lookups := make(map[string]string) lookups["-tags"] = "Build Tags" diff --git a/docker-compose.yaml b/docker-compose.yaml index b9f5f6c..cff62b2 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -11,8 +11,3 @@ services: restart: always env_file: - "./.env" - -volumes: - gokapi-data: - gokapi-config: - diff --git a/dockerentry.sh b/dockerentry.sh index 0df5749..025a395 100755 --- a/dockerentry.sh +++ b/dockerentry.sh @@ -1,5 +1,10 @@ #!/bin/sh +#DEPRECATED, see https://gokapi.readthedocs.io/en/latest/setup.html#migration-from-docker-nonroot-to-docker-user if [ "$DOCKER_NONROOT" = "true" ]; then + # TODO for the next major upgrade version: + # - Remove this code block and leave only exec /app/gokapi "@" + # - Remove gokapi user / group creation in Dockerfile + # - Remove su-exec installation from the Dockerfile echo "Setting permissions" && \ chown -R gokapi:gokapi /app && \ chmod -R 700 /app && \ diff --git a/docs/advanced.rst b/docs/advanced.rst index cfaaedb..e3f6f6e 100644 --- a/docs/advanced.rst +++ b/docs/advanced.rst @@ -94,7 +94,9 @@ Available environment variables | | | | | | | unlimited downloads enabled | | | +-------------------------------+-------------------------------------------------------------------------------------+-----------------+--------------------------------------+ -| DOCKER_NONROOT | Docker only: Runs the binary in the container as a non-root user, if set to "true" | No | false | +| DOCKER_NONROOT | DEPRECATED. See :ref:`setupdocker` section for non-root setup | No | false | +| | | | | +| | Docker only: Runs the binary in the container as a non-root user, if set to "true" | | | +-------------------------------+-------------------------------------------------------------------------------------+-----------------+--------------------------------------+ | TMPDIR | Sets the path which contains temporary files | No | Non-Docker: Default OS path | | | | | | diff --git a/docs/setup.rst b/docs/setup.rst index 3cde2ae..5a38a7f 100644 --- a/docs/setup.rst +++ b/docs/setup.rst @@ -66,6 +66,8 @@ Starting Gokapi ^^^^^^^^^^^^^^^^ +.. _setupdocker: + Docker """""""""" @@ -77,10 +79,45 @@ With the argument ``-p 127.0.0.1:53842:53842`` the service will only be accessib Set ``-e TZ=UTC`` to the timezone you are in, e.g. ``-e TZ=Europe/Berlin``. -If you do not want the binary to run as the root user in the container, you can set the environment variable ``DOCKER_NONROOT`` to true. - Please make sure that ``/app/data`` and ``/app/config`` are mounted as volumes (see example above), otherwise you will lose all your data after rebuilding or updating your container. +If you do not want the binary to run as the root user in the container, you can run it with ``--user`` option, like in the following example: :: + + docker run --user "1000:1000" -v ./gokapi-data:/app/data -v ./gokapi-config:/app/config -p 127.0.0.1:53842:53842 -e TZ=UTC f0rc3/gokapi:latest + +Where ``1000:1000`` are colon separated desired user ID and group ID. Please note the command uses bind mounts instead of named volumes. Make sure that the user has read / write permissions to the volumes directories. You can change the names of ``./gokapi-data`` and ``./gokapi-config`` directories to your liking. + + +.. _deprecation_nonroot: + +Migration from DOCKER_NONROOT to docker --user +*********************************************** + +With deprecation of ``DOCKER_NONROOT`` environment variable you may want to consider migration of your existing configuration and data to ``docker --user`` option approach. The steps are as follows: + +:: + + # Copy configuration and data from the container to your docker host machine. + # Make sure ./gokapi-config and ./gokapi-data directories are not present before + # the following commands are executed + docker cp gokapi:/app/config ./gokapi-config + docker cp gokapi:/app/data ./gokapi-data + + # Remove the current container + docker rm -f gokapi + + # Start a new container with the current linux session user + docker run --user "$(id -u):$(id -g)" -v ./gokapi-data:/app/data -v ./gokapi-config:/app/config -p 127.0.0.1:53842:53842 -e TZ=UTC f0rc3/gokapi:latest + +Where: + * ``gokapi`` is the container name. For you it can be different. + * ``/app/config`` and ``/app/data`` are directories inside container where configuration and data reside. Can be different, depending on your ``GOKAPI_CONFIG_DIR`` and ``GOKAPI_DATA_DIR`` settings. + + This example uses your current user ID and group ID for starting the container. To use a different IDs, replace ``$(id -u)`` with the actual user ID and ``$(id -g)`` with the actual group ID. + + + + Docker Compose """""""""""""""" @@ -92,6 +129,8 @@ By default, the container is set to always automatically (re)start when the syst Then, start the container with the command ``docker compose up -d`` + + Native Deployment """""""""""""""""" diff --git a/internal/environment/Environment.go b/internal/environment/Environment.go index 0a81124..9c81460 100644 --- a/internal/environment/Environment.go +++ b/internal/environment/Environment.go @@ -6,6 +6,7 @@ import ( "path" envParser "github.com/caarlos0/env/v6" + "github.com/forceu/gokapi/internal/environment/deprecation" "github.com/forceu/gokapi/internal/environment/flagparser" "github.com/forceu/gokapi/internal/helper" ) @@ -35,11 +36,21 @@ type Environment struct { AwsKeyId string `env:"AWS_KEY"` AwsKeySecret string `env:"AWS_KEY_SECRET"` AwsEndpoint string `env:"AWS_ENDPOINT"` + ActiveDeprecations []deprecation.Deprecation } // New parses the env variables func New() Environment { result := Environment{WebserverPort: DefaultPort} + + result = parseEnvVars(result) + result = parseFlags(result) + result.ActiveDeprecations = deprecation.GetActive() + + return result +} + +func parseEnvVars(result Environment) Environment { err := envParser.Parse(&result, envParser.Options{ Prefix: "GOKAPI_", }) @@ -50,6 +61,23 @@ func New() Environment { } helper.Check(err) + if result.LengthId < 5 { + result.LengthId = 5 + } + if result.LengthHotlinkId < 8 { + result.LengthHotlinkId = 8 + } + if result.MaxMemory < 5 { + result.MaxMemory = 5 + } + if result.MaxFileSize < 1 { + result.MaxFileSize = 5 + } + + return result +} + +func parseFlags(result Environment) Environment { flags := flagparser.ParseFlags() if flags.IsPortSet { result.WebserverPort = flags.Port @@ -67,8 +95,9 @@ func New() Environment { if flags.IsConfigPathSet { result.ConfigPath = flags.ConfigPath } + if IsDockerInstance() && os.Getenv("TMPDIR") == "" { - err = os.Setenv("TMPDIR", result.DataDir) + err := os.Setenv("TMPDIR", result.DataDir) helper.Check(err) } if result.LengthId < 5 { diff --git a/internal/environment/deprecation/Deprecations.go b/internal/environment/deprecation/Deprecations.go new file mode 100644 index 0000000..b8ca809 --- /dev/null +++ b/internal/environment/deprecation/Deprecations.go @@ -0,0 +1,49 @@ +package deprecation + +import ( + "os" + "strings" +) + +type Deprecation struct { + Id string + Name string + Description string + DocUrl string + checkFunction func() bool +} + +func (d *Deprecation) IsSet() bool { + if d.checkFunction == nil { + panic("checkFunction is nil") + } + return d.checkFunction() +} + +func GetActive() []Deprecation { + result := make([]Deprecation, 0) + for _, deprecation := range availableDeprecations { + if deprecation.IsSet() { + result = append(result, deprecation) + } + } + return result +} + +var availableDeprecations = []Deprecation{ + { + Id: "dockernonroot", + Name: "Docker Non-Root User", + Description: "Usage of DOCKER_NONROOT is deprecated in favor of docker --user option", + DocUrl: "https://gokapi.readthedocs.io/en/latest/setup.html#migration-from-docker-nonroot-to-docker-user", + checkFunction: isNonRootSet, + }, +} + +func isNonRootSet() bool { + envVal := os.Getenv("DOCKER_NONROOT") + if envVal == "" || strings.ToLower(envVal) == "false" { + return false + } + return true +} diff --git a/internal/logging/Logging.go b/internal/logging/Logging.go index 19a1d6b..1efc8ac 100644 --- a/internal/logging/Logging.go +++ b/internal/logging/Logging.go @@ -11,6 +11,7 @@ import ( "time" "github.com/forceu/gokapi/internal/environment" + "github.com/forceu/gokapi/internal/environment/deprecation" "github.com/forceu/gokapi/internal/helper" "github.com/forceu/gokapi/internal/models" ) @@ -139,6 +140,13 @@ func LogRestore(file models.File, user models.User) { createLogEntry(categoryEdit, fmt.Sprintf("%s, ID %s, restored by %s (user #%d)", file.Name, file.Id, user.Name, user.Id), false) } +// LogDeprecation adds a log entry to indicate that a deprecated feature is being used. Blocking +func LogDeprecation(dep deprecation.Deprecation) { + createLogEntry(categoryWarning, "Deprecated feature: "+dep.Name, true) + createLogEntry(categoryWarning, dep.Description, true) + createLogEntry(categoryWarning, "See "+dep.DocUrl+" for more information.", true) +} + // DeleteLogs removes all logs before the cutoff timestamp and inserts a new log that the user // deleted the previous logs func DeleteLogs(userName string, userId int, cutoff int64, r *http.Request) { diff --git a/internal/webserver/Webserver.go b/internal/webserver/Webserver.go index 2df31fa..8380434 100644 --- a/internal/webserver/Webserver.go +++ b/internal/webserver/Webserver.go @@ -581,14 +581,23 @@ func showAdminMenu(w http.ResponseWriter, r *http.Request) { panic(err) } - if configuration.Get().Encryption.Level == encryption.EndToEndEncryption { + config := configuration.Get() + if config.Encryption.Level == encryption.EndToEndEncryption { e2einfo := database.GetEnd2EndInfo(user.Id) if !e2einfo.HasBeenSetUp() { redirect(w, "e2eSetup") return } } - err = templateFolder.ExecuteTemplate(w, "admin", (&AdminView{}).convertGlobalConfig(ViewMain, user)) + + view := (&AdminView{}).convertGlobalConfig(ViewMain, user) + if len(configuration.Environment.ActiveDeprecations) > 0 { + if user.UserLevel == models.UserLevelSuperAdmin { + view.ShowDeprecationNotice = true + } + } + + err = templateFolder.ExecuteTemplate(w, "admin", view) helper.CheckIgnoreTimeout(err) } @@ -656,30 +665,31 @@ type e2ESetupView struct { // AdminView contains parameters for all admin related pages type AdminView struct { - Items []models.FileApiOutput - ApiKeys []models.ApiKey - Users []userInfo - ActiveUser models.User - UserMap map[int]*models.User - ServerUrl string - Logs string - PublicName string - SystemKey string - IsAdminView bool - IsDownloadView bool - IsApiView bool - IsLogoutAvailable bool - IsUserTabAvailable bool - EndToEndEncryption bool - IncludeFilename bool - IsInternalAuth bool - MaxFileSize int - ActiveView int - ChunkSize int - MaxParallelUploads int - MinLengthPassword int - TimeNow int64 - CustomContent customStatic + Items []models.FileApiOutput + ApiKeys []models.ApiKey + Users []userInfo + ActiveUser models.User + UserMap map[int]*models.User + ServerUrl string + Logs string + PublicName string + SystemKey string + IsAdminView bool + IsDownloadView bool + IsApiView bool + IsLogoutAvailable bool + IsUserTabAvailable bool + EndToEndEncryption bool + IncludeFilename bool + IsInternalAuth bool + ShowDeprecationNotice bool + MaxFileSize int + ActiveView int + ChunkSize int + MaxParallelUploads int + MinLengthPassword int + TimeNow int64 + CustomContent customStatic } // getUserMap needs to return the map with pointers, otherwise template cannot call diff --git a/internal/webserver/web/static/css/cover.css b/internal/webserver/web/static/css/cover.css index 08ddcdd..9aea819 100644 --- a/internal/webserver/web/static/css/cover.css +++ b/internal/webserver/web/static/css/cover.css @@ -174,6 +174,10 @@ a:hover { z-index: 9999; } +.toastdeprecation { + background-color: #8b0000; +} + .toastnotification.show { opacity: 1; pointer-events: auto; diff --git a/internal/webserver/web/static/css/min/gokapi.min.5.css b/internal/webserver/web/static/css/min/gokapi.min.5.css index 9c6edf8..74b4682 100644 --- a/internal/webserver/web/static/css/min/gokapi.min.5.css +++ b/internal/webserver/web/static/css/min/gokapi.min.5.css @@ -1 +1 @@ -.btn-secondary,.btn-secondary:hover,.btn-secondary:focus{color:#333;text-shadow:none}body{background:url(../../assets/background.jpg)no-repeat 50% fixed;-webkit-background-size:cover;-moz-background-size:cover;-o-background-size:cover;background-size:cover;display:-ms-flexbox;display:-webkit-box;display:flex;-ms-flex-pack:center;-webkit-box-pack:center;justify-content:center}td{vertical-align:middle;position:relative}a{color:inherit}a:hover{color:inherit;filter:brightness(80%)}.dropzone{background:#2f343a!important;color:#fff;border-radius:5px}.dropzone:hover{background:#33393f!important;color:#fff;border-radius:5px}.card{margin:0 auto;float:none;margin-bottom:10px;border:2px solid #33393f}.card-body{background-color:#212529;color:#ddd}.card-title{font-weight:900}.admin-input{text-align:center}.form-control:disabled{background:#bababa}.break{flex-basis:100%;height:0}.bd-placeholder-img{font-size:1.125rem;text-anchor:middle;-webkit-user-select:none;-moz-user-select:none;user-select:none}@media(min-width:768px){.bd-placeholder-img-lg{font-size:3.5rem}.break{flex-basis:0}}.masthead{margin-bottom:2rem}.masthead-brand{margin-bottom:0}.nav-masthead .nav-link{padding:.25rem 0;font-weight:700;color:rgba(255,255,255,.5);background-color:transparent;border-bottom:.25rem solid transparent}.nav-masthead .nav-link:hover,.nav-masthead .nav-link:focus{border-bottom-color:rgba(255,255,255,.25)}.nav-masthead .nav-link+.nav-link{margin-left:1rem}.nav-masthead .active{color:#fff;border-bottom-color:#fff}#qroverlay{display:none;position:fixed;top:0;left:0;width:100%;height:100%;background-color:rgba(0,0,0,.3)}#qrcode{position:absolute;top:50%;left:50%;margin-top:-105px;margin-left:-105px;width:210px;height:210px;border:5px solid #fff}.toastnotification{pointer-events:none;position:fixed;bottom:20px;left:50%;transform:translateX(-50%);background-color:#333;color:#fff;padding:15px;border-radius:5px;box-shadow:0 2px 5px rgba(0,0,0,.3);opacity:0;transition:opacity .3s ease-in-out;z-index:9999}.toastnotification.show{opacity:1;pointer-events:auto}.toast-undo{margin-left:20px;color:#4fc3f7;cursor:pointer;text-decoration:underline;font-weight:700;pointer-events:auto}.toast-undo:hover{color:#81d4fa}.toastnotification:not(.show){pointer-events:none!important}.toastnotification:not(.show) .toast-undo{pointer-events:none}.perm-granted{cursor:pointer;color:#0edf00}.perm-notgranted{cursor:pointer;color:#9f9999}.perm-unavailable{color:#525252}.perm-processing{pointer-events:none;color:#e5eb00}.perm-nochange{cursor:default}.prevent-select{-webkit-user-select:none;-ms-user-select:none;user-select:none}.gokapi-dialog{background-color:#212529;color:#ddd}@keyframes subtleHighlight{0%{background-color:#444950}100%{background-color:transparent}}@keyframes subtleHighlightNewJson{0%{background-color:green}100%{background-color:transparent}}.updatedDownloadCount{animation:subtleHighlight .5s ease-out}.newApiKey{animation:subtleHighlightNewJson .7s ease-out}.newUser{animation:subtleHighlightNewJson .7s ease-out}.newItem{animation:subtleHighlightNewJson 1.5s ease-out}@keyframes fadeOut{0%{opacity:1}100%{opacity:0}}.rowDeleting{animation:fadeOut .3s ease-out forwards}.highlighted-password{background-color:#444;color:#ddd;padding:2px 6px;border-radius:4px;font-weight:700;font-family:monospace;display:inline-block;margin-left:8px;border:1px solid #555}.filename{font-weight:700;font-size:14px;margin-bottom:5px}.upload-progress-container{display:flex;align-items:center}.upload-progress-bar{position:relative;height:10px;background-color:#eee;flex:1;margin-right:10px;border-radius:4px}.upload-progress-bar-progress{position:absolute;top:0;left:0;height:100%;background-color:#0a0;border-radius:4px;transition:width .2s ease-in-out}.upload-progress-info{font-size:12px}.us-container{margin-top:10px;margin-bottom:20px}.uploaderror{font-weight:700;color:red;margin-bottom:5px}.uploads-container{background-color:#2f343a;border:2px solid rgba(0,0,0,.3);border-radius:5px;margin-left:0;margin-right:0;max-width:none;visibility:hidden} \ No newline at end of file +.btn-secondary,.btn-secondary:hover,.btn-secondary:focus{color:#333;text-shadow:none}body{background:url(../../assets/background.jpg)no-repeat 50% fixed;-webkit-background-size:cover;-moz-background-size:cover;-o-background-size:cover;background-size:cover;display:-ms-flexbox;display:-webkit-box;display:flex;-ms-flex-pack:center;-webkit-box-pack:center;justify-content:center}td{vertical-align:middle;position:relative}a{color:inherit}a:hover{color:inherit;filter:brightness(80%)}.dropzone{background:#2f343a!important;color:#fff;border-radius:5px}.dropzone:hover{background:#33393f!important;color:#fff;border-radius:5px}.card{margin:0 auto;float:none;margin-bottom:10px;border:2px solid #33393f}.card-body{background-color:#212529;color:#ddd}.card-title{font-weight:900}.admin-input{text-align:center}.form-control:disabled{background:#bababa}.break{flex-basis:100%;height:0}.bd-placeholder-img{font-size:1.125rem;text-anchor:middle;-webkit-user-select:none;-moz-user-select:none;user-select:none}@media(min-width:768px){.bd-placeholder-img-lg{font-size:3.5rem}.break{flex-basis:0}}.masthead{margin-bottom:2rem}.masthead-brand{margin-bottom:0}.nav-masthead .nav-link{padding:.25rem 0;font-weight:700;color:rgba(255,255,255,.5);background-color:transparent;border-bottom:.25rem solid transparent}.nav-masthead .nav-link:hover,.nav-masthead .nav-link:focus{border-bottom-color:rgba(255,255,255,.25)}.nav-masthead .nav-link+.nav-link{margin-left:1rem}.nav-masthead .active{color:#fff;border-bottom-color:#fff}#qroverlay{display:none;position:fixed;top:0;left:0;width:100%;height:100%;background-color:rgba(0,0,0,.3)}#qrcode{position:absolute;top:50%;left:50%;margin-top:-105px;margin-left:-105px;width:210px;height:210px;border:5px solid #fff}.toastnotification{pointer-events:none;position:fixed;bottom:20px;left:50%;transform:translateX(-50%);background-color:#333;color:#fff;padding:15px;border-radius:5px;box-shadow:0 2px 5px rgba(0,0,0,.3);opacity:0;transition:opacity .3s ease-in-out;z-index:9999}.toastdeprecation{background-color:#8b0000}.toastnotification.show{opacity:1;pointer-events:auto}.toast-undo{margin-left:20px;color:#4fc3f7;cursor:pointer;text-decoration:underline;font-weight:700;pointer-events:auto}.toast-undo:hover{color:#81d4fa}.toastnotification:not(.show){pointer-events:none!important}.toastnotification:not(.show) .toast-undo{pointer-events:none}.perm-granted{cursor:pointer;color:#0edf00}.perm-notgranted{cursor:pointer;color:#9f9999}.perm-unavailable{color:#525252}.perm-processing{pointer-events:none;color:#e5eb00}.perm-nochange{cursor:default}.prevent-select{-webkit-user-select:none;-ms-user-select:none;user-select:none}.gokapi-dialog{background-color:#212529;color:#ddd}@keyframes subtleHighlight{0%{background-color:#444950}100%{background-color:transparent}}@keyframes subtleHighlightNewJson{0%{background-color:green}100%{background-color:transparent}}.updatedDownloadCount{animation:subtleHighlight .5s ease-out}.newApiKey{animation:subtleHighlightNewJson .7s ease-out}.newUser{animation:subtleHighlightNewJson .7s ease-out}.newItem{animation:subtleHighlightNewJson 1.5s ease-out}@keyframes fadeOut{0%{opacity:1}100%{opacity:0}}.rowDeleting{animation:fadeOut .3s ease-out forwards}.highlighted-password{background-color:#444;color:#ddd;padding:2px 6px;border-radius:4px;font-weight:700;font-family:monospace;display:inline-block;margin-left:8px;border:1px solid #555}.filename{font-weight:700;font-size:14px;margin-bottom:5px}.upload-progress-container{display:flex;align-items:center}.upload-progress-bar{position:relative;height:10px;background-color:#eee;flex:1;margin-right:10px;border-radius:4px}.upload-progress-bar-progress{position:absolute;top:0;left:0;height:100%;background-color:#0a0;border-radius:4px;transition:width .2s ease-in-out}.upload-progress-info{font-size:12px}.us-container{margin-top:10px;margin-bottom:20px}.uploaderror{font-weight:700;color:red;margin-bottom:5px}.uploads-container{background-color:#2f343a;border:2px solid rgba(0,0,0,.3);border-radius:5px;margin-left:0;margin-right:0;max-width:none;visibility:hidden} \ No newline at end of file diff --git a/internal/webserver/web/static/js/admin_ui_upload.js b/internal/webserver/web/static/js/admin_ui_upload.js index d5e223d..885c4d5 100644 --- a/internal/webserver/web/static/js/admin_ui_upload.js +++ b/internal/webserver/web/static/js/admin_ui_upload.js @@ -966,6 +966,7 @@ function handleUndo(button) { }); } + function shareUrl(id) { if (!navigator.share) { return; @@ -977,3 +978,13 @@ function shareUrl(id) { url: url, }) } + + +function showDeprecationNotice() { + let notification = document.getElementById("toastDeprecation"); + notification.classList.add("show"); + setTimeout(() => { + notification.classList.remove("show"); + }, 5000); +} + diff --git a/internal/webserver/web/static/js/min/admin.min.12.js b/internal/webserver/web/static/js/min/admin.min.13.js similarity index 85% rename from internal/webserver/web/static/js/min/admin.min.12.js rename to internal/webserver/web/static/js/min/admin.min.13.js index acc81bf..da19a92 100644 --- a/internal/webserver/web/static/js/min/admin.min.12.js +++ b/internal/webserver/web/static/js/min/admin.min.13.js @@ -1,6 +1,6 @@ async function apiAuthModify(e,t,n){const s="./api/auth/modify",o={method:"POST",headers:{"Content-Type":"application/json",apikey:systemKey,targetKey:e,permission:t,permissionModifier:n}};try{const e=await fetch(s,o);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`)}catch(e){throw console.error("Error in apiAuthModify:",e),e}}async function apiAuthFriendlyName(e,t){const n="./api/auth/friendlyname",s={method:"PUT",headers:{"Content-Type":"application/json",apikey:systemKey,targetKey:e,friendlyName:t}};try{const e=await fetch(n,s);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`)}catch(e){throw console.error("Error in apiAuthModify:",e),e}}async function apiAuthDelete(e){const t="./api/auth/delete",n={method:"POST",headers:{"Content-Type":"application/json",apikey:systemKey,targetKey:e}};try{const e=await fetch(t,n);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`)}catch(e){throw console.error("Error in apiAuthDelete:",e),e}}async function apiAuthCreate(){const e="./api/auth/create",t={method:"POST",headers:{"Content-Type":"application/json",apikey:systemKey,basicPermissions:"true"}};try{const n=await fetch(e,t);if(!n.ok)throw new Error(`Request failed with status: ${n.status}`);const s=await n.json();return s}catch(e){throw console.error("Error in apiAuthCreate:",e),e}}async function apiChunkComplete(e,t,n,s,o,i,a,r,c,l){const d="./api/chunk/complete",u={method:"POST",headers:{"Content-Type":"application/json",apikey:systemKey,uuid:e,filename:"base64:"+Base64.encode(t),filesize:n,realsize:s,contenttype:o,allowedDownloads:i,expiryDays:a,password:r,isE2E:c,nonblocking:l}};try{const e=await fetch(d,u);if(!e.ok){let t;try{const n=await e.json();t=n.ErrorMessage||`Request failed with status: ${e.status}`}catch{const n=await e.text();t=n||`Request failed with status: ${e.status}`}throw new Error(t)}const t=await e.json();return t}catch(e){throw console.error("Error in apiChunkComplete:",e),e}}async function apiFilesReplace(e,t){const n="./api/files/replace",s={method:"PUT",headers:{"Content-Type":"application/json",id:e,apikey:systemKey,idNewContent:t,deleteNewFile:!1}};try{const e=await fetch(n,s);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`);const t=await e.json();return t}catch(e){throw console.error("Error in apiFilesReplace:",e),e}}async function apiFilesListById(e){const t="./api/files/list/"+e,n={method:"GET",headers:{"Content-Type":"application/json",apikey:systemKey}};try{const e=await fetch(t,n);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`);const s=await e.json();return s}catch(e){throw console.error("Error in apiFilesListById:",e),e}}async function apiFilesModify(e,t,n,s,o){const i="./api/files/modify",a={method:"PUT",headers:{"Content-Type":"application/json",id:e,apikey:systemKey,allowedDownloads:t,expiryTimestamp:n,password:s,originalPassword:o}};try{const e=await fetch(i,a);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`);const t=await e.json();return t}catch(e){throw console.error("Error in apiFilesModify:",e),e}}async function apiFilesDelete(e,t){const n="./api/files/delete",s={method:"POST",headers:{"Content-Type":"application/json",apikey:systemKey,id:e,delay:t}};try{const e=await fetch(n,s);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`)}catch(e){throw console.error("Error in apiFilesDelete:",e),e}}async function apiFilesRestore(e){const t="./api/files/restore",n={method:"POST",headers:{"Content-Type":"application/json",apikey:systemKey,id:e}};try{const e=await fetch(t,n);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`);const s=await e.json();return s}catch(e){throw console.error("Error in apiFilesRestore:",e),e}}async function apiUserCreate(e){const t="./api/user/create",n={method:"POST",headers:{"Content-Type":"application/json",apikey:systemKey,username:e}};try{const e=await fetch(t,n);if(!e.ok)throw e.status==409?new Error("duplicate"):new Error(`Request failed with status: ${e.status}`);const s=await e.json();return s}catch(e){throw console.error("Error in apiUserModify:",e),e}}async function apiUserModify(e,t,n){const s="./api/user/modify",o={method:"POST",headers:{"Content-Type":"application/json",apikey:systemKey,userid:e,userpermission:t,permissionModifier:n}};try{const e=await fetch(s,o);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`)}catch(e){throw console.error("Error in apiUserModify:",e),e}}async function apiUserChangeRank(e,t){const n="./api/user/changeRank",s={method:"POST",headers:{"Content-Type":"application/json",apikey:systemKey,userid:e,newRank:t}};try{const e=await fetch(n,s);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`)}catch(e){throw console.error("Error in apiUserModify:",e),e}}async function apiUserDelete(e,t){const n="./api/user/delete",s={method:"POST",headers:{"Content-Type":"application/json",apikey:systemKey,userid:e,deleteFiles:t}};try{const e=await fetch(n,s);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`)}catch(e){throw console.error("Error in apiUserDelete:",e),e}}async function apiUserResetPassword(e,t){const n="./api/user/resetPassword",s={method:"POST",headers:{"Content-Type":"application/json",apikey:systemKey,userid:e,generateNewPassword:t}};try{const e=await fetch(n,s);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`);const t=await e.json();return t}catch(e){throw console.error("Error in apiUserResetPassword:",e),e}}async function apiLogsDelete(e){const t="./api/logs/delete",n={method:"POST",headers:{"Content-Type":"application/json",apikey:systemKey,timestamp:e}};try{const e=await fetch(t,n);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`)}catch(e){throw console.error("Error in apiLogsDelete:",e),e}}async function apiE2eGet(){const e="./api/e2e/get",t={method:"POST",headers:{"Content-Type":"application/json",apikey:systemKey}};try{const n=await fetch(e,t);if(!n.ok)throw new Error(`Request failed with status: ${n.status}`);return await n.text()}catch(e){throw console.error("Error in apiE2eGet:",e),e}}async function apiE2eStore(e){const t="./api/e2e/set",n={method:"POST",headers:{"Content-Type":"application/json",apikey:systemKey},body:JSON.stringify({content:e})};try{const e=await fetch(t,n);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`)}catch(e){throw console.error("Error in apiE2eStore:",e),e}}try{var toastId,dropzoneObject,isE2EEnabled,isUploading,rowCount,calendarInstance,statusItemCount,clipboard=new ClipboardJS(".copyurl")}catch{}function showToast(e,t){let n=document.getElementById("toastnotification");typeof t!="undefined"?n.innerText=t:n.innerText=n.dataset.default,n.classList.add("show"),clearTimeout(toastId),toastId=setTimeout(()=>{hideToast()},e)}function hideToast(){document.getElementById("toastnotification").classList.remove("show")}function changeApiPermission(e,t,n){var o,i,s=document.getElementById(n);if(s.classList.contains("perm-processing")||s.classList.contains("perm-nochange"))return;o=s.classList.contains("perm-granted"),s.classList.add("perm-processing"),s.classList.remove("perm-granted"),s.classList.remove("perm-notgranted"),i="GRANT",o&&(i="REVOKE"),apiAuthModify(e,t,i).then(e=>{o?s.classList.add("perm-notgranted"):s.classList.add("perm-granted"),s.classList.remove("perm-processing")}).catch(e=>{o?s.classList.add("perm-granted"):s.classList.add("perm-notgranted"),s.classList.remove("perm-processing"),alert("Unable to set permission: "+e),console.error("Error:",e)})}function deleteApiKey(e){document.getElementById("delete-"+e).disabled=!0,apiAuthDelete(e).then(t=>{document.getElementById("row-"+e).classList.add("rowDeleting"),setTimeout(()=>{document.getElementById("row-"+e).remove()},290)}).catch(e=>{alert("Unable to delete API key: "+e),console.error("Error:",e)})}function newApiKey(){document.getElementById("button-newapi").disabled=!0,apiAuthCreate().then(e=>{addRowApi(e.Id,e.PublicId),document.getElementById("button-newapi").disabled=!1}).catch(e=>{alert("Unable to create API key: "+e),console.error("Error:",e)})}function addFriendlyNameChange(e){let t=document.getElementById("friendlyname-"+e);if(t.classList.contains("isBeingEdited"))return;t.classList.add("isBeingEdited");let i=t.innerText,n=document.createElement("input");n.size=5,n.value=i;let s=!0,o=function(){if(!s)return;s=!1;let o=n.value;o==""&&(o="Unnamed key"),t.innerText=o,t.classList.remove("isBeingEdited"),apiAuthFriendlyName(e,o).catch(e=>{alert("Unable to save name: "+e),console.error("Error:",e)})};n.onblur=o,n.addEventListener("keyup",function(e){e.keyCode===13&&(e.preventDefault(),o())}),t.innerText="",t.appendChild(n),n.focus()}function addRowApi(e,t){let p=document.getElementById("apitable"),s=p.insertRow(0);s.id="row-"+t;let i=0,c=s.insertCell(i++),l=s.insertCell(i++),d=s.insertCell(i++),a=s.insertCell(i++),u;canViewOtherApiKeys&&(u=s.insertCell(i++));let h=s.insertCell(i++);canViewOtherApiKeys&&(u.classList.add("newApiKey"),u.innerText=userName),c.classList.add("newApiKey"),l.classList.add("newApiKey"),d.classList.add("newApiKey"),a.classList.add("newApiKey"),a.classList.add("prevent-select"),h.classList.add("newApiKey"),c.innerText="Unnamed key",c.id="friendlyname-"+t,c.onclick=function(){addFriendlyNameChange(t)},l.innerText=e,l.classList.add("font-monospace"),d.innerText="Never";const r=document.createElement("div");r.className="btn-group",r.setAttribute("role","group");const n=document.createElement("button");n.type="button",n.dataset.clipboardText=e,n.title="Copy API Key",n.className="copyurl btn btn-outline-light btn-sm",n.setAttribute("onclick","showToast(1000)");const m=document.createElement("i");m.className="bi bi-copy",n.appendChild(m);const o=document.createElement("button");o.type="button",o.id=`delete-${t}`,o.title="Delete",o.className="btn btn-outline-danger btn-sm",o.setAttribute("onclick",`deleteApiKey('${t}')`);const f=document.createElement("i");f.className="bi bi-trash3",o.appendChild(f),r.appendChild(n),r.appendChild(o),h.appendChild(r);const g=[{perm:"PERM_VIEW",icon:"bi-eye",granted:!0,title:"List Uploads"},{perm:"PERM_UPLOAD",icon:"bi-file-earmark-arrow-up",granted:!0,title:"Upload"},{perm:"PERM_EDIT",icon:"bi-pencil",granted:!0,title:"Edit Uploads"},{perm:"PERM_DELETE",icon:"bi-trash3",granted:!0,title:"Delete Uploads"},{perm:"PERM_REPLACE",icon:"bi-recycle",granted:!1,title:"Replace Uploads"},{perm:"PERM_MANAGE_USERS",icon:"bi-people",granted:!1,title:"Manage Users"},{perm:"PERM_MANAGE_LOGS",icon:"bi-card-list",granted:!1,title:"Manage System Logs"},{perm:"PERM_API_MOD",icon:"bi-sliders2",granted:!1,title:"Manage API Keys"}];if(g.forEach(({perm:e,icon:n,granted:s,title:o})=>{const i=document.createElement("i"),r=`perm_${e.toLowerCase().replace("perm_","")}_${t}`;i.id=r,i.className=`bi ${n} ${s?"perm-granted":"perm-notgranted"}`,i.title=o,i.setAttribute("onclick",`changeApiPermission("${t}","${e}", "${r}");`),a.appendChild(i),a.appendChild(document.createTextNode(" "))}),!canReplaceFiles){let e=document.getElementById("perm_replace_"+t);e.classList.add("perm-unavailable"),e.classList.add("perm-nochange")}if(!canManageUsers){let e=document.getElementById("perm_users_"+t);e.classList.add("perm-unavailable"),e.classList.add("perm-nochange")}setTimeout(()=>{c.classList.remove("newApiKey"),l.classList.remove("newApiKey"),d.classList.remove("newApiKey"),a.classList.remove("newApiKey"),h.classList.remove("newApiKey")},700)}function filterLogs(e){e=="all"?textarea.value=logContent:textarea.value=logContent.split(` `).filter(t=>t.includes("["+e+"]")).join(` -`),textarea.scrollTop=textarea.scrollHeight}function deleteLogs(e){if(e=="none")return;if(!confirm("Do you want to delete the selected logs?")){document.getElementById("deleteLogs").selectedIndex=0;return}let t=Math.floor(Date.now()/1e3);switch(e){case"all":t=0;break;case"2":t=t-2*24*60*60;break;case"7":t=t-7*24*60*60;break;case"14":t=t-14*24*60*60;break;case"30":t=t-30*24*60*60;break}apiLogsDelete(t).then(e=>{location.reload()}).catch(e=>{alert("Unable to delete logs: "+e),console.error("Error:",e)})}isE2EEnabled=!1,isUploading=!1,rowCount=-1;function initDropzone(){Dropzone.options.uploaddropzone={paramName:"file",dictDefaultMessage:"Drop files, paste or click here to upload",createImageThumbnails:!1,chunksUploaded:function(e,t){sendChunkComplete(e,t)},init:function(){dropzoneObject=this,this.on("addedfile",e=>{saveUploadDefaults(),addFileProgress(e)}),this.on("queuecomplete",function(){isUploading=!1}),this.on("sending",function(){isUploading=!0}),this.on("error",function(e,t,n){n&&n.status===413?showError(e,"File too large to upload. If you are using a reverse proxy, make sure that the allowed body size is at least 70MB."):showError(e,"Error: "+t)}),this.on("uploadprogress",function(e,t,n){updateProgressbar(e,t,n)}),isE2EEnabled&&(dropzoneObject.disable(),dropzoneObject.options.dictDefaultMessage="Loading end-to-end encryption...",document.getElementsByClassName("dz-button")[0].innerText="Loading end-to-end encryption...",setE2eUpload())}},document.onpaste=function(e){if(dropzoneObject.disabled)return;const n=document.activeElement;if(n&&(n.hasAttribute("data-allow-regular-paste")||n.hasAttribute("placeholder")))return;var t,s=(e.clipboardData||e.originalEvent.clipboardData).items;for(let e in s)t=s[e],t.kind==="file"&&dropzoneObject.addFile(t.getAsFile()),t.kind==="string"&&t.getAsString(function(e){const t=//gi;if(t.test(e)===!1){let t=new Blob([e],{type:"text/plain"}),n=new File([t],"Pasted Text.txt",{type:"text/plain",lastModified:new Date(0)});dropzoneObject.addFile(n)}})},window.addEventListener("beforeunload",e=>{isUploading&&(e.returnValue="Upload is still in progress. Do you want to close this page?")})}function updateProgressbar(e,t,n){let o=e.upload.uuid,i=document.getElementById(`us-container-${o}`);if(i==null||i.getAttribute("data-complete")==="true")return;let s=Math.round(t);s<0&&(s=0),s>100&&(s=100);let r=Date.now()-i.getAttribute("data-starttime"),c=n/(r/1e3)/1024/1024;document.getElementById(`us-progressbar-${o}`).style.width=s+"%";let a=Math.round(c*10)/10;Number.isNaN(a)||(document.getElementById(`us-progress-info-${o}`).innerText=s+"% - "+a+"MB/s")}function addFileProgress(e){addFileStatus(e.upload.uuid,e.upload.filename)}function setUploadDefaults(){let s=getLocalStorageWithDefault("defaultDownloads",1),o=getLocalStorageWithDefault("defaultExpiry",14),e=getLocalStorageWithDefault("defaultPassword",""),t=getLocalStorageWithDefault("defaultUnlimitedDownloads",!1)==="true",n=getLocalStorageWithDefault("defaultUnlimitedTime",!1)==="true";document.getElementById("allowedDownloads").value=s,document.getElementById("expiryDays").value=o,document.getElementById("password").value=e,document.getElementById("enableDownloadLimit").checked=!t,document.getElementById("enableTimeLimit").checked=!n,e===""?(document.getElementById("enablePassword").checked=!1,document.getElementById("password").disabled=!0):(document.getElementById("enablePassword").checked=!0,document.getElementById("password").disabled=!1),t&&(document.getElementById("allowedDownloads").disabled=!0),n&&(document.getElementById("expiryDays").disabled=!0)}function saveUploadDefaults(){localStorage.setItem("defaultDownloads",document.getElementById("allowedDownloads").value),localStorage.setItem("defaultExpiry",document.getElementById("expiryDays").value),localStorage.setItem("defaultPassword",document.getElementById("password").value),localStorage.setItem("defaultUnlimitedDownloads",!document.getElementById("enableDownloadLimit").checked),localStorage.setItem("defaultUnlimitedTime",!document.getElementById("enableTimeLimit").checked)}function getLocalStorageWithDefault(e,t){var n=localStorage.getItem(e);return n===null?t:n}function urlencodeFormData(e){let t="";function s(e){return encodeURIComponent(e).replace(/%20/g,"+")}for(var n of e.entries())typeof n[1]=="string"&&(t+=(t?"&":"")+s(n[0])+"="+s(n[1]));return t}function sendChunkComplete(e,t){let c=e.upload.uuid,n=e.name,s=e.size,l=e.size,o=e.type,i=document.getElementById("allowedDownloads").value,a=document.getElementById("expiryDays").value,d=document.getElementById("password").value,r=e.isEndToEndEncrypted===!0,u=!0;document.getElementById("enableDownloadLimit").checked||(i=0),document.getElementById("enableTimeLimit").checked||(a=0),r&&(s=e.sizeEncrypted,n="Encrypted File",o=""),apiChunkComplete(c,n,s,l,o,i,a,d,r,u).then(n=>{t();let s=document.getElementById(`us-progress-info-${e.upload.uuid}`);s!=null&&(s.innerText="In Queue...")}).catch(t=>{console.error("Error:",t),dropzoneUploadError(e,t)})}function dropzoneUploadError(e,t){e.accepted=!1,dropzoneObject._errorProcessing([e],t),showError(e,t)}function dropzoneGetFile(e){for(let t=0;t{addRow(n);let s=dropzoneGetFile(t);if(s==null)return;if(s.isEndToEndEncrypted===!0){try{let o=GokapiE2EAddFile(t,e,s.name);if(o instanceof Error)throw o;let n=GokapiE2EInfoEncrypt();if(n instanceof Error)throw n;storeE2EInfo(n)}catch(e){s.accepted=!1,dropzoneObject._errorProcessing([s],e);return}GokapiE2EDecryptMenu()}removeFileStatus(t)}).catch(e=>{let n=dropzoneGetFile(t);n!=null&&dropzoneUploadError(n,e),console.error("Error:",e)})}function parseProgressStatus(e){let n=document.getElementById(`us-container-${e.chunk_id}`);if(n==null)return;n.setAttribute("data-complete","true");let t;switch(e.upload_status){case 0:t="Processing file...";break;case 1:t="Uploading file...";break;case 2:t="Finalising...",requestFileInfo(e.file_id,e.chunk_id);break;case 3:t="Error";let n=dropzoneGetFile(e.chunk_id);e.error_message==""&&(e.error_message="Server Error"),n!=null&&dropzoneUploadError(n,e.error_message);return;default:t="Unknown status";break}document.getElementById(`us-progress-info-${e.chunk_id}`).innerText=t}function showError(e,t){let n=e.upload.uuid;document.getElementById(`us-progressbar-${n}`).style.width="100%",document.getElementById(`us-progressbar-${n}`).style.backgroundColor="red",document.getElementById(`us-progress-info-${n}`).innerText=t,document.getElementById(`us-progress-info-${n}`).classList.add("uploaderror")}function editFile(){const e=document.getElementById("mb_save");e.disabled=!0;let s=e.getAttribute("data-fileid"),o=document.getElementById("mi_edit_down").value,i=document.getElementById("mi_edit_expiry").value,t=document.getElementById("mi_edit_pw").value,a=t==="(unchanged)";document.getElementById("mc_download").checked||(o=0),document.getElementById("mc_expiry").checked||(i=0),document.getElementById("mc_password").checked||(a=!1,t="");let r=!1,n="";document.getElementById("mc_replace").checked&&(n=document.getElementById("mi_edit_replace").value,r=n!=""),apiFilesModify(s,o,i,t,a).then(t=>{if(!r){location.reload();return}apiFilesReplace(s,n).then(e=>{location.reload()}).catch(t=>{alert("Unable to edit file: "+t),console.error("Error:",t),e.disabled=!1})}).catch(t=>{alert("Unable to edit file: "+t),console.error("Error:",t),e.disabled=!1})}calendarInstance=null;function createCalendar(e){const t=new Date(e*1e3);calendarInstance=flatpickr("#mi_edit_expiry",{enableTime:!0,dateFormat:"U",altInput:!0,altFormat:"Y-m-d H:i",allowInput:!0,time_24hr:!0,defaultDate:t,minDate:"today"})}function handleEditCheckboxChange(e){var t=document.getElementById(e.getAttribute("data-toggle-target")),n=e.getAttribute("data-timestamp");e.checked?(t.classList.remove("disabled"),t.removeAttribute("disabled"),n!=null&&(calendarInstance._input.disabled=!1)):(n!=null&&(calendarInstance._input.disabled=!0),t.classList.add("disabled"),t.setAttribute("disabled",!0))}function showEditModal(e,t,n,s,o,i,a,r,c){let d=$("#modaledit").clone();$("#modaledit").on("hide.bs.modal",function(){$("#modaledit").remove();let e=d.clone();$("body").append(e)}),document.getElementById("m_filenamelabel").innerText=e,document.getElementById("mc_expiry").setAttribute("data-timestamp",s),document.getElementById("mb_save").setAttribute("data-fileid",t),createCalendar(s),i?(document.getElementById("mi_edit_down").value="1",document.getElementById("mi_edit_down").disabled=!0,document.getElementById("mc_download").checked=!1):(document.getElementById("mi_edit_down").value=n,document.getElementById("mi_edit_down").disabled=!1,document.getElementById("mc_download").checked=!0),a?(document.getElementById("mi_edit_expiry").value=add14DaysIfBeforeCurrentTime(s),document.getElementById("mi_edit_expiry").disabled=!0,document.getElementById("mc_expiry").checked=!1,calendarInstance._input.disabled=!0):(document.getElementById("mi_edit_expiry").value=s,document.getElementById("mi_edit_expiry").disabled=!1,document.getElementById("mc_expiry").checked=!0,calendarInstance._input.disabled=!1),o?(document.getElementById("mi_edit_pw").value="(unchanged)",document.getElementById("mi_edit_pw").disabled=!1,document.getElementById("mc_password").checked=!0):(document.getElementById("mi_edit_pw").value="",document.getElementById("mi_edit_pw").disabled=!0,document.getElementById("mc_password").checked=!1);let l=document.getElementById("mi_edit_replace");if(c)if(document.getElementById("replaceGroup").style.display="flex",r)document.getElementById("mc_replace").disabled=!0,document.getElementById("mc_replace").title="Replacing content is not available for end-to-end encrypted files",l.add(new Option("Unavailable",0)),l.title="Replacing content is not available for end-to-end encrypted files",l.value="0";else{let e=getAllAvailableFiles();for(let n=0;n{changeRowCount(!1,document.getElementById("row-"+e)),showToastFileDeletion(e)}).catch(e=>{alert("Unable to delete file: "+e),console.error("Error:",e)})}function checkBoxChanged(e,t){let n=!e.checked;n?document.getElementById(t).setAttribute("disabled",""):document.getElementById(t).removeAttribute("disabled"),t==="password"&&n&&(document.getElementById("password").value="")}function parseSseData(e){let t;try{t=JSON.parse(e)}catch(e){console.error("Failed to parse event data:",e);return}switch(t.event){case"download":setNewDownloadCount(t.file_id,t.download_count,t.downloads_remaining);return;case"uploadStatus":parseProgressStatus(t);return;default:console.error("Unknown event",t)}}function setNewDownloadCount(e,t,n){let s=document.getElementById("cell-downloads-"+e);if(s!=null&&(s.innerText=t,s.classList.add("updatedDownloadCount"),setTimeout(()=>s.classList.remove("updatedDownloadCount"),500)),n!=-1){let t=document.getElementById("cell-downloadsRemaining-"+e);t!=null&&(t.innerText=n,t.classList.add("updatedDownloadCount"),setTimeout(()=>t.classList.remove("updatedDownloadCount"),500))}}function registerChangeHandler(){const e=new EventSource("./uploadStatus");e.onmessage=e=>{parseSseData(e.data)},e.onerror=t=>{t.target.readyState!==EventSource.CLOSED&&e.close(),console.log("Reconnecting to SSE..."),setTimeout(registerChangeHandler,5e3)}}statusItemCount=0;function addFileStatus(e,t){const n=document.createElement("div");n.setAttribute("id",`us-container-${e}`),n.classList.add("us-container");const a=document.createElement("div");a.classList.add("filename"),a.textContent=t,n.appendChild(a);const s=document.createElement("div");s.classList.add("upload-progress-container"),s.setAttribute("id",`us-progress-container-${e}`);const r=document.createElement("div");r.classList.add("upload-progress-bar");const o=document.createElement("div");o.setAttribute("id",`us-progressbar-${e}`),o.classList.add("upload-progress-bar-progress"),o.style.width="0%",r.appendChild(o);const i=document.createElement("div");i.setAttribute("id",`us-progress-info-${e}`),i.classList.add("upload-progress-info"),i.textContent="0%",s.appendChild(r),s.appendChild(i),n.appendChild(s),n.setAttribute("data-starttime",Date.now()),n.setAttribute("data-complete","false");const c=document.getElementById("uploadstatus");c.appendChild(n),c.style.visibility="visible",statusItemCount++}function removeFileStatus(e){const t=document.getElementById(`us-container-${e}`);if(t==null)return;t.remove(),statusItemCount--,statusItemCount<1&&(document.getElementById("uploadstatus").style.visibility="hidden")}function addRow(e){let d=document.getElementById("downloadtable"),t=d.insertRow(0);e.Id=sanitizeId(e.Id),t.id="row-"+e.Id;let i=t.insertCell(0),a=t.insertCell(1),s=t.insertCell(2),r=t.insertCell(3),c=t.insertCell(4),o=t.insertCell(5),l=t.insertCell(6);i.innerText=e.Name,i.id="cell-name-"+e.Id,c.id="cell-downloads-"+e.Id,a.innerText=e.Size,e.UnlimitedDownloads?s.innerText="Unlimited":(s.innerText=e.DownloadsRemaining,s.id="cell-downloadsRemaining-"+e.Id),e.UnlimitedTime?r.innerText="Unlimited":r.innerText=e.ExpireAtString,c.innerText=e.DownloadCount;const n=document.createElement("a");if(n.href=e.UrlDownload,n.target="_blank",n.style.color="inherit",n.id="url-href-"+e.Id,n.textContent=e.Id,o.appendChild(n),e.IsPasswordProtected===!0){const e=document.createElement("i");e.className="bi bi-key",e.title="Password protected",o.appendChild(document.createTextNode(" ")),o.appendChild(e)}return l.appendChild(createButtonGroup(e)),i.classList.add("newItem"),a.classList.add("newItem"),s.classList.add("newItem"),r.classList.add("newItem"),c.classList.add("newItem"),o.classList.add("newItem"),l.classList.add("newItem"),a.setAttribute("data-order",e.SizeBytes),changeRowCount(!0,t),e.Id}function createButtonGroup(e){const h=document.createElement("div");h.className="btn-toolbar",h.setAttribute("role","toolbar");const t=document.createElement("div");t.className="btn-group me-2",t.setAttribute("role","group");const n=document.createElement("button");n.type="button",n.className="copyurl btn btn-outline-light btn-sm",n.dataset.clipboardText=e.UrlDownload,n.id="url-button-"+e.Id,n.title="Copy URL";const j=document.createElement("i");j.className="bi bi-copy",n.appendChild(j),n.appendChild(document.createTextNode(" URL")),n.addEventListener("click",()=>{showToast(1e3)}),t.appendChild(n);const m=document.createElement("button");m.type="button",m.className="btn btn-outline-light btn-sm dropdown-toggle dropdown-toggle-split",m.setAttribute("data-bs-toggle","dropdown"),m.setAttribute("aria-expanded","false"),t.appendChild(m);const f=document.createElement("ul");f.className="dropdown-menu dropdown-menu-end",f.setAttribute("data-bs-theme","dark");const g=document.createElement("li"),s=document.createElement("a");e.UrlHotlink!==""?(s.className="dropdown-item copyurl",s.title="Copy hotlink",s.style.cursor="pointer",s.setAttribute("data-clipboard-text",e.UrlHotlink),s.onclick=()=>showToast(1e3),s.innerHTML=` Hotlink`):(s.className="dropdown-item",s.innerText="Hotlink not available"),g.appendChild(s),f.appendChild(g),t.appendChild(f);const r=document.createElement("button");r.type="button",r.className="btn btn-outline-light btn-sm",r.title="Share",r.onclick=()=>shareUrl(e.Id),r.innerHTML=``,t.appendChild(r);const d=document.createElement("button");d.type="button",d.className="btn btn-outline-light btn-sm dropdown-toggle dropdown-toggle-split",d.setAttribute("data-bs-toggle","dropdown"),d.setAttribute("aria-expanded","false"),t.appendChild(d);const u=document.createElement("ul");u.className="dropdown-menu dropdown-menu-end",u.setAttribute("data-bs-theme","dark");const p=document.createElement("li"),a=document.createElement("a");a.className="dropdown-item",a.id=`qrcode-${e.Id}`,a.style.cursor="pointer",a.title="Open QR Code",a.onclick=()=>showQrCode(e.UrlDownload),a.innerHTML=` QR Code`,p.appendChild(a),u.appendChild(p);const v=document.createElement("li"),i=document.createElement("a");i.className="dropdown-item",i.title="Share via email",i.id=`email-${e.Id}`,i.target="_blank",i.href=`mailto:?body=${encodeURIComponent(e.UrlDownload)}`,i.innerHTML=` Email`,v.appendChild(i),u.appendChild(v),t.appendChild(u);const l=document.createElement("div");l.className="btn-group me-2",l.setAttribute("role","group");const c=document.createElement("button");c.type="button",c.className="btn btn-outline-light btn-sm",c.title="Edit";const b=document.createElement("i");b.className="bi bi-pencil",c.appendChild(b),c.addEventListener("click",()=>{showEditModal(e.Name,e.Id,e.DownloadsRemaining,e.ExpireAt,e.IsPasswordProtected,e.UnlimitedDownloads,e.UnlimitedTime,e.IsEndToEndEncrypted,canReplaceOwnFiles)}),l.appendChild(c);const o=document.createElement("button");o.type="button",o.className="btn btn-outline-danger btn-sm",o.title="Delete",o.id="button-delete-"+e.Id;const y=document.createElement("i");return y.className="bi bi-trash3",o.appendChild(y),o.addEventListener("click",()=>{deleteFile(e.Id)}),l.appendChild(o),h.appendChild(t),h.appendChild(l),h}function sanitizeId(e){return e.replace(/[^a-zA-Z0-9]/g,"")}function changeRowCount(e,t){let n=$("#maintable").DataTable();rowCount==-1&&(rowCount=n.rows().count()),e?(rowCount=rowCount+1,n.row.add(t)):(rowCount=rowCount-1,t.classList.add("rowDeleting"),setTimeout(()=>{n.row(t).remove(),t.remove()},290));let s=document.getElementsByClassName("dataTables_empty")[0];typeof s!="undefined"?s.innerText="Files stored: "+rowCount:document.getElementsByClassName("dataTables_info")[0].innerText="Files stored: "+rowCount}function hideQrCode(){document.getElementById("qroverlay").style.display="none",document.getElementById("qrcode").innerHTML=""}function showQrCode(e){const t=document.getElementById("qroverlay");t.style.display="block",new QRCode(document.getElementById("qrcode"),{text:e,width:200,height:200,colorDark:"#000000",colorLight:"#ffffff",correctLevel:QRCode.CorrectLevel.H}),t.addEventListener("click",hideQrCode)}function showToastFileDeletion(e){let t=document.getElementById("toastnotificationUndo"),n=document.getElementById("cell-name-"+e).innerText,s=document.getElementById("toastFilename"),o=document.getElementById("toastUndoButton");s.innerText=n,o.dataset.fileid=e,hideToast(),t.classList.add("show"),clearTimeout(toastId),toastId=setTimeout(()=>{hideFileToast()},5e3)}function hideFileToast(){document.getElementById("toastnotificationUndo").classList.remove("show")}function handleUndo(e){hideFileToast(),apiFilesRestore(e.dataset.fileid).then(e=>{addRow(e.FileInfo)}).catch(e=>{alert("Unable to restore file: "+e),console.error("Error:",e)})}function shareUrl(e){if(!navigator.share)return;let t=document.getElementById("cell-name-"+e).innerText,n=document.getElementById("url-href-"+e).getAttribute("href");navigator.share({title:t,url:n})}function changeUserPermission(e,t,n){let s=document.getElementById(n);if(s.classList.contains("perm-processing")||s.classList.contains("perm-nochange"))return;let o=s.classList.contains("perm-granted");s.classList.add("perm-processing"),s.classList.remove("perm-granted"),s.classList.remove("perm-notgranted");let i="GRANT";o&&(i="REVOKE"),t=="PERM_REPLACE_OTHER"&&!o&&(hasNotPermissionReplace=document.getElementById("perm_replace_"+e).classList.contains("perm-notgranted"),hasNotPermissionReplace&&(showToast(2e3,"Also granting permission to replace own files"),changeUserPermission(e,"PERM_REPLACE","perm_replace_"+e))),t=="PERM_REPLACE"&&o&&(hasPermissionReplaceOthers=document.getElementById("perm_replace_other_"+e).classList.contains("perm-granted"),hasPermissionReplaceOthers&&(showToast(2e3,"Also revoking permission to replace files of other users"),changeUserPermission(e,"PERM_REPLACE_OTHER","perm_replace_other_"+e))),apiUserModify(e,t,i).then(e=>{o?s.classList.add("perm-notgranted"):s.classList.add("perm-granted"),s.classList.remove("perm-processing")}).catch(e=>{o?s.classList.add("perm-granted"):s.classList.add("perm-notgranted"),s.classList.remove("perm-processing"),alert("Unable to set permission: "+e),console.error("Error:",e)})}function changeRank(e,t,n){let s=document.getElementById(n);if(s.disabled)return;s.disabled=!0,apiUserChangeRank(e,t).then(e=>{location.reload()}).catch(e=>{s.disabled=!1,alert("Unable to change rank: "+e),console.error("Error:",e)})}function showDeleteModal(e,t){let n=document.getElementById("checkboxDelete");n.checked=!1,document.getElementById("deleteModalBody").innerText=t,$("#deleteModal").modal("show"),document.getElementById("buttonDelete").onclick=function(){apiUserDelete(e,n.checked).then(t=>{$("#deleteModal").modal("hide"),document.getElementById("row-"+e).classList.add("rowDeleting"),setTimeout(()=>{document.getElementById("row-"+e).remove()},290)}).catch(e=>{alert("Unable to delete user: "+e),console.error("Error:",e)})}}function showAddUserModal(){let e=$("#newUserModal").clone();$("#newUserModal").on("hide.bs.modal",function(){$("#newUserModal").remove();let t=e.clone();$("body").append(t)}),$("#newUserModal").modal("show")}function showResetPwModal(e,t){let n=$("#resetPasswordModal").clone();$("#resetPasswordModal").on("hide.bs.modal",function(){$("#resetPasswordModal").remove();let e=n.clone();$("body").append(e)}),document.getElementById("l_userpwreset").innerText=t;let s=document.getElementById("resetPasswordButton");s.onclick=function(){resetPw(e,document.getElementById("generateRandomPassword").checked)},$("#resetPasswordModal").modal("show")}function resetPw(e,t){let n=document.getElementById("resetPasswordButton");document.getElementById("resetPasswordButton").disabled=!0,apiUserResetPassword(e,t).then(e=>{if(!t){$("#resetPasswordModal").modal("hide"),showToast(1e3,"Password change requirement set successfully");return}n.style.display="none",document.getElementById("cancelPasswordButton").style.display="none",document.getElementById("formentryReset").style.display="none",document.getElementById("randomPasswordContainer").style.display="block",document.getElementById("closeModalResetPw").style.display="block",document.getElementById("l_returnedPw").innerText=e.password,document.getElementById("copypwclip").onclick=function(){navigator.clipboard.writeText(e.password),showToast(1e3,"Password copied to clipboard")}}).catch(e=>{alert("Unable to reset user password: "+e),console.error("Error:",e),n.disabled=!1})}function addNewUser(){let e=document.getElementById("mb_addUser");e.disabled=!0;let t=document.getElementById("newUserForm");if(t.checkValidity()){let t=document.getElementById("e_userName");apiUserCreate(t.value.trim()).then(e=>{$("#newUserModal").modal("hide"),addRowUser(e.id,e.name)}).catch(t=>{t.message=="duplicate"?(alert("A user already exists with that name"),e.disabled=!1):(alert("Unable to create user: "+t),console.error("Error:",t),e.disabled=!1)})}else t.classList.add("was-validated"),e.disabled=!1}function addRowUser(e,t){e=sanitizeUserId(e);let h=document.getElementById("usertable"),n=h.insertRow(1);n.id="row-"+e;let r=n.insertCell(0),c=n.insertCell(1),l=n.insertCell(2),d=n.insertCell(3),u=n.insertCell(4),a=n.insertCell(5);r.classList.add("newUser"),c.classList.add("newUser"),l.classList.add("newUser"),d.classList.add("newUser"),u.classList.add("newUser"),a.classList.add("newUser"),r.innerText=t,c.innerText="User",l.innerText="Never",d.innerText="0";const i=document.createElement("div");if(i.className="btn-group",i.setAttribute("role","group"),isInternalAuth){const n=document.createElement("button");n.id=`pwchange-${e}`,n.type="button",n.className="btn btn-outline-light btn-sm",n.title="Reset Password",n.onclick=()=>showResetPwModal(e,t),n.innerHTML=``,i.appendChild(n)}const s=document.createElement("button");s.id=`changeRank_${e}`,s.type="button",s.className="btn btn-outline-light btn-sm",s.title="Promote User",s.onclick=()=>changeRank(e,"ADMIN",`changeRank_${e}`),s.innerHTML=``,i.appendChild(s);const o=document.createElement("button");o.id=`delete-${e}`,o.type="button",o.className="btn btn-outline-danger btn-sm",o.title="Delete",o.onclick=()=>showDeleteModal(e,t),o.innerHTML=``,i.appendChild(o),a.innerHTML="",a.appendChild(i),u.innerHTML=` +`),textarea.scrollTop=textarea.scrollHeight}function deleteLogs(e){if(e=="none")return;if(!confirm("Do you want to delete the selected logs?")){document.getElementById("deleteLogs").selectedIndex=0;return}let t=Math.floor(Date.now()/1e3);switch(e){case"all":t=0;break;case"2":t=t-2*24*60*60;break;case"7":t=t-7*24*60*60;break;case"14":t=t-14*24*60*60;break;case"30":t=t-30*24*60*60;break}apiLogsDelete(t).then(e=>{location.reload()}).catch(e=>{alert("Unable to delete logs: "+e),console.error("Error:",e)})}isE2EEnabled=!1,isUploading=!1,rowCount=-1;function initDropzone(){Dropzone.options.uploaddropzone={paramName:"file",dictDefaultMessage:"Drop files, paste or click here to upload",createImageThumbnails:!1,chunksUploaded:function(e,t){sendChunkComplete(e,t)},init:function(){dropzoneObject=this,this.on("addedfile",e=>{saveUploadDefaults(),addFileProgress(e)}),this.on("queuecomplete",function(){isUploading=!1}),this.on("sending",function(){isUploading=!0}),this.on("error",function(e,t,n){n&&n.status===413?showError(e,"File too large to upload. If you are using a reverse proxy, make sure that the allowed body size is at least 70MB."):showError(e,"Error: "+t)}),this.on("uploadprogress",function(e,t,n){updateProgressbar(e,t,n)}),isE2EEnabled&&(dropzoneObject.disable(),dropzoneObject.options.dictDefaultMessage="Loading end-to-end encryption...",document.getElementsByClassName("dz-button")[0].innerText="Loading end-to-end encryption...",setE2eUpload())}},document.onpaste=function(e){if(dropzoneObject.disabled)return;const n=document.activeElement;if(n&&(n.hasAttribute("data-allow-regular-paste")||n.hasAttribute("placeholder")))return;var t,s=(e.clipboardData||e.originalEvent.clipboardData).items;for(let e in s)t=s[e],t.kind==="file"&&dropzoneObject.addFile(t.getAsFile()),t.kind==="string"&&t.getAsString(function(e){const t=//gi;if(t.test(e)===!1){let t=new Blob([e],{type:"text/plain"}),n=new File([t],"Pasted Text.txt",{type:"text/plain",lastModified:new Date(0)});dropzoneObject.addFile(n)}})},window.addEventListener("beforeunload",e=>{isUploading&&(e.returnValue="Upload is still in progress. Do you want to close this page?")})}function updateProgressbar(e,t,n){let o=e.upload.uuid,i=document.getElementById(`us-container-${o}`);if(i==null||i.getAttribute("data-complete")==="true")return;let s=Math.round(t);s<0&&(s=0),s>100&&(s=100);let r=Date.now()-i.getAttribute("data-starttime"),c=n/(r/1e3)/1024/1024;document.getElementById(`us-progressbar-${o}`).style.width=s+"%";let a=Math.round(c*10)/10;Number.isNaN(a)||(document.getElementById(`us-progress-info-${o}`).innerText=s+"% - "+a+"MB/s")}function addFileProgress(e){addFileStatus(e.upload.uuid,e.upload.filename)}function setUploadDefaults(){let s=getLocalStorageWithDefault("defaultDownloads",1),o=getLocalStorageWithDefault("defaultExpiry",14),e=getLocalStorageWithDefault("defaultPassword",""),t=getLocalStorageWithDefault("defaultUnlimitedDownloads",!1)==="true",n=getLocalStorageWithDefault("defaultUnlimitedTime",!1)==="true";document.getElementById("allowedDownloads").value=s,document.getElementById("expiryDays").value=o,document.getElementById("password").value=e,document.getElementById("enableDownloadLimit").checked=!t,document.getElementById("enableTimeLimit").checked=!n,e===""?(document.getElementById("enablePassword").checked=!1,document.getElementById("password").disabled=!0):(document.getElementById("enablePassword").checked=!0,document.getElementById("password").disabled=!1),t&&(document.getElementById("allowedDownloads").disabled=!0),n&&(document.getElementById("expiryDays").disabled=!0)}function saveUploadDefaults(){localStorage.setItem("defaultDownloads",document.getElementById("allowedDownloads").value),localStorage.setItem("defaultExpiry",document.getElementById("expiryDays").value),localStorage.setItem("defaultPassword",document.getElementById("password").value),localStorage.setItem("defaultUnlimitedDownloads",!document.getElementById("enableDownloadLimit").checked),localStorage.setItem("defaultUnlimitedTime",!document.getElementById("enableTimeLimit").checked)}function getLocalStorageWithDefault(e,t){var n=localStorage.getItem(e);return n===null?t:n}function urlencodeFormData(e){let t="";function s(e){return encodeURIComponent(e).replace(/%20/g,"+")}for(var n of e.entries())typeof n[1]=="string"&&(t+=(t?"&":"")+s(n[0])+"="+s(n[1]));return t}function sendChunkComplete(e,t){let c=e.upload.uuid,n=e.name,s=e.size,l=e.size,o=e.type,i=document.getElementById("allowedDownloads").value,a=document.getElementById("expiryDays").value,d=document.getElementById("password").value,r=e.isEndToEndEncrypted===!0,u=!0;document.getElementById("enableDownloadLimit").checked||(i=0),document.getElementById("enableTimeLimit").checked||(a=0),r&&(s=e.sizeEncrypted,n="Encrypted File",o=""),apiChunkComplete(c,n,s,l,o,i,a,d,r,u).then(n=>{t();let s=document.getElementById(`us-progress-info-${e.upload.uuid}`);s!=null&&(s.innerText="In Queue...")}).catch(t=>{console.error("Error:",t),dropzoneUploadError(e,t)})}function dropzoneUploadError(e,t){e.accepted=!1,dropzoneObject._errorProcessing([e],t),showError(e,t)}function dropzoneGetFile(e){for(let t=0;t{addRow(n);let s=dropzoneGetFile(t);if(s==null)return;if(s.isEndToEndEncrypted===!0){try{let o=GokapiE2EAddFile(t,e,s.name);if(o instanceof Error)throw o;let n=GokapiE2EInfoEncrypt();if(n instanceof Error)throw n;storeE2EInfo(n)}catch(e){s.accepted=!1,dropzoneObject._errorProcessing([s],e);return}GokapiE2EDecryptMenu()}removeFileStatus(t)}).catch(e=>{let n=dropzoneGetFile(t);n!=null&&dropzoneUploadError(n,e),console.error("Error:",e)})}function parseProgressStatus(e){let n=document.getElementById(`us-container-${e.chunk_id}`);if(n==null)return;n.setAttribute("data-complete","true");let t;switch(e.upload_status){case 0:t="Processing file...";break;case 1:t="Uploading file...";break;case 2:t="Finalising...",requestFileInfo(e.file_id,e.chunk_id);break;case 3:t="Error";let n=dropzoneGetFile(e.chunk_id);e.error_message==""&&(e.error_message="Server Error"),n!=null&&dropzoneUploadError(n,e.error_message);return;default:t="Unknown status";break}document.getElementById(`us-progress-info-${e.chunk_id}`).innerText=t}function showError(e,t){let n=e.upload.uuid;document.getElementById(`us-progressbar-${n}`).style.width="100%",document.getElementById(`us-progressbar-${n}`).style.backgroundColor="red",document.getElementById(`us-progress-info-${n}`).innerText=t,document.getElementById(`us-progress-info-${n}`).classList.add("uploaderror")}function editFile(){const e=document.getElementById("mb_save");e.disabled=!0;let s=e.getAttribute("data-fileid"),o=document.getElementById("mi_edit_down").value,i=document.getElementById("mi_edit_expiry").value,t=document.getElementById("mi_edit_pw").value,a=t==="(unchanged)";document.getElementById("mc_download").checked||(o=0),document.getElementById("mc_expiry").checked||(i=0),document.getElementById("mc_password").checked||(a=!1,t="");let r=!1,n="";document.getElementById("mc_replace").checked&&(n=document.getElementById("mi_edit_replace").value,r=n!=""),apiFilesModify(s,o,i,t,a).then(t=>{if(!r){location.reload();return}apiFilesReplace(s,n).then(e=>{location.reload()}).catch(t=>{alert("Unable to edit file: "+t),console.error("Error:",t),e.disabled=!1})}).catch(t=>{alert("Unable to edit file: "+t),console.error("Error:",t),e.disabled=!1})}calendarInstance=null;function createCalendar(e){const t=new Date(e*1e3);calendarInstance=flatpickr("#mi_edit_expiry",{enableTime:!0,dateFormat:"U",altInput:!0,altFormat:"Y-m-d H:i",allowInput:!0,time_24hr:!0,defaultDate:t,minDate:"today"})}function handleEditCheckboxChange(e){var t=document.getElementById(e.getAttribute("data-toggle-target")),n=e.getAttribute("data-timestamp");e.checked?(t.classList.remove("disabled"),t.removeAttribute("disabled"),n!=null&&(calendarInstance._input.disabled=!1)):(n!=null&&(calendarInstance._input.disabled=!0),t.classList.add("disabled"),t.setAttribute("disabled",!0))}function showEditModal(e,t,n,s,o,i,a,r,c){let d=$("#modaledit").clone();$("#modaledit").on("hide.bs.modal",function(){$("#modaledit").remove();let e=d.clone();$("body").append(e)}),document.getElementById("m_filenamelabel").innerText=e,document.getElementById("mc_expiry").setAttribute("data-timestamp",s),document.getElementById("mb_save").setAttribute("data-fileid",t),createCalendar(s),i?(document.getElementById("mi_edit_down").value="1",document.getElementById("mi_edit_down").disabled=!0,document.getElementById("mc_download").checked=!1):(document.getElementById("mi_edit_down").value=n,document.getElementById("mi_edit_down").disabled=!1,document.getElementById("mc_download").checked=!0),a?(document.getElementById("mi_edit_expiry").value=add14DaysIfBeforeCurrentTime(s),document.getElementById("mi_edit_expiry").disabled=!0,document.getElementById("mc_expiry").checked=!1,calendarInstance._input.disabled=!0):(document.getElementById("mi_edit_expiry").value=s,document.getElementById("mi_edit_expiry").disabled=!1,document.getElementById("mc_expiry").checked=!0,calendarInstance._input.disabled=!1),o?(document.getElementById("mi_edit_pw").value="(unchanged)",document.getElementById("mi_edit_pw").disabled=!1,document.getElementById("mc_password").checked=!0):(document.getElementById("mi_edit_pw").value="",document.getElementById("mi_edit_pw").disabled=!0,document.getElementById("mc_password").checked=!1);let l=document.getElementById("mi_edit_replace");if(c)if(document.getElementById("replaceGroup").style.display="flex",r)document.getElementById("mc_replace").disabled=!0,document.getElementById("mc_replace").title="Replacing content is not available for end-to-end encrypted files",l.add(new Option("Unavailable",0)),l.title="Replacing content is not available for end-to-end encrypted files",l.value="0";else{let e=getAllAvailableFiles();for(let n=0;n{changeRowCount(!1,document.getElementById("row-"+e)),showToastFileDeletion(e)}).catch(e=>{alert("Unable to delete file: "+e),console.error("Error:",e)})}function checkBoxChanged(e,t){let n=!e.checked;n?document.getElementById(t).setAttribute("disabled",""):document.getElementById(t).removeAttribute("disabled"),t==="password"&&n&&(document.getElementById("password").value="")}function parseSseData(e){let t;try{t=JSON.parse(e)}catch(e){console.error("Failed to parse event data:",e);return}switch(t.event){case"download":setNewDownloadCount(t.file_id,t.download_count,t.downloads_remaining);return;case"uploadStatus":parseProgressStatus(t);return;default:console.error("Unknown event",t)}}function setNewDownloadCount(e,t,n){let s=document.getElementById("cell-downloads-"+e);if(s!=null&&(s.innerText=t,s.classList.add("updatedDownloadCount"),setTimeout(()=>s.classList.remove("updatedDownloadCount"),500)),n!=-1){let t=document.getElementById("cell-downloadsRemaining-"+e);t!=null&&(t.innerText=n,t.classList.add("updatedDownloadCount"),setTimeout(()=>t.classList.remove("updatedDownloadCount"),500))}}function registerChangeHandler(){const e=new EventSource("./uploadStatus");e.onmessage=e=>{parseSseData(e.data)},e.onerror=t=>{t.target.readyState!==EventSource.CLOSED&&e.close(),console.log("Reconnecting to SSE..."),setTimeout(registerChangeHandler,5e3)}}statusItemCount=0;function addFileStatus(e,t){const n=document.createElement("div");n.setAttribute("id",`us-container-${e}`),n.classList.add("us-container");const a=document.createElement("div");a.classList.add("filename"),a.textContent=t,n.appendChild(a);const s=document.createElement("div");s.classList.add("upload-progress-container"),s.setAttribute("id",`us-progress-container-${e}`);const r=document.createElement("div");r.classList.add("upload-progress-bar");const o=document.createElement("div");o.setAttribute("id",`us-progressbar-${e}`),o.classList.add("upload-progress-bar-progress"),o.style.width="0%",r.appendChild(o);const i=document.createElement("div");i.setAttribute("id",`us-progress-info-${e}`),i.classList.add("upload-progress-info"),i.textContent="0%",s.appendChild(r),s.appendChild(i),n.appendChild(s),n.setAttribute("data-starttime",Date.now()),n.setAttribute("data-complete","false");const c=document.getElementById("uploadstatus");c.appendChild(n),c.style.visibility="visible",statusItemCount++}function removeFileStatus(e){const t=document.getElementById(`us-container-${e}`);if(t==null)return;t.remove(),statusItemCount--,statusItemCount<1&&(document.getElementById("uploadstatus").style.visibility="hidden")}function addRow(e){let d=document.getElementById("downloadtable"),t=d.insertRow(0);e.Id=sanitizeId(e.Id),t.id="row-"+e.Id;let i=t.insertCell(0),a=t.insertCell(1),s=t.insertCell(2),r=t.insertCell(3),c=t.insertCell(4),o=t.insertCell(5),l=t.insertCell(6);i.innerText=e.Name,i.id="cell-name-"+e.Id,c.id="cell-downloads-"+e.Id,a.innerText=e.Size,e.UnlimitedDownloads?s.innerText="Unlimited":(s.innerText=e.DownloadsRemaining,s.id="cell-downloadsRemaining-"+e.Id),e.UnlimitedTime?r.innerText="Unlimited":r.innerText=e.ExpireAtString,c.innerText=e.DownloadCount;const n=document.createElement("a");if(n.href=e.UrlDownload,n.target="_blank",n.style.color="inherit",n.id="url-href-"+e.Id,n.textContent=e.Id,o.appendChild(n),e.IsPasswordProtected===!0){const e=document.createElement("i");e.className="bi bi-key",e.title="Password protected",o.appendChild(document.createTextNode(" ")),o.appendChild(e)}return l.appendChild(createButtonGroup(e)),i.classList.add("newItem"),a.classList.add("newItem"),s.classList.add("newItem"),r.classList.add("newItem"),c.classList.add("newItem"),o.classList.add("newItem"),l.classList.add("newItem"),a.setAttribute("data-order",e.SizeBytes),changeRowCount(!0,t),e.Id}function createButtonGroup(e){const h=document.createElement("div");h.className="btn-toolbar",h.setAttribute("role","toolbar");const t=document.createElement("div");t.className="btn-group me-2",t.setAttribute("role","group");const n=document.createElement("button");n.type="button",n.className="copyurl btn btn-outline-light btn-sm",n.dataset.clipboardText=e.UrlDownload,n.id="url-button-"+e.Id,n.title="Copy URL";const j=document.createElement("i");j.className="bi bi-copy",n.appendChild(j),n.appendChild(document.createTextNode(" URL")),n.addEventListener("click",()=>{showToast(1e3)}),t.appendChild(n);const m=document.createElement("button");m.type="button",m.className="btn btn-outline-light btn-sm dropdown-toggle dropdown-toggle-split",m.setAttribute("data-bs-toggle","dropdown"),m.setAttribute("aria-expanded","false"),t.appendChild(m);const f=document.createElement("ul");f.className="dropdown-menu dropdown-menu-end",f.setAttribute("data-bs-theme","dark");const g=document.createElement("li"),s=document.createElement("a");e.UrlHotlink!==""?(s.className="dropdown-item copyurl",s.title="Copy hotlink",s.style.cursor="pointer",s.setAttribute("data-clipboard-text",e.UrlHotlink),s.onclick=()=>showToast(1e3),s.innerHTML=` Hotlink`):(s.className="dropdown-item",s.innerText="Hotlink not available"),g.appendChild(s),f.appendChild(g),t.appendChild(f);const r=document.createElement("button");r.type="button",r.className="btn btn-outline-light btn-sm",r.title="Share",r.onclick=()=>shareUrl(e.Id),r.innerHTML=``,t.appendChild(r);const d=document.createElement("button");d.type="button",d.className="btn btn-outline-light btn-sm dropdown-toggle dropdown-toggle-split",d.setAttribute("data-bs-toggle","dropdown"),d.setAttribute("aria-expanded","false"),t.appendChild(d);const u=document.createElement("ul");u.className="dropdown-menu dropdown-menu-end",u.setAttribute("data-bs-theme","dark");const p=document.createElement("li"),a=document.createElement("a");a.className="dropdown-item",a.id=`qrcode-${e.Id}`,a.style.cursor="pointer",a.title="Open QR Code",a.onclick=()=>showQrCode(e.UrlDownload),a.innerHTML=` QR Code`,p.appendChild(a),u.appendChild(p);const v=document.createElement("li"),i=document.createElement("a");i.className="dropdown-item",i.title="Share via email",i.id=`email-${e.Id}`,i.target="_blank",i.href=`mailto:?body=${encodeURIComponent(e.UrlDownload)}`,i.innerHTML=` Email`,v.appendChild(i),u.appendChild(v),t.appendChild(u);const l=document.createElement("div");l.className="btn-group me-2",l.setAttribute("role","group");const c=document.createElement("button");c.type="button",c.className="btn btn-outline-light btn-sm",c.title="Edit";const b=document.createElement("i");b.className="bi bi-pencil",c.appendChild(b),c.addEventListener("click",()=>{showEditModal(e.Name,e.Id,e.DownloadsRemaining,e.ExpireAt,e.IsPasswordProtected,e.UnlimitedDownloads,e.UnlimitedTime,e.IsEndToEndEncrypted,canReplaceOwnFiles)}),l.appendChild(c);const o=document.createElement("button");o.type="button",o.className="btn btn-outline-danger btn-sm",o.title="Delete",o.id="button-delete-"+e.Id;const y=document.createElement("i");return y.className="bi bi-trash3",o.appendChild(y),o.addEventListener("click",()=>{deleteFile(e.Id)}),l.appendChild(o),h.appendChild(t),h.appendChild(l),h}function sanitizeId(e){return e.replace(/[^a-zA-Z0-9]/g,"")}function changeRowCount(e,t){let n=$("#maintable").DataTable();rowCount==-1&&(rowCount=n.rows().count()),e?(rowCount=rowCount+1,n.row.add(t)):(rowCount=rowCount-1,t.classList.add("rowDeleting"),setTimeout(()=>{n.row(t).remove(),t.remove()},290));let s=document.getElementsByClassName("dataTables_empty")[0];typeof s!="undefined"?s.innerText="Files stored: "+rowCount:document.getElementsByClassName("dataTables_info")[0].innerText="Files stored: "+rowCount}function hideQrCode(){document.getElementById("qroverlay").style.display="none",document.getElementById("qrcode").innerHTML=""}function showQrCode(e){const t=document.getElementById("qroverlay");t.style.display="block",new QRCode(document.getElementById("qrcode"),{text:e,width:200,height:200,colorDark:"#000000",colorLight:"#ffffff",correctLevel:QRCode.CorrectLevel.H}),t.addEventListener("click",hideQrCode)}function showToastFileDeletion(e){let t=document.getElementById("toastnotificationUndo"),n=document.getElementById("cell-name-"+e).innerText,s=document.getElementById("toastFilename"),o=document.getElementById("toastUndoButton");s.innerText=n,o.dataset.fileid=e,hideToast(),t.classList.add("show"),clearTimeout(toastId),toastId=setTimeout(()=>{hideFileToast()},5e3)}function hideFileToast(){document.getElementById("toastnotificationUndo").classList.remove("show")}function handleUndo(e){hideFileToast(),apiFilesRestore(e.dataset.fileid).then(e=>{addRow(e.FileInfo)}).catch(e=>{alert("Unable to restore file: "+e),console.error("Error:",e)})}function shareUrl(e){if(!navigator.share)return;let t=document.getElementById("cell-name-"+e).innerText,n=document.getElementById("url-href-"+e).getAttribute("href");navigator.share({title:t,url:n})}function showDeprecationNotice(){let e=document.getElementById("toastDeprecation");e.classList.add("show"),setTimeout(()=>{e.classList.remove("show")},5e3)}function changeUserPermission(e,t,n){let s=document.getElementById(n);if(s.classList.contains("perm-processing")||s.classList.contains("perm-nochange"))return;let o=s.classList.contains("perm-granted");s.classList.add("perm-processing"),s.classList.remove("perm-granted"),s.classList.remove("perm-notgranted");let i="GRANT";o&&(i="REVOKE"),t=="PERM_REPLACE_OTHER"&&!o&&(hasNotPermissionReplace=document.getElementById("perm_replace_"+e).classList.contains("perm-notgranted"),hasNotPermissionReplace&&(showToast(2e3,"Also granting permission to replace own files"),changeUserPermission(e,"PERM_REPLACE","perm_replace_"+e))),t=="PERM_REPLACE"&&o&&(hasPermissionReplaceOthers=document.getElementById("perm_replace_other_"+e).classList.contains("perm-granted"),hasPermissionReplaceOthers&&(showToast(2e3,"Also revoking permission to replace files of other users"),changeUserPermission(e,"PERM_REPLACE_OTHER","perm_replace_other_"+e))),apiUserModify(e,t,i).then(e=>{o?s.classList.add("perm-notgranted"):s.classList.add("perm-granted"),s.classList.remove("perm-processing")}).catch(e=>{o?s.classList.add("perm-granted"):s.classList.add("perm-notgranted"),s.classList.remove("perm-processing"),alert("Unable to set permission: "+e),console.error("Error:",e)})}function changeRank(e,t,n){let s=document.getElementById(n);if(s.disabled)return;s.disabled=!0,apiUserChangeRank(e,t).then(e=>{location.reload()}).catch(e=>{s.disabled=!1,alert("Unable to change rank: "+e),console.error("Error:",e)})}function showDeleteModal(e,t){let n=document.getElementById("checkboxDelete");n.checked=!1,document.getElementById("deleteModalBody").innerText=t,$("#deleteModal").modal("show"),document.getElementById("buttonDelete").onclick=function(){apiUserDelete(e,n.checked).then(t=>{$("#deleteModal").modal("hide"),document.getElementById("row-"+e).classList.add("rowDeleting"),setTimeout(()=>{document.getElementById("row-"+e).remove()},290)}).catch(e=>{alert("Unable to delete user: "+e),console.error("Error:",e)})}}function showAddUserModal(){let e=$("#newUserModal").clone();$("#newUserModal").on("hide.bs.modal",function(){$("#newUserModal").remove();let t=e.clone();$("body").append(t)}),$("#newUserModal").modal("show")}function showResetPwModal(e,t){let n=$("#resetPasswordModal").clone();$("#resetPasswordModal").on("hide.bs.modal",function(){$("#resetPasswordModal").remove();let e=n.clone();$("body").append(e)}),document.getElementById("l_userpwreset").innerText=t;let s=document.getElementById("resetPasswordButton");s.onclick=function(){resetPw(e,document.getElementById("generateRandomPassword").checked)},$("#resetPasswordModal").modal("show")}function resetPw(e,t){let n=document.getElementById("resetPasswordButton");document.getElementById("resetPasswordButton").disabled=!0,apiUserResetPassword(e,t).then(e=>{if(!t){$("#resetPasswordModal").modal("hide"),showToast(1e3,"Password change requirement set successfully");return}n.style.display="none",document.getElementById("cancelPasswordButton").style.display="none",document.getElementById("formentryReset").style.display="none",document.getElementById("randomPasswordContainer").style.display="block",document.getElementById("closeModalResetPw").style.display="block",document.getElementById("l_returnedPw").innerText=e.password,document.getElementById("copypwclip").onclick=function(){navigator.clipboard.writeText(e.password),showToast(1e3,"Password copied to clipboard")}}).catch(e=>{alert("Unable to reset user password: "+e),console.error("Error:",e),n.disabled=!1})}function addNewUser(){let e=document.getElementById("mb_addUser");e.disabled=!0;let t=document.getElementById("newUserForm");if(t.checkValidity()){let t=document.getElementById("e_userName");apiUserCreate(t.value.trim()).then(e=>{$("#newUserModal").modal("hide"),addRowUser(e.id,e.name)}).catch(t=>{t.message=="duplicate"?(alert("A user already exists with that name"),e.disabled=!1):(alert("Unable to create user: "+t),console.error("Error:",t),e.disabled=!1)})}else t.classList.add("was-validated"),e.disabled=!1}function addRowUser(e,t){e=sanitizeUserId(e);let h=document.getElementById("usertable"),n=h.insertRow(1);n.id="row-"+e;let r=n.insertCell(0),c=n.insertCell(1),l=n.insertCell(2),d=n.insertCell(3),u=n.insertCell(4),a=n.insertCell(5);r.classList.add("newUser"),c.classList.add("newUser"),l.classList.add("newUser"),d.classList.add("newUser"),u.classList.add("newUser"),a.classList.add("newUser"),r.innerText=t,c.innerText="User",l.innerText="Never",d.innerText="0";const i=document.createElement("div");if(i.className="btn-group",i.setAttribute("role","group"),isInternalAuth){const n=document.createElement("button");n.id=`pwchange-${e}`,n.type="button",n.className="btn btn-outline-light btn-sm",n.title="Reset Password",n.onclick=()=>showResetPwModal(e,t),n.innerHTML=``,i.appendChild(n)}const s=document.createElement("button");s.id=`changeRank_${e}`,s.type="button",s.className="btn btn-outline-light btn-sm",s.title="Promote User",s.onclick=()=>changeRank(e,"ADMIN",`changeRank_${e}`),s.innerHTML=``,i.appendChild(s);const o=document.createElement("button");o.id=`delete-${e}`,o.type="button",o.className="btn btn-outline-danger btn-sm",o.title="Delete",o.onclick=()=>showDeleteModal(e,t),o.innerHTML=``,i.appendChild(o),a.innerHTML="",a.appendChild(i),u.innerHTML=` diff --git a/internal/webserver/web/templates/html_admin.tmpl b/internal/webserver/web/templates/html_admin.tmpl index c830fd8..481faab 100644 --- a/internal/webserver/web/templates/html_admin.tmpl +++ b/internal/webserver/web/templates/html_admin.tmpl @@ -111,6 +111,9 @@ File deleted: Restore +
+ Warning! This server is using a deprecated feature. Detailed information can be found in the system logs. +
@@ -155,6 +158,9 @@ var canReplaceOwnFiles = {{.ActiveUser.HasPermissionReplace}}; setUploadDefaults(); +{{ if .ShowDeprecationNotice }} + showDeprecationNotice(); +{{ end }} diff --git a/internal/webserver/web/templates/string_constants.tmpl b/internal/webserver/web/templates/string_constants.tmpl index 4ffcc92..475818a 100644 --- a/internal/webserver/web/templates/string_constants.tmpl +++ b/internal/webserver/web/templates/string_constants.tmpl @@ -3,7 +3,7 @@ // Specifies the version of JS files, so that the browser doesn't // use a cached version, if the file has been updated -{{define "js_admin_version"}}12{{end}} +{{define "js_admin_version"}}13{{end}} {{define "js_dropzone_version"}}5{{end}} {{define "js_e2eversion"}}8{{end}} {{define "css_main"}}5{{end}} \ No newline at end of file