mirror of
https://github.com/go-vikunja/vikunja.git
synced 2026-05-08 21:08:04 -05:00
Task/project duplication and the Todoist migration were passing stored or API-reported sizes into NewAttachment. Derive the size from the actual buffered content so every caller matches the hardened boundary behaviour (GHSA-qh78-rvg3-cv54 defence-in-depth).
174 lines
5.1 KiB
Go
174 lines
5.1 KiB
Go
// Vikunja is a to-do list application to facilitate your life.
|
|
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
package models
|
|
|
|
import (
|
|
"bytes"
|
|
"io"
|
|
|
|
"code.vikunja.io/api/pkg/files"
|
|
"code.vikunja.io/api/pkg/web"
|
|
|
|
"xorm.io/xorm"
|
|
)
|
|
|
|
// TaskDuplicate holds everything needed to duplicate a task
|
|
type TaskDuplicate struct {
|
|
// The task id of the task to duplicate
|
|
TaskID int64 `json:"-" param:"projecttask"`
|
|
|
|
// The duplicated task
|
|
Task *Task `json:"duplicated_task,omitempty"`
|
|
|
|
web.Permissions `json:"-"`
|
|
web.CRUDable `json:"-"`
|
|
}
|
|
|
|
// CanCreate checks if a user has the permission to duplicate a task
|
|
func (td *TaskDuplicate) CanCreate(s *xorm.Session, a web.Auth) (canCreate bool, err error) {
|
|
// Need read access on the original task
|
|
originalTask := &Task{ID: td.TaskID}
|
|
canRead, _, err := originalTask.CanRead(s, a)
|
|
if err != nil || !canRead {
|
|
return canRead, err
|
|
}
|
|
|
|
// Need write access on the project to create tasks in it
|
|
p := &Project{ID: originalTask.ProjectID}
|
|
return p.CanUpdate(s, a)
|
|
}
|
|
|
|
// Create duplicates a task
|
|
// @Summary Duplicate a task
|
|
// @Description Copies a task with all its properties (labels, assignees, attachments, reminders) into the same project. Creates a "copied from" relation between the new and original task.
|
|
// @tags task
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Security JWTKeyAuth
|
|
// @Param taskID path int true "The task ID to duplicate"
|
|
// @Success 201 {object} models.TaskDuplicate "The duplicated task."
|
|
// @Failure 403 {object} web.HTTPError "The user does not have access to the task."
|
|
// @Failure 500 {object} models.Message "Internal error"
|
|
// @Router /tasks/{taskID}/duplicate [put]
|
|
func (td *TaskDuplicate) Create(s *xorm.Session, doer web.Auth) (err error) {
|
|
// Get the original task with all details
|
|
originalTask := &Task{ID: td.TaskID}
|
|
err = originalTask.ReadOne(s, doer)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create the new task
|
|
newTask := &Task{
|
|
Title: originalTask.Title,
|
|
Description: originalTask.Description,
|
|
Done: false,
|
|
DueDate: originalTask.DueDate,
|
|
ProjectID: originalTask.ProjectID,
|
|
RepeatAfter: originalTask.RepeatAfter,
|
|
RepeatMode: originalTask.RepeatMode,
|
|
Priority: originalTask.Priority,
|
|
StartDate: originalTask.StartDate,
|
|
EndDate: originalTask.EndDate,
|
|
HexColor: originalTask.HexColor,
|
|
PercentDone: originalTask.PercentDone,
|
|
Assignees: originalTask.Assignees,
|
|
Reminders: originalTask.Reminders,
|
|
}
|
|
|
|
err = createTask(s, newTask, doer, true, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Duplicate labels
|
|
labelTasks := []*LabelTask{}
|
|
err = s.Where("task_id = ?", td.TaskID).Find(&labelTasks)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, lt := range labelTasks {
|
|
lt.ID = 0
|
|
lt.TaskID = newTask.ID
|
|
}
|
|
if len(labelTasks) > 0 {
|
|
if _, err := s.Insert(&labelTasks); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Duplicate attachments (copy underlying files)
|
|
attachments, err := getTaskAttachmentsByTaskIDs(s, []int64{td.TaskID})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
oldToNewAttachmentIDs := make(map[int64]int64)
|
|
for _, attachment := range attachments {
|
|
oldAttachmentID := attachment.ID
|
|
attachment.ID = 0
|
|
attachment.TaskID = newTask.ID
|
|
attachment.File = &files.File{ID: attachment.FileID}
|
|
if err := attachment.File.LoadFileMetaByID(); err != nil {
|
|
if files.IsErrFileDoesNotExist(err) {
|
|
continue
|
|
}
|
|
return err
|
|
}
|
|
if err := attachment.File.LoadFileByID(); err != nil {
|
|
return err
|
|
}
|
|
|
|
sourceFile := attachment.File.File
|
|
defer sourceFile.Close()
|
|
buf, err := io.ReadAll(sourceFile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = attachment.NewAttachment(s, bytes.NewReader(buf), attachment.File.Name, uint64(len(buf)), doer)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
oldToNewAttachmentIDs[oldAttachmentID] = attachment.ID
|
|
}
|
|
|
|
// Re-set the cover image if the original task had one
|
|
if originalTask.CoverImageAttachmentID != 0 {
|
|
if newAttachmentID, ok := oldToNewAttachmentIDs[originalTask.CoverImageAttachmentID]; ok {
|
|
newTask.CoverImageAttachmentID = newAttachmentID
|
|
if _, err := s.Where("id = ?", newTask.ID).
|
|
Cols("cover_image_attachment_id").
|
|
Update(newTask); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
// Create "copied from/to" relation
|
|
rel := &TaskRelation{
|
|
TaskID: newTask.ID,
|
|
OtherTaskID: td.TaskID,
|
|
RelationKind: RelationKindCopiedFrom,
|
|
}
|
|
if err := rel.Create(s, doer); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Re-read the task to populate all fields for the response
|
|
td.Task = newTask
|
|
return td.Task.ReadOne(s, doer)
|
|
}
|