All Downloads are FREE. Search and download functionalities are using the official Maven repository.

gitbucket.core.controller.IssuesController.scala Maven / Gradle / Ivy

The newest version!
package gitbucket.core.controller

import gitbucket.core.issues.html
import gitbucket.core.model.{Account, CustomFieldBehavior}
import gitbucket.core.service.IssuesService._
import gitbucket.core.service._
import gitbucket.core.util.Implicits._
import gitbucket.core.util._
import gitbucket.core.view
import gitbucket.core.view.Markdown
import org.scalatra.forms._
import org.scalatra.{BadRequest, Ok}

class IssuesController
    extends IssuesControllerBase
    with IssuesService
    with RepositoryService
    with AccountService
    with LabelsService
    with MilestonesService
    with ActivityService
    with HandleCommentService
    with IssueCreationService
    with CustomFieldsService
    with ReadableUsersAuthenticator
    with ReferrerAuthenticator
    with WritableUsersAuthenticator
    with MergeService
    with PullRequestService
    with WebHookIssueCommentService
    with WebHookPullRequestReviewCommentService
    with CommitsService
    with PrioritiesService
    with RequestCache

trait IssuesControllerBase extends ControllerBase {
  self: IssuesService
    with RepositoryService
    with AccountService
    with LabelsService
    with MilestonesService
    with ActivityService
    with HandleCommentService
    with IssueCreationService
    with CustomFieldsService
    with ReadableUsersAuthenticator
    with ReferrerAuthenticator
    with WritableUsersAuthenticator
    with PullRequestService
    with WebHookIssueCommentService
    with PrioritiesService =>

  case class IssueCreateForm(
    title: String,
    content: Option[String],
    assigneeUserNames: Option[String],
    milestoneId: Option[Int],
    priorityId: Option[Int],
    labelNames: Option[String]
  )
  case class CommentForm(issueId: Int, content: String)
  case class IssueStateForm(issueId: Int, content: Option[String])

  val issueCreateForm = mapping(
    "title" -> trim(label("Title", text(required))),
    "content" -> trim(optional(text())),
    "assigneeUserNames" -> trim(optional(text())),
    "milestoneId" -> trim(optional(number())),
    "priorityId" -> trim(optional(number())),
    "labelNames" -> trim(optional(text()))
  )(IssueCreateForm.apply)

  val issueTitleEditForm = mapping(
    "title" -> trim(label("Title", text(required)))
  )(x => x)
  val issueEditForm = mapping(
    "content" -> trim(optional(text()))
  )(x => x)

  val commentForm = mapping(
    "issueId" -> label("Issue Id", number()),
    "content" -> trim(label("Comment", text(required)))
  )(CommentForm.apply)

  val issueStateForm = mapping(
    "issueId" -> label("Issue Id", number()),
    "content" -> trim(optional(text()))
  )(IssueStateForm.apply)

  get("/:owner/:repository/issues")(referrersOnly { repository =>
    val q = request.getParameter("q")
    Option(q) match {
      case Some(filter) if filter.contains("is:pr") =>
        redirect(s"/${repository.owner}/${repository.name}/pulls?q=${StringUtil.urlEncode(q)}")
      case Some(filter) =>
        val condition = IssueSearchCondition(filter)
        if (condition.isEmpty) {
          // Redirect to keyword search
          redirect(s"/${repository.owner}/${repository.name}/search?q=${StringUtil.urlEncode(q)}&type=issues")
        } else {
          searchIssues(repository, condition, IssueSearchCondition.page(request))
        }
      case None =>
        searchIssues(repository, IssueSearchCondition(request), IssueSearchCondition.page(request))
    }
  })

  get("/:owner/:repository/issues/:id")(referrersOnly { repository =>
    val issueId = params("id")
    getIssue(repository.owner, repository.name, issueId) map { issue =>
      if (issue.isPullRequest) {
        redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
      } else {
        html.issue(
          issue,
          getComments(repository.owner, repository.name, issueId.toInt),
          getIssueLabels(repository.owner, repository.name, issueId.toInt),
          getIssueAssignees(repository.owner, repository.name, issueId.toInt),
          getAssignableUserNames(repository.owner, repository.name),
          getMilestonesWithIssueCount(repository.owner, repository.name),
          getPriorities(repository.owner, repository.name),
          getLabels(repository.owner, repository.name),
          getCustomFieldsWithValue(repository.owner, repository.name, issueId.toInt).filter(_._1.enableForIssues),
          isIssueEditable(repository),
          isIssueManageable(repository),
          isIssueCommentManageable(repository),
          repository
        )
      }
    } getOrElse NotFound()
  })

