Full Mattermost server source with integrated Community Enterprise features. Includes vendor directory for offline/air-gapped builds. Structure: - enterprise-impl/: Enterprise feature implementations - enterprise-community/: Init files that register implementations - enterprise/: Bridge imports (community_imports.go) - vendor/: All dependencies for offline builds Build (online): go build ./cmd/mattermost Build (offline/air-gapped): go build -mod=vendor ./cmd/mattermost 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
908 lines
27 KiB
Go
908 lines
27 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package api4
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/subtle"
|
|
"encoding/json"
|
|
"io"
|
|
"mime"
|
|
"mime/multipart"
|
|
"net/http"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
|
"github.com/mattermost/mattermost/server/v8/channels/app"
|
|
"github.com/mattermost/mattermost/server/v8/channels/utils"
|
|
"github.com/mattermost/mattermost/server/v8/platform/shared/web"
|
|
)
|
|
|
|
const (
|
|
FileTeamId = "noteam"
|
|
|
|
PreviewImageType = "image/jpeg"
|
|
ThumbnailImageType = "image/jpeg"
|
|
)
|
|
|
|
const maxMultipartFormDataBytes = 10 * 1024 // 10Kb
|
|
|
|
func (api *API) InitFile() {
|
|
api.BaseRoutes.Files.Handle("", api.APISessionRequired(uploadFileStream, handlerParamFileAPI)).Methods(http.MethodPost)
|
|
api.BaseRoutes.File.Handle("", api.APISessionRequiredTrustRequester(getFile)).Methods(http.MethodGet)
|
|
api.BaseRoutes.File.Handle("/thumbnail", api.APISessionRequiredTrustRequester(getFileThumbnail)).Methods(http.MethodGet)
|
|
api.BaseRoutes.File.Handle("/link", api.APISessionRequired(getFileLink)).Methods(http.MethodGet)
|
|
api.BaseRoutes.File.Handle("/preview", api.APISessionRequiredTrustRequester(getFilePreview)).Methods(http.MethodGet)
|
|
api.BaseRoutes.File.Handle("/info", api.APISessionRequired(getFileInfo)).Methods(http.MethodGet)
|
|
|
|
api.BaseRoutes.Team.Handle("/files/search", api.APISessionRequiredDisableWhenBusy(searchFilesInTeam)).Methods(http.MethodPost)
|
|
api.BaseRoutes.Files.Handle("/search", api.APISessionRequiredDisableWhenBusy(searchFilesInAllTeams)).Methods(http.MethodPost)
|
|
|
|
api.BaseRoutes.PublicFile.Handle("", api.APIHandler(getPublicFile)).Methods(http.MethodGet, http.MethodHead)
|
|
}
|
|
|
|
func parseMultipartRequestHeader(req *http.Request) (boundary string, err error) {
|
|
v := req.Header.Get("Content-Type")
|
|
if v == "" {
|
|
return "", http.ErrNotMultipart
|
|
}
|
|
d, params, err := mime.ParseMediaType(v)
|
|
if err != nil || d != "multipart/form-data" {
|
|
return "", http.ErrNotMultipart
|
|
}
|
|
boundary, ok := params["boundary"]
|
|
if !ok {
|
|
return "", http.ErrMissingBoundary
|
|
}
|
|
|
|
return boundary, nil
|
|
}
|
|
|
|
func multipartReader(req *http.Request, stream io.Reader) (*multipart.Reader, error) {
|
|
boundary, err := parseMultipartRequestHeader(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if stream != nil {
|
|
return multipart.NewReader(stream, boundary), nil
|
|
}
|
|
|
|
return multipart.NewReader(req.Body, boundary), nil
|
|
}
|
|
|
|
func uploadFileStream(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
if !*c.App.Config().FileSettings.EnableFileAttachments {
|
|
c.Err = model.NewAppError("uploadFileStream",
|
|
"api.file.attachments.disabled.app_error",
|
|
nil, "", http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
// Parse the post as a regular form (in practice, use the URL values
|
|
// since we never expect a real application/x-www-form-urlencoded
|
|
// form).
|
|
if r.Form == nil {
|
|
err := r.ParseForm()
|
|
if err != nil {
|
|
c.Err = model.NewAppError("uploadFileStream",
|
|
"api.file.upload_file.read_request.app_error",
|
|
nil, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
}
|
|
|
|
if r.ContentLength == 0 {
|
|
c.Err = model.NewAppError("uploadFileStream",
|
|
"api.file.upload_file.read_request.app_error",
|
|
nil, "Content-Length should not be 0", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
timestamp := time.Now()
|
|
var fileUploadResponse *model.FileUploadResponse
|
|
|
|
_, err := parseMultipartRequestHeader(r)
|
|
switch err {
|
|
case nil:
|
|
fileUploadResponse = uploadFileMultipart(c, r, nil, timestamp)
|
|
|
|
case http.ErrNotMultipart:
|
|
fileUploadResponse = uploadFileSimple(c, r, timestamp)
|
|
|
|
default:
|
|
c.Err = model.NewAppError("uploadFileStream",
|
|
"api.file.upload_file.read_request.app_error",
|
|
nil, err.Error(), http.StatusBadRequest)
|
|
}
|
|
if c.Err != nil {
|
|
return
|
|
}
|
|
|
|
// Write the response values to the output upon return
|
|
w.WriteHeader(http.StatusCreated)
|
|
if err := json.NewEncoder(w).Encode(fileUploadResponse); err != nil {
|
|
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
|
}
|
|
}
|
|
|
|
// uploadFileSimple uploads a file from a simple POST with the file in the request body
|
|
func uploadFileSimple(c *Context, r *http.Request, timestamp time.Time) *model.FileUploadResponse {
|
|
// Simple POST with the file in the body and all metadata in the args.
|
|
c.RequireChannelId()
|
|
c.RequireFilename()
|
|
if c.Err != nil {
|
|
return nil
|
|
}
|
|
|
|
auditRec := c.MakeAuditRecord(model.AuditEventUploadFileSimple, model.AuditStatusFail)
|
|
defer c.LogAuditRec(auditRec)
|
|
model.AddEventParameterToAuditRec(auditRec, "channel_id", c.Params.ChannelId)
|
|
|
|
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionUploadFile) {
|
|
c.SetPermissionError(model.PermissionUploadFile)
|
|
return nil
|
|
}
|
|
|
|
channel, err := c.App.GetChannel(c.AppContext, c.Params.ChannelId)
|
|
if err != nil {
|
|
c.Err = model.NewAppError("uploadFileSimple",
|
|
"api.file.upload_file.get_channel.app_error",
|
|
nil, err.Error(), http.StatusBadRequest)
|
|
return nil
|
|
}
|
|
|
|
restrictDM, appErr := c.App.CheckIfChannelIsRestrictedDM(c.AppContext, channel)
|
|
if appErr != nil {
|
|
c.Err = appErr
|
|
return nil
|
|
}
|
|
|
|
if restrictDM {
|
|
c.Err = model.NewAppError("uploadFileSimple", "api.file.upload_file.restricted_dm.error", nil, "", http.StatusBadRequest)
|
|
return nil
|
|
}
|
|
|
|
clientId := r.Form.Get("client_id")
|
|
model.AddEventParameterToAuditRec(auditRec, "client_id", clientId)
|
|
|
|
creatorId := c.AppContext.Session().UserId
|
|
if isBookmark, err := strconv.ParseBool(r.URL.Query().Get(model.BookmarkFileOwner)); err == nil && isBookmark {
|
|
creatorId = model.BookmarkFileOwner
|
|
model.AddEventParameterToAuditRec(auditRec, model.BookmarkFileOwner, true)
|
|
}
|
|
|
|
info, appErr := c.App.UploadFileX(c.AppContext, c.Params.ChannelId, c.Params.Filename, r.Body,
|
|
app.UploadFileSetTeamId(FileTeamId),
|
|
app.UploadFileSetUserId(creatorId),
|
|
app.UploadFileSetTimestamp(timestamp),
|
|
app.UploadFileSetContentLength(r.ContentLength),
|
|
app.UploadFileSetClientId(clientId))
|
|
if appErr != nil {
|
|
c.Err = appErr
|
|
return nil
|
|
}
|
|
model.AddEventParameterAuditableToAuditRec(auditRec, "file", info)
|
|
|
|
fileUploadResponse := &model.FileUploadResponse{
|
|
FileInfos: []*model.FileInfo{info},
|
|
}
|
|
if clientId != "" {
|
|
fileUploadResponse.ClientIds = []string{clientId}
|
|
}
|
|
auditRec.Success()
|
|
return fileUploadResponse
|
|
}
|
|
|
|
// uploadFileMultipart parses and uploads file(s) from a mime/multipart
|
|
// request. It pre-buffers up to the first part which is either the (a)
|
|
// `channel_id` value, or (b) a file. Then in case of (a) it re-processes the
|
|
// entire message recursively calling itself in stream mode. In case of (b) it
|
|
// calls to uploadFileMultipartLegacy for legacy support
|
|
func uploadFileMultipart(c *Context, r *http.Request, asStream io.Reader, timestamp time.Time) *model.FileUploadResponse {
|
|
expectClientIds := true
|
|
var clientIds []string
|
|
resp := model.FileUploadResponse{
|
|
FileInfos: []*model.FileInfo{},
|
|
ClientIds: []string{},
|
|
}
|
|
|
|
var buf *bytes.Buffer
|
|
var mr *multipart.Reader
|
|
var err error
|
|
if asStream == nil {
|
|
// We need to buffer until we get the channel_id, or the first file.
|
|
buf = &bytes.Buffer{}
|
|
mr, err = multipartReader(r, io.TeeReader(r.Body, buf))
|
|
} else {
|
|
mr, err = multipartReader(r, asStream)
|
|
}
|
|
if err != nil {
|
|
c.Err = model.NewAppError("uploadFileMultipart",
|
|
"api.file.upload_file.read_request.app_error",
|
|
nil, err.Error(), http.StatusBadRequest)
|
|
return nil
|
|
}
|
|
|
|
nFiles := 0
|
|
NextPart:
|
|
for {
|
|
part, err := mr.NextPart()
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
if err != nil {
|
|
c.Err = model.NewAppError("uploadFileMultipart",
|
|
"api.file.upload_file.read_request.app_error",
|
|
nil, err.Error(), http.StatusBadRequest)
|
|
return nil
|
|
}
|
|
|
|
// Parse any form fields in the multipart.
|
|
formname := part.FormName()
|
|
if formname == "" {
|
|
continue
|
|
}
|
|
filename := part.FileName()
|
|
if filename == "" {
|
|
var b bytes.Buffer
|
|
_, err = io.CopyN(&b, part, maxMultipartFormDataBytes)
|
|
if err != nil && err != io.EOF {
|
|
c.Err = model.NewAppError("uploadFileMultipart",
|
|
"api.file.upload_file.read_form_value.app_error",
|
|
map[string]any{"Formname": formname},
|
|
err.Error(), http.StatusBadRequest)
|
|
return nil
|
|
}
|
|
v := b.String()
|
|
|
|
switch formname {
|
|
case "channel_id":
|
|
if c.Params.ChannelId != "" && c.Params.ChannelId != v {
|
|
c.Err = model.NewAppError("uploadFileMultipart",
|
|
"api.file.upload_file.multiple_channel_ids.app_error",
|
|
nil, "", http.StatusBadRequest)
|
|
return nil
|
|
}
|
|
if v != "" {
|
|
c.Params.ChannelId = v
|
|
}
|
|
|
|
// Got channel_id, re-process the entire post
|
|
// in the streaming mode.
|
|
if asStream == nil {
|
|
return uploadFileMultipart(c, r, io.MultiReader(buf, r.Body), timestamp)
|
|
}
|
|
|
|
case "client_ids":
|
|
if !expectClientIds {
|
|
c.SetInvalidParam("client_ids")
|
|
return nil
|
|
}
|
|
clientIds = append(clientIds, v)
|
|
|
|
default:
|
|
c.SetInvalidParam(formname)
|
|
return nil
|
|
}
|
|
|
|
continue NextPart
|
|
}
|
|
|
|
isBookmark := false
|
|
if val, queryErr := strconv.ParseBool(r.URL.Query().Get(model.BookmarkFileOwner)); queryErr == nil {
|
|
isBookmark = val
|
|
}
|
|
|
|
// A file part.
|
|
|
|
if c.Params.ChannelId == "" && asStream == nil {
|
|
// Got file before channel_id, fall back to legacy buffered mode
|
|
mr, err = multipartReader(r, io.MultiReader(buf, r.Body))
|
|
if err != nil {
|
|
c.Err = model.NewAppError("uploadFileMultipart",
|
|
"api.file.upload_file.read_request.app_error",
|
|
nil, err.Error(), http.StatusBadRequest)
|
|
return nil
|
|
}
|
|
|
|
return uploadFileMultipartLegacy(c, mr, timestamp, isBookmark)
|
|
}
|
|
|
|
c.RequireChannelId()
|
|
if c.Err != nil {
|
|
return nil
|
|
}
|
|
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionUploadFile) {
|
|
c.SetPermissionError(model.PermissionUploadFile)
|
|
return nil
|
|
}
|
|
|
|
channel, appErr := c.App.GetChannel(c.AppContext, c.Params.ChannelId)
|
|
if appErr != nil {
|
|
c.Err = model.NewAppError("uploadFileMultipart",
|
|
"api.file.upload_file.get_channel.app_error",
|
|
nil, appErr.Error(), http.StatusBadRequest)
|
|
return nil
|
|
}
|
|
|
|
restrictDM, appErr := c.App.CheckIfChannelIsRestrictedDM(c.AppContext, channel)
|
|
if appErr != nil {
|
|
c.Err = appErr
|
|
return nil
|
|
}
|
|
|
|
if restrictDM {
|
|
c.Err = model.NewAppError("uploadFileSimple", "api.file.upload_file.restricted_dm.error", nil, "", http.StatusBadRequest)
|
|
return nil
|
|
}
|
|
|
|
// If there's no clientIds when the first file comes, expect
|
|
// none later.
|
|
if nFiles == 0 && len(clientIds) == 0 {
|
|
expectClientIds = false
|
|
}
|
|
|
|
// Must have a exactly one client ID for each file.
|
|
clientId := ""
|
|
if expectClientIds {
|
|
if nFiles >= len(clientIds) {
|
|
c.SetInvalidParam("client_ids")
|
|
return nil
|
|
}
|
|
|
|
clientId = clientIds[nFiles]
|
|
}
|
|
|
|
auditRec := c.MakeAuditRecord(model.AuditEventUploadFileMultipart, model.AuditStatusFail)
|
|
model.AddEventParameterToAuditRec(auditRec, "channel_id", c.Params.ChannelId)
|
|
model.AddEventParameterToAuditRec(auditRec, "client_id", clientId)
|
|
|
|
creatorId := c.AppContext.Session().UserId
|
|
if isBookmark {
|
|
creatorId = model.BookmarkFileOwner
|
|
model.AddEventParameterToAuditRec(auditRec, model.BookmarkFileOwner, true)
|
|
}
|
|
|
|
info, appErr := c.App.UploadFileX(c.AppContext, c.Params.ChannelId, filename, part,
|
|
app.UploadFileSetTeamId(FileTeamId),
|
|
app.UploadFileSetUserId(creatorId),
|
|
app.UploadFileSetTimestamp(timestamp),
|
|
app.UploadFileSetContentLength(-1),
|
|
app.UploadFileSetClientId(clientId))
|
|
if appErr != nil {
|
|
c.Err = appErr
|
|
c.LogAuditRec(auditRec)
|
|
return nil
|
|
}
|
|
model.AddEventParameterAuditableToAuditRec(auditRec, "file", info)
|
|
|
|
auditRec.Success()
|
|
c.LogAuditRec(auditRec)
|
|
|
|
// add to the response
|
|
resp.FileInfos = append(resp.FileInfos, info)
|
|
if expectClientIds {
|
|
resp.ClientIds = append(resp.ClientIds, clientId)
|
|
}
|
|
|
|
nFiles++
|
|
}
|
|
|
|
// Verify that the number of ClientIds matched the number of files.
|
|
if expectClientIds && len(clientIds) != nFiles {
|
|
c.Err = model.NewAppError("uploadFileMultipart",
|
|
"api.file.upload_file.incorrect_number_of_client_ids.app_error",
|
|
map[string]any{"NumClientIds": len(clientIds), "NumFiles": nFiles},
|
|
"", http.StatusBadRequest)
|
|
return nil
|
|
}
|
|
|
|
return &resp
|
|
}
|
|
|
|
// uploadFileMultipartLegacy reads, buffers, and then uploads the message,
|
|
// borrowing from http.ParseMultipartForm. If successful it returns a
|
|
// *model.FileUploadResponse filled in with the individual model.FileInfo's.
|
|
func uploadFileMultipartLegacy(c *Context, mr *multipart.Reader,
|
|
timestamp time.Time, isBookmark bool) *model.FileUploadResponse {
|
|
// Parse the entire form.
|
|
form, err := mr.ReadForm(*c.App.Config().FileSettings.MaxFileSize)
|
|
if err != nil {
|
|
c.Err = model.NewAppError("uploadFileMultipartLegacy",
|
|
"api.file.upload_file.read_request.app_error",
|
|
nil, err.Error(), http.StatusInternalServerError)
|
|
return nil
|
|
}
|
|
|
|
// get and validate the channel Id, permission to upload there.
|
|
if len(form.Value["channel_id"]) == 0 {
|
|
c.SetInvalidParam("channel_id")
|
|
return nil
|
|
}
|
|
channelId := form.Value["channel_id"][0]
|
|
c.Params.ChannelId = channelId
|
|
c.RequireChannelId()
|
|
if c.Err != nil {
|
|
return nil
|
|
}
|
|
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channelId, model.PermissionUploadFile) {
|
|
c.SetPermissionError(model.PermissionUploadFile)
|
|
return nil
|
|
}
|
|
|
|
// Check that we have either no client IDs, or one per file.
|
|
clientIds := form.Value["client_ids"]
|
|
fileHeaders := form.File["files"]
|
|
if len(clientIds) != 0 && len(clientIds) != len(fileHeaders) {
|
|
c.Err = model.NewAppError("uploadFilesMultipartBuffered",
|
|
"api.file.upload_file.incorrect_number_of_client_ids.app_error",
|
|
map[string]any{"NumClientIds": len(clientIds), "NumFiles": len(fileHeaders)},
|
|
"", http.StatusBadRequest)
|
|
return nil
|
|
}
|
|
|
|
resp := model.FileUploadResponse{
|
|
FileInfos: []*model.FileInfo{},
|
|
ClientIds: []string{},
|
|
}
|
|
|
|
for i, fileHeader := range fileHeaders {
|
|
f, err := fileHeader.Open()
|
|
if err != nil {
|
|
c.Err = model.NewAppError("uploadFileMultipartLegacy",
|
|
"api.file.upload_file.read_request.app_error",
|
|
nil, err.Error(), http.StatusBadRequest)
|
|
return nil
|
|
}
|
|
|
|
clientId := ""
|
|
if len(clientIds) > 0 {
|
|
clientId = clientIds[i]
|
|
}
|
|
|
|
auditRec := c.MakeAuditRecord(model.AuditEventUploadFileMultipartLegacy, model.AuditStatusFail)
|
|
defer c.LogAuditRec(auditRec)
|
|
model.AddEventParameterToAuditRec(auditRec, "channel_id", channelId)
|
|
model.AddEventParameterToAuditRec(auditRec, "client_id", clientId)
|
|
|
|
creatorId := c.AppContext.Session().UserId
|
|
if isBookmark {
|
|
creatorId = model.BookmarkFileOwner
|
|
model.AddEventParameterToAuditRec(auditRec, model.BookmarkFileOwner, true)
|
|
}
|
|
|
|
info, appErr := c.App.UploadFileX(c.AppContext, c.Params.ChannelId, fileHeader.Filename, f,
|
|
app.UploadFileSetTeamId(FileTeamId),
|
|
app.UploadFileSetUserId(creatorId),
|
|
app.UploadFileSetTimestamp(timestamp),
|
|
app.UploadFileSetContentLength(-1),
|
|
app.UploadFileSetClientId(clientId))
|
|
f.Close()
|
|
if appErr != nil {
|
|
c.Err = appErr
|
|
c.LogAuditRec(auditRec)
|
|
return nil
|
|
}
|
|
model.AddEventParameterAuditableToAuditRec(auditRec, "file", info)
|
|
|
|
auditRec.Success()
|
|
c.LogAuditRec(auditRec)
|
|
|
|
resp.FileInfos = append(resp.FileInfos, info)
|
|
if clientId != "" {
|
|
resp.ClientIds = append(resp.ClientIds, clientId)
|
|
}
|
|
}
|
|
|
|
return &resp
|
|
}
|
|
|
|
func getFile(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
c.RequireFileId()
|
|
if c.Err != nil {
|
|
return
|
|
}
|
|
|
|
forceDownload, _ := strconv.ParseBool(r.URL.Query().Get("download"))
|
|
|
|
auditRec := c.MakeAuditRecord(model.AuditEventGetFile, model.AuditStatusFail)
|
|
defer c.LogAuditRec(auditRec)
|
|
model.AddEventParameterToAuditRec(auditRec, "force_download", forceDownload)
|
|
|
|
fileInfos, storeErr := c.App.Srv().Store().FileInfo().GetByIds([]string{c.Params.FileId}, true, true)
|
|
if storeErr != nil {
|
|
c.Err = model.NewAppError("getFile", "api.file.get_file_info.app_error", nil, "", http.StatusInternalServerError)
|
|
setInaccessibleFileHeader(w, c.Err)
|
|
return
|
|
} else if len(fileInfos) == 0 {
|
|
c.Err = model.NewAppError("getFile", "api.file.get_file_info.app_error", nil, "", http.StatusNotFound)
|
|
setInaccessibleFileHeader(w, c.Err)
|
|
return
|
|
}
|
|
|
|
fileInfo := fileInfos[0]
|
|
|
|
channel, err := c.App.GetChannel(c.AppContext, fileInfo.ChannelId)
|
|
if err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
|
|
isContentReviewer := false
|
|
asContentReviewer, _ := strconv.ParseBool(r.URL.Query().Get(model.AsContentReviewerParam))
|
|
if asContentReviewer {
|
|
requireContentFlaggingEnabled(c)
|
|
if c.Err != nil {
|
|
return
|
|
}
|
|
|
|
flaggedPostId := r.URL.Query().Get("flagged_post_id")
|
|
requireFlaggedPost(c, flaggedPostId)
|
|
if c.Err != nil {
|
|
return
|
|
}
|
|
|
|
if flaggedPostId != fileInfo.PostId {
|
|
c.Err = model.NewAppError("getFile", "api.file.get_file.invalid_flagged_post.app_error", nil, "file_id="+fileInfo.Id+", flagged_post_id="+flaggedPostId, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
requireTeamContentReviewer(c, c.AppContext.Session().UserId, channel.TeamId)
|
|
if c.Err != nil {
|
|
return
|
|
}
|
|
|
|
isContentReviewer = true
|
|
}
|
|
|
|
// at this point we may have fetched a deleted file info and
|
|
// if the user is not a content reviewer, the request should fail as
|
|
// fetching deleted file info is only allowed for content reviewers of the specific post
|
|
if fileInfo.DeleteAt != 0 && !isContentReviewer {
|
|
c.Err = model.NewAppError("getFile", "app.file_info.get.app_error", nil, "", http.StatusNotFound)
|
|
setInaccessibleFileHeader(w, c.Err)
|
|
return
|
|
}
|
|
|
|
model.AddEventParameterAuditableToAuditRec(auditRec, "file", fileInfo)
|
|
|
|
if !isContentReviewer {
|
|
perm := c.App.SessionHasPermissionToReadChannel(c.AppContext, *c.AppContext.Session(), channel)
|
|
if fileInfo.CreatorId == model.BookmarkFileOwner {
|
|
if !perm {
|
|
c.SetPermissionError(model.PermissionReadChannelContent)
|
|
return
|
|
}
|
|
} else if fileInfo.CreatorId != c.AppContext.Session().UserId && !perm {
|
|
c.SetPermissionError(model.PermissionReadChannelContent)
|
|
return
|
|
}
|
|
}
|
|
|
|
fileReader, err := c.App.FileReader(fileInfo.Path)
|
|
if err != nil {
|
|
c.Err = err
|
|
c.Err.StatusCode = http.StatusNotFound
|
|
return
|
|
}
|
|
defer fileReader.Close()
|
|
|
|
auditRec.Success()
|
|
|
|
web.WriteFileResponse(fileInfo.Name, fileInfo.MimeType, fileInfo.Size, time.Unix(0, fileInfo.UpdateAt*int64(1000*1000)), *c.App.Config().ServiceSettings.WebserverMode, fileReader, forceDownload, w, r)
|
|
}
|
|
|
|
func getFileThumbnail(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
c.RequireFileId()
|
|
if c.Err != nil {
|
|
return
|
|
}
|
|
|
|
forceDownload, _ := strconv.ParseBool(r.URL.Query().Get("download"))
|
|
info, err := c.App.GetFileInfo(c.AppContext, c.Params.FileId)
|
|
if err != nil {
|
|
c.Err = err
|
|
setInaccessibleFileHeader(w, err)
|
|
return
|
|
}
|
|
|
|
channel, err := c.App.GetChannel(c.AppContext, info.ChannelId)
|
|
if err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
perm := c.App.SessionHasPermissionToReadChannel(c.AppContext, *c.AppContext.Session(), channel)
|
|
if info.CreatorId == model.BookmarkFileOwner {
|
|
if !perm {
|
|
c.SetPermissionError(model.PermissionReadChannelContent)
|
|
return
|
|
}
|
|
} else if info.CreatorId != c.AppContext.Session().UserId && !perm {
|
|
c.SetPermissionError(model.PermissionReadChannelContent)
|
|
return
|
|
}
|
|
|
|
if info.ThumbnailPath == "" {
|
|
c.Err = model.NewAppError("getFileThumbnail", "api.file.get_file_thumbnail.no_thumbnail.app_error", nil, "file_id="+info.Id, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
fileReader, err := c.App.FileReader(info.ThumbnailPath)
|
|
if err != nil {
|
|
c.Err = err
|
|
c.Err.StatusCode = http.StatusNotFound
|
|
return
|
|
}
|
|
defer fileReader.Close()
|
|
|
|
web.WriteFileResponse(info.Name, ThumbnailImageType, 0, time.Unix(0, info.UpdateAt*int64(1000*1000)), *c.App.Config().ServiceSettings.WebserverMode, fileReader, forceDownload, w, r)
|
|
}
|
|
|
|
func getFileLink(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
c.RequireFileId()
|
|
if c.Err != nil {
|
|
return
|
|
}
|
|
|
|
if !*c.App.Config().FileSettings.EnablePublicLink {
|
|
c.Err = model.NewAppError("getPublicLink", "api.file.get_public_link.disabled.app_error", nil, "", http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
auditRec := c.MakeAuditRecord(model.AuditEventGetFileLink, model.AuditStatusFail)
|
|
defer c.LogAuditRec(auditRec)
|
|
|
|
info, err := c.App.GetFileInfo(c.AppContext, c.Params.FileId)
|
|
if err != nil {
|
|
c.Err = err
|
|
setInaccessibleFileHeader(w, err)
|
|
return
|
|
}
|
|
model.AddEventParameterAuditableToAuditRec(auditRec, "file", info)
|
|
|
|
channel, err := c.App.GetChannel(c.AppContext, info.ChannelId)
|
|
if err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
perm := c.App.SessionHasPermissionToReadChannel(c.AppContext, *c.AppContext.Session(), channel)
|
|
if info.CreatorId == model.BookmarkFileOwner {
|
|
if !perm {
|
|
c.SetPermissionError(model.PermissionReadChannelContent)
|
|
return
|
|
}
|
|
} else if info.CreatorId != c.AppContext.Session().UserId && !perm {
|
|
c.SetPermissionError(model.PermissionReadChannelContent)
|
|
return
|
|
}
|
|
|
|
if info.PostId == "" && info.CreatorId != model.BookmarkFileOwner {
|
|
c.Err = model.NewAppError("getPublicLink", "api.file.get_public_link.no_post.app_error", nil, "file_id="+info.Id, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
resp := make(map[string]string)
|
|
link := c.App.GeneratePublicLink(c.GetSiteURLHeader(), info)
|
|
resp["link"] = link
|
|
|
|
auditRec.Success()
|
|
|
|
if _, err := w.Write([]byte(model.MapToJSON(resp))); err != nil {
|
|
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
|
}
|
|
}
|
|
|
|
func getFilePreview(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
c.RequireFileId()
|
|
if c.Err != nil {
|
|
return
|
|
}
|
|
|
|
forceDownload, _ := strconv.ParseBool(r.URL.Query().Get("download"))
|
|
info, err := c.App.GetFileInfo(c.AppContext, c.Params.FileId)
|
|
if err != nil {
|
|
c.Err = err
|
|
setInaccessibleFileHeader(w, err)
|
|
return
|
|
}
|
|
|
|
channel, err := c.App.GetChannel(c.AppContext, info.ChannelId)
|
|
if err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
perm := c.App.SessionHasPermissionToReadChannel(c.AppContext, *c.AppContext.Session(), channel)
|
|
if info.CreatorId == model.BookmarkFileOwner {
|
|
if !perm {
|
|
c.SetPermissionError(model.PermissionReadChannelContent)
|
|
return
|
|
}
|
|
} else if info.CreatorId != c.AppContext.Session().UserId && !perm {
|
|
c.SetPermissionError(model.PermissionReadChannelContent)
|
|
return
|
|
}
|
|
|
|
if info.PreviewPath == "" {
|
|
c.Err = model.NewAppError("getFilePreview", "api.file.get_file_preview.no_preview.app_error", nil, "file_id="+info.Id, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
fileReader, err := c.App.FileReader(info.PreviewPath)
|
|
if err != nil {
|
|
c.Err = err
|
|
c.Err.StatusCode = http.StatusNotFound
|
|
return
|
|
}
|
|
defer fileReader.Close()
|
|
|
|
web.WriteFileResponse(info.Name, PreviewImageType, 0, time.Unix(0, info.UpdateAt*int64(1000*1000)), *c.App.Config().ServiceSettings.WebserverMode, fileReader, forceDownload, w, r)
|
|
}
|
|
|
|
func getFileInfo(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
c.RequireFileId()
|
|
if c.Err != nil {
|
|
return
|
|
}
|
|
|
|
info, err := c.App.GetFileInfo(c.AppContext, c.Params.FileId)
|
|
if err != nil {
|
|
c.Err = err
|
|
setInaccessibleFileHeader(w, err)
|
|
return
|
|
}
|
|
|
|
channel, err := c.App.GetChannel(c.AppContext, info.ChannelId)
|
|
if err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
perm := c.App.SessionHasPermissionToReadChannel(c.AppContext, *c.AppContext.Session(), channel)
|
|
if info.CreatorId == model.BookmarkFileOwner {
|
|
if !perm {
|
|
c.SetPermissionError(model.PermissionReadChannelContent)
|
|
return
|
|
}
|
|
} else if info.CreatorId != c.AppContext.Session().UserId && !perm {
|
|
c.SetPermissionError(model.PermissionReadChannelContent)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Cache-Control", "max-age=2592000, private")
|
|
if err := json.NewEncoder(w).Encode(info); err != nil {
|
|
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
|
}
|
|
}
|
|
|
|
func getPublicFile(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
c.RequireFileId()
|
|
if c.Err != nil {
|
|
return
|
|
}
|
|
|
|
if !*c.App.Config().FileSettings.EnablePublicLink {
|
|
c.Err = model.NewAppError("getPublicFile", "api.file.get_public_link.disabled.app_error", nil, "", http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
info, err := c.App.GetFileInfo(c.AppContext, c.Params.FileId)
|
|
if err != nil {
|
|
c.Err = err
|
|
setInaccessibleFileHeader(w, err)
|
|
return
|
|
}
|
|
|
|
hash := r.URL.Query().Get("h")
|
|
|
|
if hash == "" {
|
|
c.Err = model.NewAppError("getPublicFile", "api.file.get_file.public_invalid.app_error", nil, "", http.StatusBadRequest)
|
|
utils.RenderWebAppError(c.App.Config(), w, r, c.Err, c.App.AsymmetricSigningKey())
|
|
return
|
|
}
|
|
|
|
if subtle.ConstantTimeCompare([]byte(hash), []byte(app.GeneratePublicLinkHash(info.Id, *c.App.Config().FileSettings.PublicLinkSalt))) != 1 {
|
|
c.Err = model.NewAppError("getPublicFile", "api.file.get_file.public_invalid.app_error", nil, "", http.StatusBadRequest)
|
|
utils.RenderWebAppError(c.App.Config(), w, r, c.Err, c.App.AsymmetricSigningKey())
|
|
return
|
|
}
|
|
|
|
fileReader, err := c.App.FileReader(info.Path)
|
|
if err != nil {
|
|
c.Err = err
|
|
c.Err.StatusCode = http.StatusNotFound
|
|
return
|
|
}
|
|
defer fileReader.Close()
|
|
|
|
web.WriteFileResponse(info.Name, info.MimeType, info.Size, time.Unix(0, info.UpdateAt*int64(1000*1000)), *c.App.Config().ServiceSettings.WebserverMode, fileReader, false, w, r)
|
|
}
|
|
|
|
func searchFilesInTeam(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
c.RequireTeamId()
|
|
if c.Err != nil {
|
|
return
|
|
}
|
|
|
|
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), c.Params.TeamId, model.PermissionViewTeam) {
|
|
c.SetPermissionError(model.PermissionViewTeam)
|
|
return
|
|
}
|
|
|
|
searchFiles(c, w, r, c.Params.TeamId)
|
|
}
|
|
|
|
func searchFilesInAllTeams(c *Context, w http.ResponseWriter, r *http.Request) {
|
|
searchFiles(c, w, r, "")
|
|
}
|
|
|
|
func searchFiles(c *Context, w http.ResponseWriter, r *http.Request, teamID string) {
|
|
var params model.SearchParameter
|
|
jsonErr := json.NewDecoder(r.Body).Decode(¶ms)
|
|
if jsonErr != nil {
|
|
c.Err = model.NewAppError("searchFiles", "api.post.search_files.invalid_body.app_error", nil, "", http.StatusBadRequest).Wrap(jsonErr)
|
|
return
|
|
}
|
|
|
|
if params.Terms == nil || *params.Terms == "" {
|
|
c.SetInvalidParam("terms")
|
|
return
|
|
}
|
|
terms := *params.Terms
|
|
|
|
timeZoneOffset := 0
|
|
if params.TimeZoneOffset != nil {
|
|
timeZoneOffset = *params.TimeZoneOffset
|
|
}
|
|
|
|
isOrSearch := false
|
|
if params.IsOrSearch != nil {
|
|
isOrSearch = *params.IsOrSearch
|
|
}
|
|
|
|
page := 0
|
|
if params.Page != nil {
|
|
page = *params.Page
|
|
}
|
|
|
|
perPage := 60
|
|
if params.PerPage != nil {
|
|
perPage = *params.PerPage
|
|
}
|
|
|
|
includeDeletedChannels := false
|
|
if params.IncludeDeletedChannels != nil {
|
|
includeDeletedChannels = *params.IncludeDeletedChannels
|
|
}
|
|
|
|
startTime := time.Now()
|
|
|
|
results, err := c.App.SearchFilesInTeamForUser(c.AppContext, terms, c.AppContext.Session().UserId, teamID, isOrSearch, includeDeletedChannels, timeZoneOffset, page, perPage)
|
|
|
|
elapsedTime := float64(time.Since(startTime)) / float64(time.Second)
|
|
metrics := c.App.Metrics()
|
|
if metrics != nil {
|
|
metrics.IncrementFilesSearchCounter()
|
|
metrics.ObserveFilesSearchDuration(elapsedTime)
|
|
}
|
|
|
|
if err != nil {
|
|
c.Err = err
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
|
if err := json.NewEncoder(w).Encode(results); err != nil {
|
|
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
|
}
|
|
}
|
|
|
|
func setInaccessibleFileHeader(w http.ResponseWriter, appErr *model.AppError) {
|
|
// File is inaccessible due to cloud plan's limit.
|
|
if appErr.Id == "app.file.cloud.get.app_error" {
|
|
w.Header().Set(model.HeaderFirstInaccessibleFileTime, "1")
|
|
}
|
|
}
|