summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--cmd/web.go8
-rw-r--r--models/issue.go18
-rw-r--r--modules/middleware/context.go10
-rw-r--r--modules/setting/setting.go4
-rwxr-xr-xpublic/css/gogs.css17
-rw-r--r--public/js/app.js27
-rw-r--r--routers/repo/issue.go321
-rw-r--r--templates/repo/issue/create.tmpl15
-rw-r--r--templates/repo/issue/view.tmpl15
9 files changed, 132 insertions, 303 deletions
diff --git a/cmd/web.go b/cmd/web.go
index aea0cd86aa..bb020fab90 100644
--- a/cmd/web.go
+++ b/cmd/web.go
@@ -238,6 +238,7 @@ func runWeb(*cli.Context) {
r.Post("/:index/label", repo.UpdateIssueLabel)
r.Post("/:index/milestone", repo.UpdateIssueMilestone)
r.Post("/:index/assignee", repo.UpdateAssignee)
+ r.Get("/:index/attachment/:id", repo.IssueGetAttachment)
r.Post("/labels/new", bindIgnErr(auth.CreateLabelForm{}), repo.NewLabel)
r.Post("/labels/edit", bindIgnErr(auth.CreateLabelForm{}), repo.UpdateLabel)
r.Post("/labels/delete", repo.DeleteLabel)
@@ -254,13 +255,6 @@ func runWeb(*cli.Context) {
r.Get("/releases/edit/:tagname", repo.EditRelease)
}, reqSignIn, middleware.RepoAssignment(true))
- m.Group("/:username/:reponame/issues/:index/attachment", func(r martini.Router) {
- r.Get("/:id", repo.IssueGetAttachment)
- r.Post("/", repo.IssuePostAttachment)
- r.Post("/:comment", repo.IssuePostAttachment)
- r.Delete("/:comment/:id", repo.IssueDeleteAttachment)
- }, reqSignIn, middleware.RepoAssignment(true), middleware.Toggle(&middleware.ToggleOptions{DisableCsrf: true}))
-
m.Group("/:username/:reponame", func(r martini.Router) {
r.Post("/releases/new", bindIgnErr(auth.NewReleaseForm{}), repo.NewReleasePost)
r.Post("/releases/edit/:tagname", bindIgnErr(auth.EditReleaseForm{}), repo.EditReleasePost)
diff --git a/models/issue.go b/models/issue.go
index f16c384b6f..9422a7380d 100644
--- a/models/issue.go
+++ b/models/issue.go
@@ -1085,21 +1085,3 @@ func DeleteAttachmentsByComment(commentId int64, remove bool) (int, error) {
return DeleteAttachments(attachments, remove)
}
-
-// AssignAttachment assigns the given attachment to the specified comment
-func AssignAttachment(issueId, commentId, attachmentId int64) error {
- a, err := GetAttachmentById(attachmentId)
-
- if err != nil {
- return err
- }
-
- if a.IssueId != issueId {
- return ErrAttachmentNotLinked
- }
-
- a.CommentId = commentId
-
- _, err = x.Id(a.Id).Update(a)
- return err
-}
diff --git a/modules/middleware/context.go b/modules/middleware/context.go
index c641449a87..6b47e94fb0 100644
--- a/modules/middleware/context.go
+++ b/modules/middleware/context.go
@@ -323,7 +323,6 @@ func (f *Flash) Success(msg string) {
// InitContext initializes a classic context for a request.
func InitContext() martini.Handler {
return func(res http.ResponseWriter, r *http.Request, c martini.Context, rd *Render) {
-
ctx := &Context{
c: c,
// p: p,
@@ -332,7 +331,6 @@ func InitContext() martini.Handler {
Cache: setting.Cache,
Render: rd,
}
-
ctx.Data["PageStartTime"] = time.Now()
// start session
@@ -374,6 +372,14 @@ func InitContext() martini.Handler {
ctx.Data["IsAdmin"] = ctx.User.IsAdmin
}
+ // If request sends files, parse them here otherwise the Query() can't be parsed and the CsrfToken will be invalid.
+ if strings.Contains(r.Header.Get("Content-Type"), "multipart/form-data") {
+ if err = ctx.Req.ParseMultipartForm(setting.AttachmentMaxSize << 20); err != nil { // 32MB max size
+ ctx.Handle(500, "issue.Comment(ctx.Req.ParseMultipartForm)", err)
+ return
+ }
+ }
+
// get or create csrf token
ctx.Data["CsrfToken"] = ctx.CsrfToken()
ctx.Data["CsrfTokenHtml"] = template.HTML(`<input type="hidden" name="_csrf" value="` + ctx.csrfToken + `">`)
diff --git a/modules/setting/setting.go b/modules/setting/setting.go
index ba9e86dc8f..349ef11595 100644
--- a/modules/setting/setting.go
+++ b/modules/setting/setting.go
@@ -74,6 +74,8 @@ var (
// Attachment settings.
AttachmentPath string
AttachmentAllowedTypes string
+ AttachmentMaxSize int64
+ AttachmentMaxFiles int
// Cache settings.
Cache cache.Cache
@@ -172,6 +174,8 @@ func NewConfigContext() {
AttachmentPath = Cfg.MustValue("attachment", "PATH", "files/attachments")
AttachmentAllowedTypes = Cfg.MustValue("attachment", "ALLOWED_TYPES", "*/*")
+ AttachmentMaxSize = Cfg.MustInt64("attachment", "MAX_SIZE", 32)
+ AttachmentMaxFiles = Cfg.MustInt("attachment", "MAX_FILES", 10)
if err = os.MkdirAll(AttachmentPath, os.ModePerm); err != nil {
log.Fatal("Could not create directory %s: %s", AttachmentPath, err)
diff --git a/public/css/gogs.css b/public/css/gogs.css
index e78d7f940c..cc48f211f4 100755
--- a/public/css/gogs.css
+++ b/public/css/gogs.css
@@ -1819,4 +1819,21 @@ body {
.attachment-preview-img {
border: 1px solid #d8d8d8;
+}
+
+#attachments-button {
+ float: left;
+}
+
+#attached {
+ height: 18px;
+ margin: 10px 10px 15px 10px;
+}
+
+#attached-list .label {
+ margin-right: 10px;
+}
+
+#issue-create-form #attached {
+ margin-bottom: 0;
} \ No newline at end of file
diff --git a/public/js/app.js b/public/js/app.js
index 3d4ed62362..7ffcbd4a3e 100644
--- a/public/js/app.js
+++ b/public/js/app.js
@@ -536,7 +536,7 @@ function initIssue() {
var over = function() {
var $this = $(this);
- if ($this.text().match(/\.(png|jpg|jpeg|gif)$/) == false) {
+ if ($this.text().match(/\.(png|jpg|jpeg|gif)$/i) == false) {
return;
}
@@ -576,15 +576,30 @@ function initIssue() {
// Upload.
(function() {
- var $attached = $("#attached");
- var $attachments = $("input[name=attachments]");
+ var $attachedList = $("#attached-list");
var $addButton = $("#attachments-button");
- var commentId = $addButton.attr("data-comment-id"); // "0" == for issue, "" == for comment
- var accepted = $addButton.attr("data-accept");
+ var fileInput = $("#attachments-input")[0];
+
+ fileInput.addEventListener("change", function(event) {
+ $attachedList.empty();
+ $attachedList.append("<b>Attachments:</b> ");
+
+ for (var index = 0; index < fileInput.files.length; index++) {
+ var file = fileInput.files[index];
+
+ var $span = $("<span></span>");
+
+ $span.addClass("label");
+ $span.addClass("label-default");
+
+ $span.append(file.name.toLowerCase());
+ $attachedList.append($span);
+ }
+ });
$addButton.on("click", function() {
- // TODO: (nuss-justin): open dialog, upload file, add id to list, add file to $attached list
+ fileInput.click();
return false;
});
}());
diff --git a/routers/repo/issue.go b/routers/repo/issue.go
index 903a32d968..81261e6cd5 100644
--- a/routers/repo/issue.go
+++ b/routers/repo/issue.go
@@ -5,6 +5,7 @@
package repo
import (
+ "errors"
"fmt"
"io"
"io/ioutil"
@@ -35,6 +36,11 @@ const (
MILESTONE_EDIT base.TplName = "repo/issue/milestone_edit"
)
+var (
+ ErrFileTypeForbidden = errors.New("File type is not allowed")
+ ErrTooManyFiles = errors.New("Maximum number of files to upload exceeded")
+)
+
func Issues(ctx *middleware.Context) {
ctx.Data["Title"] = "Issues"
ctx.Data["IsRepoToolbarIssues"] = true
@@ -233,6 +239,8 @@ func CreateIssuePost(ctx *middleware.Context, params martini.Params, form auth.C
return
}
+ uploadFiles(ctx, issue.Id, 0)
+
// Update mentions.
ms := base.MentionPattern.FindAllString(issue.Content, -1)
if len(ms) > 0 {
@@ -619,6 +627,67 @@ func UpdateAssignee(ctx *middleware.Context) {
})
}
+func uploadFiles(ctx *middleware.Context, issueId, commentId int64) {
+ allowedTypes := strings.Split(setting.AttachmentAllowedTypes, "|")
+ attachments := ctx.Req.MultipartForm.File["attachments"]
+
+ if len(attachments) > setting.AttachmentMaxFiles {
+ ctx.Handle(400, "issue.Comment", ErrTooManyFiles)
+ return
+ }
+
+ for _, header := range attachments {
+ file, err := header.Open()
+
+ if err != nil {
+ ctx.Handle(500, "issue.Comment(header.Open)", err)
+ return
+ }
+
+ defer file.Close()
+
+ allowed := false
+ fileType := mime.TypeByExtension(header.Filename)
+
+ for _, t := range allowedTypes {
+ t := strings.Trim(t, " ")
+
+ if t == "*/*" || t == fileType {
+ allowed = true
+ break
+ }
+ }
+
+ if !allowed {
+ ctx.Handle(400, "issue.Comment", ErrFileTypeForbidden)
+ return
+ }
+
+ out, err := ioutil.TempFile(setting.AttachmentPath, "attachment_")
+
+ if err != nil {
+ ctx.Handle(500, "issue.Comment(ioutil.TempFile)", err)
+ return
+ }
+
+ defer out.Close()
+
+ _, err = io.Copy(out, file)
+
+ if err != nil {
+ ctx.Handle(500, "issue.Comment(io.Copy)", err)
+ return
+ }
+
+ _, err = models.CreateAttachment(issueId, commentId, header.Filename, out.Name())
+
+ if err != nil {
+ ctx.Handle(500, "issue.Comment(io.Copy)", err)
+ return
+ }
+ }
+}
+
func Comment(ctx *middleware.Context, params martini.Params) {
index, err := base.StrTo(ctx.Query("issueIndex")).Int64()
if err != nil {
@@ -706,28 +775,8 @@ func Comment(ctx *middleware.Context, params martini.Params) {
}
}
- attachments := strings.Split(params["attachments"], ",")
-
- for _, a := range attachments {
- a = strings.Trim(a, " ")
-
- if len(a) == 0 {
- continue
- }
-
- aId, err := base.StrTo(a).Int64()
-
- if err != nil {
- ctx.Handle(400, "issue.Comment(base.StrTo.Int64)", err)
- return
- }
-
- err = models.AssignAttachment(issue.Id, comment.Id, aId)
-
- if err != nil {
- ctx.Handle(400, "issue.Comment(models.AssignAttachment)", err)
- return
- }
+ if comment != nil {
+ uploadFiles(ctx, issue.Id, comment.Id)
}
// Notify watchers.
@@ -1007,122 +1056,6 @@ func UpdateMilestonePost(ctx *middleware.Context, params martini.Params, form au
ctx.Redirect(ctx.Repo.RepoLink + "/issues/milestones")
}
-func IssuePostAttachment(ctx *middleware.Context, params martini.Params) {
- index, _ := base.StrTo(params["index"]).Int64()
-
- if index == 0 {
- ctx.JSON(400, map[string]interface{}{
- "ok": false,
- "error": "invalid issue index",
- })
-
- return
- }
-
- issue, err := models.GetIssueByIndex(ctx.Repo.Repository.Id, index)
-
- if err != nil {
- ctx.JSON(400, map[string]interface{}{
- "ok": false,
- "error": "invalid comment id",
- })
-
- return
- }
-
- commentId, err := base.StrTo(params["comment"]).Int64()
-
- if err != nil && len(params["comment"]) > 0 {
- ctx.JSON(400, map[string]interface{}{
- "ok": false,
- "error": "invalid comment id",
- })
-
- return
- }
-
- if commentId == 0 {
- commentId = -1
- }
-
- file, header, err := ctx.Req.FormFile("attachment")
-
- if err != nil {
- ctx.JSON(400, map[string]interface{}{
- "ok": false,
- "error": "upload error",
- })
-
- return
- }
-
- defer file.Close()
-
- // check mime type, write to file, insert attachment to db
- allowedTypes := strings.Split(setting.AttachmentAllowedTypes, "|")
- allowed := false
-
- fileType := mime.TypeByExtension(header.Filename)
-
- for _, t := range allowedTypes {
- t := strings.Trim(t, " ")
-
- if t == "*/*" || t == fileType {
- allowed = true
- break
- }
- }
-
- if !allowed {
- ctx.JSON(400, map[string]interface{}{
- "ok": false,
- "error": "mime type not allowed",
- })
-
- return
- }
-
- out, err := ioutil.TempFile(setting.AttachmentPath, "attachment_")
-
- if err != nil {
- ctx.JSON(500, map[string]interface{}{
- "ok": false,
- "error": "internal server error",
- })
-
- return
- }
-
- defer out.Close()
-
- _, err = io.Copy(out, file)
-
- if err != nil {
- ctx.JSON(500, map[string]interface{}{
- "ok": false,
- "error": "internal server error",
- })
-
- return
- }
-
- a, err := models.CreateAttachment(issue.Id, commentId, header.Filename, out.Name())
-
- if err != nil {
- ctx.JSON(500, map[string]interface{}{
- "ok": false,
- "error": "internal server error",
- })
-
- return
- }
-
- ctx.JSON(500, map[string]interface{}{
- "ok": true,
- "id": a.Id,
- })
-}
-
func IssueGetAttachment(ctx *middleware.Context, params martini.Params) {
id, err := base.StrTo(params["id"]).Int64()
@@ -1138,117 +1071,5 @@ func IssueGetAttachment(ctx *middleware.Context, params martini.Params) {
return
}
- log.Error("path=%s name=%s", attachment.Path, attachment.Name)
-
ctx.ServeFile(attachment.Path, attachment.Name)
}
-
-func IssueDeleteAttachment(ctx *middleware.Context, params martini.Params) {
- index, _ := base.StrTo(params["index"]).Int64()
-
- if index == 0 {
- ctx.JSON(400, map[string]interface{}{
- "ok": false,
- "error": "invalid issue index",
- })
-
- return
- }
-
- issue, err := models.GetIssueByIndex(ctx.Repo.Repository.Id, index)
-
- if err != nil {
- ctx.JSON(400, map[string]interface{}{
- "ok": false,
- "error": "invalid comment id",
- })
-
- return
- }
-
- commentId, err := base.StrTo(params["comment"]).Int64()
-
- if err != nil || commentId < 0 {
- ctx.JSON(400, map[string]interface{}{
- "ok": false,
- "error": "invalid comment id",
- })
-
- return
- }
-
- comment, err := models.GetCommentById(commentId)
-
- if err != nil {
- ctx.JSON(400, map[string]interface{}{
- "ok": false,
- "error": "invalid issue id",
- })
-
- return
- }
-
- if comment.PosterId != ctx.User.Id && !ctx.User.IsAdmin {
- ctx.JSON(400, map[string]interface{}{
- "ok": false,
- "error": "no permissions",
- })
-
- return
- }
-
- attachmentId, err := base.StrTo(params["id"]).Int64()
-
- if err != nil {
- ctx.JSON(400, map[string]interface{}{
- "ok": false,
- "error": "invalid attachment id",
- })
-
- return
- }
-
- attachment, err := models.GetAttachmentById(attachmentId)
-
- if err != nil {
- ctx.JSON(400, map[string]interface{}{
- "ok": false,
- "error": "wrong attachment id",
- })
-
- return
- }
-
- if attachment.IssueId != issue.Id {
- ctx.JSON(400, map[string]interface{}{
- "ok": false,
- "error": "attachment not associated with the given issue",
- })
-
- return
- }
-
- if attachment.CommentId != commentId {
- ctx.JSON(400, map[string]interface{}{
- "ok": false,
- "error": "attachment not associated with the given comment",
- })
-
- return
- }
-
- err = models.DeleteAttachment(attachment, true)
-
- if err != nil {
- ctx.JSON(500, map[string]interface{}{
- "ok": false,
- "error": "could not delete attachment",
- })
-
- return
- }
-
- ctx.JSON(200, map[string]interface{}{
- "ok": true,
- })
-}
diff --git a/templates/repo/issue/create.tmpl b/templates/repo/issue/create.tmpl
index 1146a4664f..0d72123b81 100644
--- a/templates/repo/issue/create.tmpl
+++ b/templates/repo/issue/create.tmpl
@@ -4,7 +4,7 @@
{{template "repo/toolbar" .}}
<div id="body" class="container">
<div id="issue">
- <form class="form" action="{{.RepoLink}}/issues/new" method="post" id="issue-create-form">
+ <form class="form" action="{{.RepoLink}}/issues/new" method="post" id="issue-create-form" enctype="multipart/form-data">
{{.CsrfTokenHtml}}
{{template "base/alert" .}}
<div class="col-md-1">
@@ -101,18 +101,13 @@
<div class="tab-pane issue-preview-content" id="issue-preview">loading...</div>
</div>
</div>
- <!--
- <div>
- <div id="attached"></div>
+ <div id="attached">
+ <div id="attached-list"></div>
</div>
- -->
<div class="text-right panel-body">
<div class="form-group">
- <!--
- <input type="hidden" name="attachments" value="" />
- <button data-accept="{{.AllowedTypes}}" data-comment-id="0" class="btn-default btn attachment-add" id="attachments-button">Add Attachments...</button>
- -->
-
+ <input type="file" accept="{{.AllowedTypes}}" style="display: none;" id="attachments-input" name="attachments" multiple />
+ <button class="btn-default btn attachment-add" id="attachments-button">Select Attachments...</button>
<input type="hidden" value="id" name="repo-id"/>
<button class="btn-success btn">Create new issue</button>
</div>
diff --git a/templates/repo/issue/view.tmpl b/templates/repo/issue/view.tmpl
index 8c90f312c3..dd200e8016 100644
--- a/templates/repo/issue/view.tmpl
+++ b/templates/repo/issue/view.tmpl
@@ -117,7 +117,7 @@
<hr class="issue-line"/>
{{if .SignedUser}}<div class="issue-child issue-reply">
<a class="user pull-left" href="/user/{{.SignedUser.Name}}"><img class="avatar" src="{{.SignedUser.AvatarLink}}" alt=""/></a>
- <form class="panel panel-default issue-content" action="{{.RepoLink}}/comment/new" method="post">
+ <form class="panel panel-default issue-content" action="{{.RepoLink}}/comment/new" method="post" enctype="multipart/form-data">
{{.CsrfTokenHtml}}
<div class="panel-body">
<div class="form-group">
@@ -137,18 +137,13 @@
<div class="tab-pane issue-preview-content" id="issue-preview">Loading...</div>
</div>
</div>
- <!--
- <div>
- <div id="attached"></div>
+ <div id="attached">
+ <div id="attached-list"></div>
</div>
- -->
<div class="text-right">
<div class="form-group">
- <!--
- <input type="hidden" name="attachments" value="" />
- <button data-accept="{{.AllowedTypes}}" class="btn-default btn attachment-add" id="attachments-button">Add Attachments...</button>
- -->
-
+ <input type="file" accept="{{.AllowedTypes}}" style="display: none;" id="attachments-input" name="attachments" multiple />
+ <button class="btn-default btn attachment-add" id="attachments-button">Select Attachments...</button>
{{if .IsIssueOwner}}{{if .Issue.IsClosed}}
<input type="submit" class="btn-default btn issue-open" id="issue-open-btn" data-origin="Reopen" data-text="Reopen & Comment" name="change_status" value="Reopen"/>{{else}}
<input type="submit" class="btn-default btn issue-close" id="issue-close-btn" data-origin="Close" data-text="Close & Comment" name="change_status" value="Close"/>{{end}}{{end}}&nbsp;&nbsp;