  get("/:owner/:repository/issues/new")(readableUsersOnly { repository =>
    if (isIssueEditable(repository)) { // TODO Should this check is provided by authenticator?
      html.create(
        getAssignableUserNames(repository.owner, repository.name),
        getMilestones(repository.owner, repository.name),
        getPriorities(repository.owner, repository.name),
        getDefaultPriority(repository.owner, repository.name),
        getLabels(repository.owner, repository.name),
        getCustomFields(repository.owner, repository.name).filter(_.enableForIssues),
        isIssueManageable(repository),
        getContentTemplate(repository, "ISSUE_TEMPLATE"),
        repository
      )
    } else Unauthorized()
  })

  post("/:owner/:repository/issues/new", issueCreateForm)(readableUsersOnly { (form, repository) =>
    context.withLoginAccount { loginAccount =>
      if (isIssueEditable(repository)) { // TODO Should this check is provided by authenticator?
        val issue = createIssue(
          repository,
          form.title,
          form.content,
          form.assigneeUserNames.toSeq.flatMap(_.split(",")),
          form.milestoneId,
          form.priorityId,
          form.labelNames.toSeq.flatMap(_.split(",")),
          loginAccount
        )

        // Insert custom field values
        params.toMap.foreach { case (key, value) =>
          if (key.startsWith("custom-field-")) {
            getCustomField(
              repository.owner,
              repository.name,
              key.replaceFirst("^custom-field-", "").toInt
            ).foreach { field =>
              CustomFieldBehavior.validate(field, value, messages) match {
                case None =>
                  insertOrUpdateCustomFieldValue(field, repository.owner, repository.name, issue.issueId, value)
                case Some(_) => halt(400)
              }
            }
          }
        }

        redirect(s"/${issue.userName}/${issue.repositoryName}/issues/${issue.issueId}")
      } else Unauthorized()
    }
  })

  ajaxPost("/:owner/:repository/issues/edit_title/:id", issueTitleEditForm)(readableUsersOnly { (title, repository) =>
    context.withLoginAccount { loginAccount =>
      getIssue(repository.owner, repository.name, params("id")).map { issue =>
        if (isEditableContent(repository.owner, repository.name, issue.openedUserName, loginAccount)) {
          if (issue.title != title) {
            // update issue
            updateIssue(repository.owner, repository.name, issue.issueId, title, issue.content)
            // extract references and create refer comment
            createReferComment(repository.owner, repository.name, issue.copy(title = title), title, loginAccount)
            createComment(
              repository.owner,
              repository.name,
              loginAccount.userName,
              issue.issueId,
              issue.title + "\r\n" + title,
              "change_title"
            )
          }
          redirect(s"/${repository.owner}/${repository.name}/issues/_data/${issue.issueId}")
        } else Unauthorized()
      } getOrElse NotFound()
    }
  })

  ajaxPost("/:owner/:repository/issues/edit/:id", issueEditForm)(readableUsersOnly { (content, repository) =>
    context.withLoginAccount { loginAccount =>
      getIssue(repository.owner, repository.name, params("id")).map { issue =>
        if (isEditableContent(repository.owner, repository.name, issue.openedUserName, loginAccount)) {
          // update issue
          updateIssue(repository.owner, repository.name, issue.issueId, issue.title, content)
          // extract references and create refer comment
          createReferComment(repository.owner, repository.name, issue, content.getOrElse(""), loginAccount)

          redirect(s"/${repository.owner}/${repository.name}/issues/_data/${issue.issueId}")
        } else Unauthorized()
      } getOrElse NotFound()
    }
  })

  post("/:owner/:repository/issue_comments/new", commentForm)(readableUsersOnly { (form, repository) =>
    context.withLoginAccount { loginAccount =>
      getIssue(repository.owner, repository.name, form.issueId.toString).flatMap { issue =>
        val actionOpt =
          params
            .get("action")
            .filter(_ => isEditableContent(issue.userName, issue.repositoryName, issue.openedUserName, loginAccount))
        handleComment(issue, Some(form.content), repository, actionOpt) map { case (issue, id) =>
          redirect(
            s"/${repository.owner}/${repository.name}/${if (issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}"
          )
        }
      } getOrElse NotFound()
    }
  })

