diff --git a/client/src/components/files.vue b/client/src/components/files.vue index b5f263f9..e959dc12 100644 --- a/client/src/components/files.vue +++ b/client/src/components/files.vue @@ -10,6 +10,12 @@

{{ item.name }}

{{ fileSize(item.size) }}

+
@@ -181,10 +187,15 @@ .refresh:hover, .download:hover, + .delete:hover, .remove-transfer:hover { cursor: pointer; } + .delete { + margin-left: 0.5em; + } + .transfer-area { margin-top: auto; } @@ -286,6 +297,10 @@ return this.$accessor.user.admin || this.$accessor.files.userDownload } + get canDelete() { + return this.$accessor.user.admin || this.$accessor.files.userDelete + } + get canUpload() { return this.$accessor.user.admin || this.$accessor.files.userUpload } @@ -450,6 +465,28 @@ this.$accessor.files.removeTransfer(transfer) } + async deleteFile(item: FileListItem) { + const { isConfirmed } = await this.$swal({ + title: this.$t('files.delete_title', { name: item.name }) as string, + text: this.$t('files.delete_confirm', { name: item.name }) as string, + icon: 'warning', + showCancelButton: true, + confirmButtonText: this.$t('context.confirm.button_yes') as string, + cancelButtonText: this.$t('context.confirm.button_cancel') as string, + }) + if (!isConfirmed) { + return + } + const url = + '/file?pwd=' + encodeURIComponent(this.$accessor.password) + '&filename=' + encodeURIComponent(item.name) + try { + await this.$http.delete(url) + this.$accessor.files.refresh() + } catch (error: any) { + this.$log.error(error) + } + } + fileIcon(file: FileListItem) { let className = 'file-icon fas ' // if is directory diff --git a/client/src/locale/de-de.ts b/client/src/locale/de-de.ts index 8633afae..34aec98e 100644 --- a/client/src/locale/de-de.ts +++ b/client/src/locale/de-de.ts @@ -124,4 +124,7 @@ export const files = { downloads: 'Herunterladen', uploads: 'Hochladen', upload_here: 'Klicken oder ziehen Sie Dateien zum Hochladen hierher', + delete: 'Löschen', + delete_title: '"{name}" löschen?', + delete_confirm: 'Möchten Sie diese Datei wirklich entfernen?', } diff --git a/client/src/locale/en-us.ts b/client/src/locale/en-us.ts index 47f3c47e..99cb16ce 100644 --- a/client/src/locale/en-us.ts +++ b/client/src/locale/en-us.ts @@ -127,4 +127,7 @@ export const files = { downloads: 'Downloads', uploads: 'Uploads', upload_here: 'Click or drag files here to upload', + delete: 'Delete', + delete_title: 'Delete "{name}"?', + delete_confirm: 'Do you really want to remove this file?', } diff --git a/client/src/locale/es-sp.ts b/client/src/locale/es-sp.ts index 2a27e9a2..a8f300b5 100644 --- a/client/src/locale/es-sp.ts +++ b/client/src/locale/es-sp.ts @@ -124,4 +124,7 @@ export const files = { downloads: 'Descargas', uploads: 'Subidas', upload_here: 'Haz clic o arrastra archivos aquí para subirlos', + delete: 'Eliminar', + delete_title: '¿Eliminar "{name}"?', + delete_confirm: '¿Realmente deseas eliminar este archivo?', } diff --git a/client/src/locale/fi-fi.ts b/client/src/locale/fi-fi.ts index 804d8e85..e36e0638 100644 --- a/client/src/locale/fi-fi.ts +++ b/client/src/locale/fi-fi.ts @@ -124,4 +124,7 @@ export const files = { downloads: 'Lataukset', uploads: 'Lataa', upload_here: 'Klikkaa tai vedä tiedostoja tähän ladataksesi', + delete: 'Poista', + delete_title: 'Poistetaanko "{name}"?', + delete_confirm: 'Haluatko varmasti poistaa tämän tiedoston?', } diff --git a/client/src/locale/fr-fr.ts b/client/src/locale/fr-fr.ts index 1d32ce17..471539e3 100644 --- a/client/src/locale/fr-fr.ts +++ b/client/src/locale/fr-fr.ts @@ -2,7 +2,7 @@ export const logout = 'Se déconnecter' export const unsupported = 'ce navigateur ne prend pas en charge WebRTC' export const admin_loggedin = "Vous êtes connecté en tant qu'admin" export const you = 'Vous' -export const somebody = 'Quelqu\'un' // TODO: vérifier la traduction +export const somebody = "Quelqu'un" // TODO: vérifier la traduction export const send_a_message = 'Envoyer un message' export const side = { @@ -18,7 +18,7 @@ export const connect = { password: 'Mot de passe', connect: 'Connexion', error: 'Erreur de connexion', - empty_displayname: 'Le nom d\'affichage ne peut pas être vide.', // TODO: vérifier la traduction + empty_displayname: "Le nom d'affichage ne peut pas être vide.", // TODO: vérifier la traduction } export const context = { @@ -51,7 +51,7 @@ export const controls = { lock: 'Vérouiller le contrôle', unlock: 'Débloquer le contrôle', has: 'Vous avez le contrôle', // TODO: vérifier la traduction - hasnot: 'Vous n\'avez pas le contrôle', // TODO: vérifier la traduction + hasnot: "Vous n'avez pas le contrôle", // TODO: vérifier la traduction } export const locks = { @@ -124,4 +124,7 @@ export const files = { downloads: 'Téléchargements', uploads: 'Envois', upload_here: 'Cliquez ou faites glisser les fichiers ici pour les envoyer', + delete: 'Supprimer', + delete_title: 'Supprimer "{name}" ?', + delete_confirm: 'Voulez-vous vraiment supprimer ce fichier ?', } diff --git a/client/src/locale/id-id.ts b/client/src/locale/id-id.ts index 863b61f8..3106ac03 100644 --- a/client/src/locale/id-id.ts +++ b/client/src/locale/id-id.ts @@ -124,4 +124,7 @@ export const files = { downloads: 'Unduhan', uploads: 'Unggahan', upload_here: 'Klik atau seret berkas ke sini untuk mengunggah', + delete: 'Hapus', + delete_title: 'Hapus "{name}"?', + delete_confirm: 'Apakah Anda benar-benar ingin menghapus berkas ini?', } diff --git a/client/src/locale/ja-jp.ts b/client/src/locale/ja-jp.ts index e2b31a35..850ce716 100644 --- a/client/src/locale/ja-jp.ts +++ b/client/src/locale/ja-jp.ts @@ -124,4 +124,7 @@ export const files = { downloads: 'ダウンロード', uploads: 'アップロード', upload_here: 'アップロードするにはここをクリックするかファイルをドラッグしてください', + delete: '削除', + delete_title: '"{name}" を削除しますか?', + delete_confirm: '本当にこのファイルを削除しますか?', } diff --git a/client/src/locale/ko-kr.ts b/client/src/locale/ko-kr.ts index 71b7ab50..99d7074d 100644 --- a/client/src/locale/ko-kr.ts +++ b/client/src/locale/ko-kr.ts @@ -124,4 +124,7 @@ export const files = { downloads: '다운로드', uploads: '업로드', upload_here: '업로드할 파일을 여기로 클릭하거나 드래그하세요', + delete: '삭제', + delete_title: '"{name}" 을(를) 삭제하시겠습니까?', + delete_confirm: '정말로 이 파일을 삭제하시겠습니까?', } diff --git a/client/src/locale/nb-no.ts b/client/src/locale/nb-no.ts index b5c70f35..0b75d553 100644 --- a/client/src/locale/nb-no.ts +++ b/client/src/locale/nb-no.ts @@ -124,4 +124,7 @@ export const files = { downloads: 'Nedlastinger', uploads: 'Opplastinger', upload_here: 'Klikk eller dra filer hit for å laste opp', + delete: 'Slett', + delete_title: 'Slette "{name}"?', + delete_confirm: 'Er du sikker på at du vil slette denne filen?', } diff --git a/client/src/locale/pl-pl.ts b/client/src/locale/pl-pl.ts index ccad05e7..3cb123e8 100644 --- a/client/src/locale/pl-pl.ts +++ b/client/src/locale/pl-pl.ts @@ -124,4 +124,7 @@ export const files = { downloads: 'Pobrane pliki', uploads: 'Wysyłane pliki', upload_here: 'Kliknij lub przeciągnij pliki tutaj, aby przesłać', + delete: 'Usuń', + delete_title: 'Usunąć "{name}"?', + delete_confirm: 'Czy na pewno chcesz usunąć ten plik?', } diff --git a/client/src/locale/ru-ru.ts b/client/src/locale/ru-ru.ts index f4e15f97..91e40a93 100644 --- a/client/src/locale/ru-ru.ts +++ b/client/src/locale/ru-ru.ts @@ -124,4 +124,7 @@ export const files = { downloads: 'Загрузки', uploads: 'Загрузить', upload_here: 'Нажмите или перетащите сюда файлы для загрузки', + delete: 'Удалить', + delete_title: 'Удалить "{name}"?', + delete_confirm: 'Вы действительно хотите удалить этот файл?', } diff --git a/client/src/locale/sk-sk.ts b/client/src/locale/sk-sk.ts index c5e23654..846d4389 100644 --- a/client/src/locale/sk-sk.ts +++ b/client/src/locale/sk-sk.ts @@ -130,4 +130,7 @@ export const files = { downloads: 'Stiahnutia', uploads: 'Nahrávanie', upload_here: 'Kliknutím alebo pretiahnutím súborov sem ich môžete nahrať', + delete: 'Odstrániť', + delete_title: 'Odstrániť "{name}"?', + delete_confirm: 'Naozaj chcete odstrániť tento súbor?', } diff --git a/client/src/locale/sv-se.ts b/client/src/locale/sv-se.ts index 2a25f906..bbbe6a52 100644 --- a/client/src/locale/sv-se.ts +++ b/client/src/locale/sv-se.ts @@ -124,4 +124,7 @@ export const files = { downloads: 'Nedladdningar', uploads: 'Ladda upp', upload_here: 'Klicka eller dra filer hit för att ladda upp dem', + delete: 'Ta bort', + delete_title: 'Ta bort "{name}"?', + delete_confirm: 'Vill du verkligen ta bort den här filen?', } diff --git a/client/src/locale/zh-cn.ts b/client/src/locale/zh-cn.ts index 7bcc0549..d6b0a1b7 100644 --- a/client/src/locale/zh-cn.ts +++ b/client/src/locale/zh-cn.ts @@ -72,12 +72,12 @@ export const locks = { notif_unlocked: '房间已解锁', }, file_transfer: { - lock: '锁定文件传输(对用户)', - unlock: '解锁文件传输(对用户)', - locked: '文件传输已锁定(对用户)', - unlocked: '文件传输已解锁(对用户)', - notif_locked: '已锁定文件传输', - notif_unlocked: '已解锁文件传输', + lock: '锁定文件传输(对用户)', + unlock: '解锁文件传输(对用户)', + locked: '文件传输已锁定(对用户)', + unlocked: '文件传输已解锁(对用户)', + notif_locked: '已锁定文件传输', + notif_unlocked: '已解锁文件传输', }, } @@ -124,4 +124,7 @@ export const files = { downloads: '下载', uploads: '上传', upload_here: '点击或拖动文件到此处上传', + delete: '删除', + delete_title: '删除"{name}"?', + delete_confirm: '确定要删除此文件吗?', } diff --git a/client/src/locale/zh-tw.ts b/client/src/locale/zh-tw.ts index 0bd9f7d4..dddbfd09 100644 --- a/client/src/locale/zh-tw.ts +++ b/client/src/locale/zh-tw.ts @@ -124,4 +124,7 @@ export const files = { downloads: '下載', uploads: '上傳', upload_here: '點選或拖曳檔案至此上傳', + delete: '刪除', + delete_title: '刪除「{name}」?', + delete_confirm: '確定要刪除此檔案嗎?', } diff --git a/client/src/neko/index.ts b/client/src/neko/index.ts index 01695f60..848f8464 100644 --- a/client/src/neko/index.ts +++ b/client/src/neko/index.ts @@ -361,11 +361,18 @@ export class NekoClient extends BaseClient implements EventEmitter { ///////////////////////////// // File Transfer Events ///////////////////////////// - protected [EVENT.FILETRANSFER.LIST]({ cwd, user_download, user_upload, files }: FileTransferListPayload) { + protected [EVENT.FILETRANSFER.LIST]({ + cwd, + user_download, + user_upload, + user_delete, + files, + }: FileTransferListPayload) { this.$accessor.files.setCwd(cwd) this.$accessor.files.setFileList(files) this.$accessor.files.setUserDownload(user_download) this.$accessor.files.setUserUpload(user_upload) + this.$accessor.files.setUserDelete(user_delete) } ///////////////////////////// diff --git a/client/src/neko/messages.ts b/client/src/neko/messages.ts index 1c3546d4..6c73aa0f 100644 --- a/client/src/neko/messages.ts +++ b/client/src/neko/messages.ts @@ -206,6 +206,7 @@ export interface FileTransferListPayload { cwd: string user_download: boolean user_upload: boolean + user_delete: boolean files: FileListItem[] } diff --git a/client/src/store/files.ts b/client/src/store/files.ts index c45a4ac4..361c8ed3 100644 --- a/client/src/store/files.ts +++ b/client/src/store/files.ts @@ -9,6 +9,7 @@ export const state = () => ({ transfers: [] as FileTransfer[], userDownload: false, userUpload: false, + userDelete: false, }) export const getters = getterTree(state, { @@ -32,6 +33,10 @@ export const mutations = mutationTree(state, { state.userUpload = val }, + _setUserDelete(state, val: boolean) { + state.userDelete = val + }, + _addTransfer(state, transfer: FileTransfer) { state.transfers = [...state.transfers, transfer] }, @@ -60,6 +65,10 @@ export const actions = actionTree( accessor.files._setUserUpload(val) }, + setUserDelete(store, val: boolean) { + accessor.files._setUserDelete(val) + }, + addTransfer(store, transfer: FileTransfer) { if (transfer.status !== 'pending') { return diff --git a/server/internal/http/legacy/handler.go b/server/internal/http/legacy/handler.go index 263f70fd..04db30f4 100644 --- a/server/internal/http/legacy/handler.go +++ b/server/internal/http/legacy/handler.go @@ -388,6 +388,34 @@ func (h *LegacyHandler) Route(r types.Router) { return err }) + r.Delete("/file", func(w http.ResponseWriter, r *http.Request) error { + if h.isBanned(r) { + return utils.HttpForbidden("banned ip") + } + + s := h.newSession(r) + + // create a new session + username := r.URL.Query().Get("usr") + password := r.URL.Query().Get("pwd") + err := s.create(username, password) + if err != nil { + return utils.HttpForbidden(err.Error()) + } + defer s.destroy() + + filename := r.URL.Query().Get("filename") + + body, _, err := s.req(http.MethodDelete, "/api/filetransfer?filename="+url.QueryEscape(filename), r.Header, nil) + if err != nil { + return utils.HttpInternalServerError().WithInternalErr(err) + } + + // copy the body to the response writer + _, err = io.Copy(w, body) + return err + }) + r.Get("/health", func(w http.ResponseWriter, r *http.Request) error { _, err := w.Write([]byte("true")) return err diff --git a/server/internal/http/legacy/message/messages.go b/server/internal/http/legacy/message/messages.go index 17d527bd..c835848e 100644 --- a/server/internal/http/legacy/message/messages.go +++ b/server/internal/http/legacy/message/messages.go @@ -113,6 +113,7 @@ type FileTransferList struct { Cwd string `json:"cwd"` UserDownload bool `json:"user_download"` UserUpload bool `json:"user_upload"` + UserDelete bool `json:"user_delete"` Files []types.FileListItem `json:"files"` } diff --git a/server/internal/http/legacy/wstoclient.go b/server/internal/http/legacy/wstoclient.go index d5609d6c..5e319757 100644 --- a/server/internal/http/legacy/wstoclient.go +++ b/server/internal/http/legacy/wstoclient.go @@ -611,6 +611,7 @@ func (s *session) wsToClient(msg []byte) error { Cwd: request.RootDir, UserDownload: request.UserDownload, UserUpload: request.UserUpload, + UserDelete: request.UserDelete, Files: files, }) diff --git a/server/internal/plugins/filetransfer/config.go b/server/internal/plugins/filetransfer/config.go index b84f8b51..c2ad6619 100644 --- a/server/internal/plugins/filetransfer/config.go +++ b/server/internal/plugins/filetransfer/config.go @@ -14,6 +14,7 @@ type Config struct { RefreshInterval time.Duration UserDownload bool UserUpload bool + UserDelete bool } func (Config) Init(cmd *cobra.Command) error { @@ -42,6 +43,11 @@ func (Config) Init(cmd *cobra.Command) error { return err } + cmd.PersistentFlags().Bool("filetransfer.user_delete", false, "allow non-admin users to delete files") + if err := viper.BindPFlag("filetransfer.user_delete", cmd.PersistentFlags().Lookup("filetransfer.user_delete")); err != nil { + return err + } + // v2 config cmd.PersistentFlags().Bool("file_transfer_enabled", false, "enable file transfer feature") @@ -64,6 +70,7 @@ func (s *Config) Set() { s.RefreshInterval = viper.GetDuration("filetransfer.refresh_interval") s.UserDownload = viper.GetBool("filetransfer.user_download") s.UserUpload = viper.GetBool("filetransfer.user_upload") + s.UserDelete = viper.GetBool("filetransfer.user_delete") // v2 config diff --git a/server/internal/plugins/filetransfer/manager.go b/server/internal/plugins/filetransfer/manager.go index 28e2919a..33a717de 100644 --- a/server/internal/plugins/filetransfer/manager.go +++ b/server/internal/plugins/filetransfer/manager.go @@ -107,6 +107,7 @@ func (m *Manager) broadcastUpdate() { RootDir: m.config.RootDir, UserDownload: m.config.UserDownload, UserUpload: m.config.UserUpload, + UserDelete: m.config.UserDelete, Files: fileList, }) } @@ -121,6 +122,7 @@ func (m *Manager) sendUpdate(session types.Session) { RootDir: m.config.RootDir, UserDownload: m.config.UserDownload, UserUpload: m.config.UserUpload, + UserDelete: m.config.UserDelete, Files: fileList, }) } @@ -206,6 +208,59 @@ func (m *Manager) Start() error { return nil } +func (m *Manager) deleteFileHandler(w http.ResponseWriter, r *http.Request) error { + session, ok := auth.GetSession(r) + if !ok { + return utils.HttpUnauthorized("session not found") + } + + enabled, err := m.isEnabledForSession(session) + if err != nil { + return utils.HttpInternalServerError(). + WithInternalErr(err). + Msg("error checking file transfer permissions") + } + + if !enabled { + return utils.HttpForbidden("file transfer is disabled") + } + + if !session.Profile().IsAdmin && !m.config.UserDelete { + return utils.HttpForbidden("file delete is not allowed for non-admin users") + } + + filename := r.URL.Query().Get("filename") + badChars, err := regexp.MatchString(`(?m)\.\.(?:\/|$)`, filename) + if filename == "" || badChars || err != nil { + return utils.HttpBadRequest(). + WithInternalErr(err). + Msg("bad filename") + } + + filename = filepath.Clean(filename) + filename = filepath.Base(filename) + filePath := filepath.Join(m.config.RootDir, filename) + + if err := os.Remove(filePath); err != nil { + if os.IsNotExist(err) { + return utils.HttpNotFound("file not found") + } + return utils.HttpInternalServerError(). + WithInternalErr(err). + Msg("error deleting file") + } + + err, changed := m.refresh() + if err != nil { + m.logger.Err(err).Msg("unable to refresh file list after delete") + } + if changed { + m.broadcastUpdate() + } + + return nil +} + func (m *Manager) Shutdown() error { close(m.shutdown) return nil @@ -214,6 +269,7 @@ func (m *Manager) Shutdown() error { func (m *Manager) Route(r types.Router) { r.Get("/", m.downloadFileHandler) r.Post("/", m.uploadFileHandler) + r.Delete("/", m.deleteFileHandler) } func (m *Manager) WebSocketHandler(session types.Session, msg types.WebSocketMessage) bool { diff --git a/server/internal/plugins/filetransfer/types.go b/server/internal/plugins/filetransfer/types.go index dcb6ec50..f15da854 100644 --- a/server/internal/plugins/filetransfer/types.go +++ b/server/internal/plugins/filetransfer/types.go @@ -15,6 +15,7 @@ type Message struct { RootDir string `json:"root_dir"` UserDownload bool `json:"user_download"` UserUpload bool `json:"user_upload"` + UserDelete bool `json:"user_delete"` Files []Item `json:"files"` } diff --git a/webpage/docs/configuration/plugins.md b/webpage/docs/configuration/plugins.md index b511f21b..8a5fc5a4 100644 --- a/webpage/docs/configuration/plugins.md +++ b/webpage/docs/configuration/plugins.md @@ -61,6 +61,7 @@ The file transfer plugin is a simple pre-loaded internal plugin that allows you }, 'filetransfer.user_download': false, 'filetransfer.user_upload': false, + 'filetransfer.user_delete': false, }} /> - enables the file transfer support. If set to `false`, the file transfer is disabled. @@ -68,6 +69,7 @@ The file transfer plugin is a simple pre-loaded internal plugin that allows you - refers to the interval at which the file list is refreshed. - allows non-admin users to download files. If set to `false` (default), only admins can download files. - allows non-admin users to upload files. If set to `false` (default), only admins can upload files. +- allows non-admin users to delete files. If set to `false` (default), only admins can delete files. The file transfer plugin extends user profile and room settings by adding the following fields: