mirror of
https://github.com/mattermost/mattermost.git
synced 2026-04-15 22:12:19 -04:00
* feat: filewillbedonwloaded hook * feat: error popup * chore: make generated pluginapi * tests * feat: different errors for different download types * feat: allow toast positions * fix: avoid using deprecated i18n function * feat: add plugin API to show toasts * feat: downloadType parameter * tests: updated tests * chore: make check-style * chore: i18n * chore: missing fields in tests * chore: sorted i18n for webapp * chore: run mmjstool * test: fixed webapp tests with new changes * test: missing mocks * fix: ensure one-file attachments (previews) are handler properly as thumbnails * chore: lint * test: added new logic to tests * chore: lint * Add SendToastMessage API and FileWillBeDownloaded hook - Introduced SendToastMessage method for sending toast notifications to users with customizable options. - Added FileWillBeDownloaded hook to handle file download requests, allowing plugins to control access to files. - Updated related types and constants for file download handling. - Enhanced PluginSettings to include HookTimeoutSeconds for better timeout management. * Update webapp/channels/src/components/single_image_view/single_image_view.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * chore: copilot reviews * test: head requests * chore: linted the webapp * tests: fixed path * test: fixed mocked args * allow sending message to a connection directly * fix: hook thread safety * chore: formatting * chore: remove configuration from system console * chore: release version * chore: update signature * chore: update release version * chore: addressed comments * fix: update file rejection handling to use 403 Forbidden status and include rejection reason header * Fix nil pointer panic in runFileWillBeDownloadedHook The atomic.Value in runFileWillBeDownloadedHook can be nil if no plugins implement the FileWillBeDownloaded hook. This causes a panic when trying to assert the nil interface to string. This fix adds a nil check before the type assertion, defaulting to an empty string (which allows the download) when no hooks have run. Fixes: - TestUploadDataMultipart/success panic - TestUploadDataMultipart/resume_success panic * test: move the logout test last * chore: restored accidential deletion * chore: lint * chore: make generated * refactor: move websocket events to new package * chore: go vet * chore: missing mock * chore: revert incorrect fmt * chore: import ordering * chore: npm i18n-extract * chore: update constants.tsx from master * chore: make i18n-extract * revert: conflict merge * fix: add missing isFileRejected prop to SingleImageView tests * fix: mock fetch in SingleImageView tests for async thumbnail check The component now performs an async fetch to check thumbnail availability before rendering. Tests need to mock fetch and use waitFor to handle the async state updates. * refactor: move hook logic to app layer * chore: update version to 11.5 * Scope file download rejection toast to the requesting connection Thread the Connection-Id header through RunFileWillBeDownloadedHook and sendFileDownloadRejectedEvent so the WebSocket event is sent only to the connection that initiated the download, instead of all connections for the user. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
191 lines
5.7 KiB
Go
191 lines
5.7 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package model
|
|
|
|
import (
|
|
"mime"
|
|
"net/http"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
FileinfoSortByCreated = "CreateAt"
|
|
FileinfoSortBySize = "Size"
|
|
)
|
|
|
|
// FileDownloadType represents the type of file download or access being performed.
|
|
type FileDownloadType string
|
|
|
|
const (
|
|
// FileDownloadTypeFile represents a full file download request.
|
|
FileDownloadTypeFile FileDownloadType = "file"
|
|
// FileDownloadTypeThumbnail represents a thumbnail image request.
|
|
FileDownloadTypeThumbnail FileDownloadType = "thumbnail"
|
|
// FileDownloadTypePreview represents a preview image request.
|
|
FileDownloadTypePreview FileDownloadType = "preview"
|
|
// FileDownloadTypePublic represents a public link access (unauthenticated).
|
|
FileDownloadTypePublic FileDownloadType = "public"
|
|
)
|
|
|
|
// GetFileInfosOptions contains options for getting FileInfos
|
|
type GetFileInfosOptions struct {
|
|
// UserIds optionally limits the FileInfos to those created by the given users.
|
|
UserIds []string `json:"user_ids"`
|
|
// ChannelIds optionally limits the FileInfos to those created in the given channels.
|
|
ChannelIds []string `json:"channel_ids"`
|
|
// Since optionally limits FileInfos to those created at or after the given time, specified as Unix time in milliseconds.
|
|
Since int64 `json:"since"`
|
|
// IncludeDeleted if set includes deleted FileInfos.
|
|
IncludeDeleted bool `json:"include_deleted"`
|
|
// SortBy sorts the FileInfos by this field. The default is to sort by date created.
|
|
SortBy string `json:"sort_by"`
|
|
// SortDescending changes the sort direction to descending order when true.
|
|
SortDescending bool `json:"sort_descending"`
|
|
}
|
|
|
|
type FileInfo struct {
|
|
Id string `json:"id"`
|
|
CreatorId string `json:"user_id"`
|
|
PostId string `json:"post_id,omitempty"`
|
|
// ChannelId is the denormalized value from the corresponding post. Note that this value is
|
|
// potentially distinct from the ChannelId provided when the file is first uploaded and
|
|
// used to organize the directories in the file store, since in theory that same file
|
|
// could be attached to a post from a different channel (or not attached to a post at all).
|
|
ChannelId string `json:"channel_id"`
|
|
CreateAt int64 `json:"create_at"`
|
|
UpdateAt int64 `json:"update_at"`
|
|
DeleteAt int64 `json:"delete_at"`
|
|
Path string `json:"-"` // not sent back to the client
|
|
ThumbnailPath string `json:"-"` // not sent back to the client
|
|
PreviewPath string `json:"-"` // not sent back to the client
|
|
Name string `json:"name"`
|
|
Extension string `json:"extension"`
|
|
Size int64 `json:"size"`
|
|
MimeType string `json:"mime_type"`
|
|
Width int `json:"width,omitempty"`
|
|
Height int `json:"height,omitempty"`
|
|
HasPreviewImage bool `json:"has_preview_image,omitempty"`
|
|
MiniPreview *[]byte `json:"mini_preview"` // pointer to distinguish NULL (no preview) from empty data
|
|
Content string `json:"-"`
|
|
RemoteId *string `json:"remote_id"`
|
|
Archived bool `json:"archived"`
|
|
}
|
|
|
|
func (fi *FileInfo) Auditable() map[string]any {
|
|
return map[string]any{
|
|
"id": fi.Id,
|
|
"creator_id": fi.CreatorId,
|
|
"post_id": fi.PostId,
|
|
"channel_id": fi.ChannelId,
|
|
"create_at": fi.CreateAt,
|
|
"update_at": fi.UpdateAt,
|
|
"delete_at": fi.DeleteAt,
|
|
"name": fi.Name,
|
|
"extension": fi.Extension,
|
|
"size": fi.Size,
|
|
}
|
|
}
|
|
|
|
func (fi *FileInfo) PreSave() {
|
|
if fi.Id == "" {
|
|
fi.Id = NewId()
|
|
}
|
|
|
|
if fi.CreateAt == 0 {
|
|
fi.CreateAt = GetMillis()
|
|
}
|
|
|
|
if fi.UpdateAt < fi.CreateAt {
|
|
fi.UpdateAt = fi.CreateAt
|
|
}
|
|
|
|
if fi.RemoteId == nil {
|
|
fi.RemoteId = NewPointer("")
|
|
}
|
|
}
|
|
|
|
func (fi *FileInfo) IsValid() *AppError {
|
|
if !IsValidId(fi.Id) {
|
|
return NewAppError("FileInfo.IsValid", "model.file_info.is_valid.id.app_error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if !IsValidId(fi.CreatorId) && (fi.CreatorId != "nouser" && fi.CreatorId != BookmarkFileOwner) {
|
|
return NewAppError("FileInfo.IsValid", "model.file_info.is_valid.user_id.app_error", nil, "id="+fi.Id, http.StatusBadRequest)
|
|
}
|
|
|
|
if fi.PostId != "" && !IsValidId(fi.PostId) {
|
|
return NewAppError("FileInfo.IsValid", "model.file_info.is_valid.post_id.app_error", nil, "id="+fi.Id, http.StatusBadRequest)
|
|
}
|
|
|
|
if fi.CreateAt == 0 {
|
|
return NewAppError("FileInfo.IsValid", "model.file_info.is_valid.create_at.app_error", nil, "id="+fi.Id, http.StatusBadRequest)
|
|
}
|
|
|
|
if fi.UpdateAt == 0 {
|
|
return NewAppError("FileInfo.IsValid", "model.file_info.is_valid.update_at.app_error", nil, "id="+fi.Id, http.StatusBadRequest)
|
|
}
|
|
|
|
if fi.Path == "" {
|
|
return NewAppError("FileInfo.IsValid", "model.file_info.is_valid.path.app_error", nil, "id="+fi.Id, http.StatusBadRequest)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (fi *FileInfo) IsImage() bool {
|
|
return strings.HasPrefix(fi.MimeType, "image")
|
|
}
|
|
|
|
func (fi *FileInfo) IsSvg() bool {
|
|
return fi.MimeType == "image/svg+xml"
|
|
}
|
|
|
|
func NewInfo(name string) *FileInfo {
|
|
info := &FileInfo{
|
|
Name: name,
|
|
}
|
|
|
|
extension := strings.ToLower(filepath.Ext(name))
|
|
info.MimeType = mime.TypeByExtension(extension)
|
|
|
|
if extension != "" && extension[0] == '.' {
|
|
// The client expects a file extension without the leading period
|
|
info.Extension = extension[1:]
|
|
} else {
|
|
info.Extension = extension
|
|
}
|
|
|
|
return info
|
|
}
|
|
|
|
func GetEtagForFileInfos(infos []*FileInfo) string {
|
|
if len(infos) == 0 {
|
|
return Etag()
|
|
}
|
|
|
|
var maxUpdateAt int64
|
|
|
|
for _, info := range infos {
|
|
if info.UpdateAt > maxUpdateAt {
|
|
maxUpdateAt = info.UpdateAt
|
|
}
|
|
}
|
|
|
|
return Etag(infos[0].PostId, maxUpdateAt)
|
|
}
|
|
|
|
func (fi *FileInfo) MakeContentInaccessible() {
|
|
if fi == nil {
|
|
return
|
|
}
|
|
|
|
fi.Archived = true
|
|
fi.Content = ""
|
|
fi.HasPreviewImage = false
|
|
fi.MiniPreview = nil
|
|
fi.Path = ""
|
|
fi.PreviewPath = ""
|
|
fi.ThumbnailPath = ""
|
|
}
|