  post("/:owner/:repository/issue_comments/state", issueStateForm)(readableUsersOnly { (form, repository) =>
    context.withLoginAccount { loginAccount =>
      getIssue(repository.owner, repository.name, form.issueId.toString).flatMap { issue =>
        val actionOpt =
          params
            .get("action")
            .filter(_ => isEditableContent(issue.userName, issue.repositoryName, issue.openedUserName, loginAccount))
        handleComment(issue, form.content, repository, actionOpt) map { case (issue, id) =>
          redirect(
            s"/${repository.owner}/${repository.name}/${if (issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}"
          )
        }
      } getOrElse NotFound()
    }
  })

  ajaxPost("/:owner/:repository/issue_comments/edit/:id", commentForm)(readableUsersOnly { (form, repository) =>
    context.withLoginAccount { loginAccount =>
      getComment(repository.owner, repository.name, params("id")).map { comment =>
        if (isEditableContent(repository.owner, repository.name, comment.commentedUserName, loginAccount)) {
          updateComment(repository.owner, repository.name, comment.issueId, comment.commentId, form.content)
          redirect(s"/${repository.owner}/${repository.name}/issue_comments/_data/${comment.commentId}")
        } else Unauthorized()
      } getOrElse NotFound()
    }
  })

  ajaxPost("/:owner/:repository/issue_comments/delete/:id")(readableUsersOnly { repository =>
    context.withLoginAccount { loginAccount =>
      getComment(repository.owner, repository.name, params("id")).map { comment =>
        if (isDeletableComment(repository.owner, repository.name, comment.commentedUserName, loginAccount)) {
          Ok(deleteComment(repository.owner, repository.name, comment.issueId, comment.commentId))
        } else Unauthorized()
      } getOrElse NotFound()
    }
  })

  ajaxGet("/:owner/:repository/issues/_data/:id")(readableUsersOnly { repository =>
    context.withLoginAccount { loginAccount =>
      getIssue(repository.owner, repository.name, params("id")) map { x =>
        if (isEditableContent(x.userName, x.repositoryName, x.openedUserName, loginAccount)) {
          params.get("dataType") collect {
            case t if t == "html" => html.editissue(x.content, x.issueId, repository)
          } getOrElse {
            contentType = formats("json")
            org.json4s.jackson.Serialization.write(
              Map(
                "title" -> x.title,
                "content" -> Markdown.toHtml(
                  markdown = x.content getOrElse "No description given.",
                  repository = repository,
                  branch = repository.repository.defaultBranch,
                  enableWikiLink = false,
                  enableRefsLink = true,
                  enableAnchor = true,
                  enableLineBreaks = true,
                  enableTaskList = true,
                  hasWritePermission = true
                )
              )
            )
          }
        } else Unauthorized()
      } getOrElse NotFound()
    }
  })

  ajaxGet("/:owner/:repository/issue_comments/_data/:id")(readableUsersOnly { repository =>
    context.withLoginAccount { loginAccount =>
      getComment(repository.owner, repository.name, params("id")) map { x =>
        if (isEditableContent(x.userName, x.repositoryName, x.commentedUserName, loginAccount)) {
          params.get("dataType") collect {
            case t if t == "html" => html.editcomment(x.content, x.commentId, repository)
          } getOrElse {
            contentType = formats("json")
            org.json4s.jackson.Serialization.write(
              Map(
                "content" -> view.Markdown.toHtml(
                  markdown = x.content,
                  repository = repository,
                  branch = repository.repository.defaultBranch,
                  enableWikiLink = false,
                  enableRefsLink = true,
                  enableAnchor = true,
                  enableLineBreaks = true,
                  enableTaskList = true,
                  hasWritePermission = true
                )
              )
            )
          }
        } else Unauthorized()
      } getOrElse NotFound()
    }
  })

  ajaxPost("/:owner/:repository/issues/new/label")(writableUsersOnly { repository =>
    val labelNames = params("labelNames").split(",")
    val labels = getLabels(repository.owner, repository.name).filter(x => labelNames.contains(x.labelName))
    html.labellist(labels)
  })

  ajaxPost("/:owner/:repository/issues/:id/label/new")(writableUsersOnly { repository =>
    val issueId = params("id").toInt
    registerIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt, true)
    html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
  })

  ajaxPost("/:owner/:repository/issues/:id/label/delete")(writableUsersOnly { repository =>
    val issueId = params("id").toInt
    deleteIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt, true)
    html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
  })

  ajaxPost("/:owner/:repository/issues/:id/assignee/new")(writableUsersOnly { repository =>
    val issueId = params("id").toInt
    registerIssueAssignee(repository.owner, repository.name, issueId, params("assigneeUserName"), true)
    Ok()
  })

  ajaxPost("/:owner/:repository/issues/:id/assignee/delete")(writableUsersOnly { repository =>
    val issueId = params("id").toInt
    deleteIssueAssignee(repository.owner, repository.name, issueId, params("assigneeUserName"), true)
    Ok()
  })

  ajaxPost("/:owner/:repository/issues/:id/milestone")(writableUsersOnly { repository =>
    updateMilestoneId(repository.owner, repository.name, params("id").toInt, milestoneId("milestoneId"), true)
    milestoneId("milestoneId").map { milestoneId =>
      getMilestonesWithIssueCount(repository.owner, repository.name)
        .find(_._1.milestoneId == milestoneId)
        .map { case (_, openCount, closeCount) =>
          gitbucket.core.issues.milestones.html.progress(openCount + closeCount, closeCount)
        } getOrElse NotFound()
    } getOrElse Ok()
  })

  ajaxPost("/:owner/:repository/issues/:id/priority")(writableUsersOnly { repository =>
    val priority = priorityId("priorityId")
    updatePriorityId(repository.owner, repository.name, params("id").toInt, priority, true)
    Ok("updated")
  })

  ajaxPost("/:owner/:repository/issues/customfield_validation/:fieldId")(writableUsersOnly { repository =>
    val fieldId = params("fieldId").toInt
    val value = params("value")
    getCustomField(repository.owner, repository.name, fieldId)
      .flatMap { field =>
        CustomFieldBehavior.validate(field, value, messages).map { error =>
          Ok(error)
        }
      }
      .getOrElse(Ok())
  })

  ajaxPost("/:owner/:repository/issues/:id/customfield/:fieldId")(writableUsersOnly { repository =>
    val issueId = params("id").toInt
    val fieldId = params("fieldId").toInt
    val value = params("value")

    for {
      _ <- getIssue(repository.owner, repository.name, issueId.toString)
      field <- getCustomField(repository.owner, repository.name, fieldId)
    } {
      CustomFieldBehavior.validate(field, value, messages) match {
        case None    => insertOrUpdateCustomFieldValue(field, repository.owner, repository.name, issueId, value)
        case Some(_) => halt(400)
      }
    }
    Ok(value)
  })

  post("/:owner/:repository/issues/batchedit/state")(writableUsersOnly { repository =>
    val action = params.get("value")
    action match {
      case Some("open") =>
        executeBatch(repository) { issueId =>
          getIssue(repository.owner, repository.name, issueId.toString).foreach { issue =>
            handleComment(issue, None, repository, Some("reopen"))
          }
        }
        if (params("uri").nonEmpty) {
          redirect(params("uri"))
        }
      case Some("close") =>
        executeBatch(repository) { issueId =>
          getIssue(repository.owner, repository.name, issueId.toString).foreach { issue =>
            handleComment(issue, None, repository, Some("close"))
          }
        }
        if (params("uri").nonEmpty) {
          redirect(params("uri"))
        }
      case _ => BadRequest()
    }
  })

  post("/:owner/:repository/issues/batchedit/label")(writableUsersOnly { repository =>
    params("value").toIntOpt.map { labelId =>
      executeBatch(repository) { issueId =>
        getIssueLabel(repository.owner, repository.name, issueId, labelId) getOrElse {
          registerIssueLabel(repository.owner, repository.name, issueId, labelId, true)
          if (params("uri").nonEmpty) {
            redirect(params("uri"))
          }
        }
      }
    } getOrElse NotFound()
  })

  post("/:owner/:repository/issues/batchedit/assign")(writableUsersOnly { repository =>
    val value = assignedUserName("value")
    executeBatch(repository) {
      // updateAssignedUserName(repository.owner, repository.name, _, value, true)
      value match {
        case Some(assignedUserName) =>
          registerIssueAssignee(repository.owner, repository.name, _, assignedUserName, true)
        case None =>
          deleteAllIssueAssignees(repository.owner, repository.name, _, true)
      }
    }
    if (params("uri").nonEmpty) {
      redirect(params("uri"))
    }
  })

  post("/:owner/:repository/issues/batchedit/milestone")(writableUsersOnly { repository =>
    val value = milestoneId("value")
    executeBatch(repository) {
      updateMilestoneId(repository.owner, repository.name, _, value, true)
    }
  })

  post("/:owner/:repository/issues/batchedit/priority")(writableUsersOnly { repository =>
    val value = priorityId("value")
    executeBatch(repository) {
      updatePriorityId(repository.owner, repository.name, _, value, true)
    }
  })

  get("/:owner/:repository/_attached/:file")(referrersOnly { repository =>
    (Directory.getAttachedDir(repository.owner, repository.name) match {
      case dir if (dir.exists && dir.isDirectory) =>
        dir.listFiles.find(_.getName.startsWith(params("file") + ".")).map { file =>
          response.setHeader("Content-Disposition", f"""inline; filename=${file.getName}""")
          RawData(FileUtil.getSafeMimeType(file.getName), file)
        }
      case _ => None
    }) getOrElse NotFound()
  })

  /**
   * JSON API for issue and PR completion.
   */
  ajaxGet("/:owner/:repository/_issue/proposals")(writableUsersOnly { repository =>
    contentType = formats("json")
    org.json4s.jackson.Serialization.write(
      Map(
        "options" -> (
          getOpenIssues(repository.owner, repository.name)
            .map { t =>
              Map(
                "label" -> s"""${if (t.isPullRequest) ""
                  else ""} #${StringUtil
                    .escapeHtml(t.issueId.toString)} ${StringUtil
                    .escapeHtml(StringUtil.cutTail(t.title, 50, "..."))}""",
                "value" -> t.issueId.toString
              )
            }
        )
      )
    )
  })

  val assignedUserName = (key: String) => params.get(key) filter (_.trim != "")
  val milestoneId: String => Option[Int] = (key: String) => params.get(key).flatMap(_.toIntOpt)
  val priorityId: String => Option[Int] = (key: String) => params.get(key).flatMap(_.toIntOpt)

  private def executeBatch(repository: RepositoryService.RepositoryInfo)(execute: Int => Unit) = {
    params("checked").split(',') map (_.toInt) foreach execute
    params("from") match {
      case "issues" => redirect(s"/${repository.owner}/${repository.name}/issues")
      case "pulls"  => redirect(s"/${repository.owner}/${repository.name}/pulls")
      case _        =>
    }
  }

  private def searchIssues(repository: RepositoryService.RepositoryInfo, condition: IssueSearchCondition, page: Int) = {
    // search issues
    val issues =
      searchIssue(
        condition,
        IssueSearchOption.Issues,
        (page - 1) * IssueLimit,
        IssueLimit,
        repository.owner -> repository.name
      )

    html.list(
      "issues",
      issues.map(issue => (issue, None)),
      page,
      getAssignableUserNames(repository.owner, repository.name),
      getMilestones(repository.owner, repository.name),
      getPriorities(repository.owner, repository.name),
      getLabels(repository.owner, repository.name),
      countIssue(condition.copy(state = "open"), IssueSearchOption.Issues, repository.owner -> repository.name),
      countIssue(condition.copy(state = "closed"), IssueSearchOption.Issues, repository.owner -> repository.name),
      condition,
      repository,
      isIssueEditable(repository),
      isIssueManageable(repository)
    )
  }

  /**
   * Tests whether an issue or a comment is editable by a logged-in user.
   */
  private def isEditableContent(owner: String, repository: String, author: String, loginAccount: Account)(implicit
    context: Context
  ): Boolean = {
    hasDeveloperRole(owner, repository, context.loginAccount) || author == loginAccount.userName
  }

  /**
   * Tests whether an issue comment is deletable by a logged-in user.
   */
  private def isDeletableComment(owner: String, repository: String, author: String, loginAccount: Account)(implicit
    context: Context
  ): Boolean = {
    hasOwnerRole(owner, repository, context.loginAccount) || author == loginAccount.userName
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy