diff --git a/CHANGELOG.md b/CHANGELOG.md index e83c5da..0811076 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,14 @@ # Changelog All changes to the project will be documented in this file. +### 4.25.0 - 29 May 2018 +- Security improvements +- Show mail address at the profile page +- Task list on commit comments +- More detailed editing history of issues and pull requests +- Expose user public keys +- Download repository improvements + ### 4.24.1 - 1 May 2018 - Fix bug in Web API authentication diff --git a/README.md b/README.md index 2a65ebc..36dd631 100644 --- a/README.md +++ b/README.md @@ -68,17 +68,14 @@ - If you can't find same question and report, send it to [gitter room](https://gitter.im/gitbucket/gitbucket) before raising an issue. - The highest priority of GitBucket is the ease of installation and API compatibility with GitHub, so your feature request might be rejected if they go against those principles. -What's New in 4.24.x +What's New in 4.25.x ------------- -### 4.24.1 - 1 May 2018 -- Fix bug in Web API authentication - -### 4.24.0 - 30 Apr 2018 -- Diff for each review comment on pull requests -- Extra mail addresses support -- Show tags at the commit list -- Keep wrap mode of the online editor -- Renew layout of gitbucket-gist-plugin -- Web API of gitbucket-ci-plugin +### 4.25.0 - 29 May 2018 +- Security improvements +- Show mail address at the profile page +- Task list on commit comments +- More detailed editing history of issues and pull requests +- Expose user public keys +- Download repository improvements See the [change log](CHANGELOG.md) for all of the updates. diff --git a/build.sbt b/build.sbt index 7517ca7..8f113a6 100644 --- a/build.sbt +++ b/build.sbt @@ -3,7 +3,7 @@ val Organization = "io.github.gitbucket" val Name = "gitbucket" -val GitBucketVersion = "4.24.1" +val GitBucketVersion = "4.25.0" val ScalatraVersion = "2.6.1" val JettyVersion = "9.4.7.v20170914" @@ -30,8 +30,8 @@ ) libraryDependencies ++= Seq( - "org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "4.11.0.201803080745-r", - "org.eclipse.jgit" % "org.eclipse.jgit.archive" % "4.11.0.201803080745-r", + "org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "5.0.0.201805301535-rc2", + "org.eclipse.jgit" % "org.eclipse.jgit.archive" % "5.0.0.201805301535-rc2", "org.scalatra" %% "scalatra" % ScalatraVersion, "org.scalatra" %% "scalatra-json" % ScalatraVersion, "org.scalatra" %% "scalatra-forms" % ScalatraVersion, @@ -47,7 +47,7 @@ "com.github.takezoe" %% "blocking-slick-32" % "0.0.10", "com.novell.ldap" % "jldap" % "2009-10-07", "com.h2database" % "h2" % "1.4.196", - "org.mariadb.jdbc" % "mariadb-java-client" % "2.2.3", + "org.mariadb.jdbc" % "mariadb-java-client" % "2.2.5", "org.postgresql" % "postgresql" % "42.1.4", "ch.qos.logback" % "logback-classic" % "1.2.3", "com.zaxxer" % "HikariCP" % "2.7.4", diff --git a/doc/comment_action.md b/doc/comment_action.md index 55d154b..3adae83 100644 --- a/doc/comment_action.md +++ b/doc/comment_action.md @@ -6,22 +6,26 @@ To determine if it was any operation, you see the `ACTION` column. And in the case of some actions, `CONTENT` column value contains additional information. -|ACTION |CONTENT | -|----------------|----------------------| -|comment |comment | -|close_comment |comment | -|reopen_comment |comment | -|close |"Close" | -|reopen |"Reopen" | -|commit |comment commitId | -|merge |comment | -|delete_branch |branchName | -|refer |issueId:title | -|add_label |labelName | -|delete_label |labelName | -|change_priority |oldPriority:priority | -|change_milestone|oldMilestone:milestone| -|assign |oldAssigned:assigned | +|ACTION |CONTENT | +|----------------|--------------------------| +|comment |comment | +|close_comment |comment | +|reopen_comment |comment | +|close |"Close" | +|reopen |"Reopen" | +|commit |comment commitId | +|merge |comment | +|delete_branch |branchName | +|refer |issueId:title | +|add_label |labelName | +|delete_label |labelName | +|change_priority |oldPriority:priority | +|change_milestone|oldMilestone:milestone | +|assign |oldAssigned:assigned | +|change_title |oldTitle(CRLF)title \[1\] | + +\[1\]: (CRLF) is "\r\n" + ### comment @@ -79,3 +83,7 @@ ### assign This value is saved when users have assign issue/PR to user or remove the assign. + +### change_title + +This value is saved when users have changed the title. diff --git a/plugins.json b/plugins.json index c34cb56..3682660 100644 --- a/plugins.json +++ b/plugins.json @@ -31,9 +31,9 @@ "description": "Provides Gist feature on GitBucket.", "versions": [ { - "version": "4.14.0", - "range": ">=4.23.0", - "url": "https://github.com/gitbucket/gitbucket-gist-plugin/releases/download/4.14.0/gitbucket-gist-plugin-assembly-4.14.0.jar" + "version": "4.15.0", + "range": ">=4.25.0", + "url": "https://github.com/gitbucket/gitbucket-gist-plugin/releases/download/4.15.0/gitbucket-gist-plugin-gitbucket_4.25.0-4.15.0.jar" } ], "default": false diff --git a/project/build.properties b/project/build.properties index 7c81737..d6e3507 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.1.5 +sbt.version=1.1.6 diff --git a/src/main/resources/update/gitbucket-core_4.25.xml b/src/main/resources/update/gitbucket-core_4.25.xml new file mode 100644 index 0000000..8f78bd2 --- /dev/null +++ b/src/main/resources/update/gitbucket-core_4.25.xml @@ -0,0 +1,8 @@ + + + + + + EXTRA_MAIL_ADDRESS = '' + + diff --git a/src/main/scala/gitbucket/core/GitBucketCoreModule.scala b/src/main/scala/gitbucket/core/GitBucketCoreModule.scala index 24885e4..33b19f4 100644 --- a/src/main/scala/gitbucket/core/GitBucketCoreModule.scala +++ b/src/main/scala/gitbucket/core/GitBucketCoreModule.scala @@ -53,5 +53,6 @@ new Version("4.23.0", new LiquibaseMigration("update/gitbucket-core_4.23.xml")), new Version("4.23.1"), new Version("4.24.0", new LiquibaseMigration("update/gitbucket-core_4.24.xml")), - new Version("4.24.1") + new Version("4.24.1"), + new Version("4.25.0", new LiquibaseMigration("update/gitbucket-core_4.25.xml")) ) diff --git a/src/main/scala/gitbucket/core/controller/AccountController.scala b/src/main/scala/gitbucket/core/controller/AccountController.scala index e50b583..7663ba9 100644 --- a/src/main/scala/gitbucket/core/controller/AccountController.scala +++ b/src/main/scala/gitbucket/core/controller/AccountController.scala @@ -229,13 +229,15 @@ get("/:userName") { val userName = params("userName") getAccountByUserName(userName).map { account => + val extraMailAddresses = getAccountExtraMailAddresses(userName) params.getOrElse("tab", "repositories") match { // Public Activity case "activity" => gitbucket.core.account.html.activity( account, if (account.isGroupAccount) Nil else getGroupsByUserName(userName), - getActivitiesByUser(userName, true) + getActivitiesByUser(userName, true), + extraMailAddresses ) // Members @@ -244,6 +246,7 @@ gitbucket.core.account.html.members( account, members, + extraMailAddresses, context.loginAccount.exists( x => members.exists { member => @@ -260,6 +263,7 @@ account, if (account.isGroupAccount) Nil else getGroupsByUserName(userName), getVisibleRepositories(context.loginAccount, Some(userName)), + extraMailAddresses, context.loginAccount.exists( x => members.exists { member => @@ -278,6 +282,12 @@ helper.xml.feed(getActivitiesByUser(userName, true)) } + get("/:userName.keys") { + val keys = getPublicKeys(params("userName")) + contentType = "text/plain; charset=utf-8" + keys.map(_.publicKey).mkString("", "\n", "\n") + } + get("/:userName/_avatar") { val userName = params("userName") contentType = "image/png" @@ -318,7 +328,7 @@ account => updateAccount( account.copy( - password = form.password.map(sha1).getOrElse(account.password), + password = form.password.map(pbkdf2_sha256).getOrElse(account.password), fullName = form.fullName, mailAddress = form.mailAddress, description = form.description, @@ -559,7 +569,7 @@ if (context.settings.allowAccountRegistration) { createAccount( form.userName, - sha1(form.password), + pbkdf2_sha256(form.password), form.fullName, form.mailAddress, false, @@ -567,7 +577,7 @@ form.url ) updateImage(form.userName, form.fileId, false) - updateAccountExtraMailAddresses(form.userName, form.extraMailAddresses) + updateAccountExtraMailAddresses(form.userName, form.extraMailAddresses.filter(_ != "")) redirect("/signin") } else NotFound() } @@ -703,7 +713,7 @@ } helper.html.forkrepository( repository, - (groups zip managerPermissions).toMap + (groups zip managerPermissions).sortBy(_._1) ) case _ => redirect(s"/${loginUserName}") } diff --git a/src/main/scala/gitbucket/core/controller/ApiController.scala b/src/main/scala/gitbucket/core/controller/ApiController.scala index 6b8c4ad..fda5fdc 100644 --- a/src/main/scala/gitbucket/core/controller/ApiController.scala +++ b/src/main/scala/gitbucket/core/controller/ApiController.scala @@ -11,6 +11,8 @@ import gitbucket.core.util.SyntaxSugars._ import gitbucket.core.util._ import gitbucket.core.plugin.PluginRegistry +import gitbucket.core.servlet.Database +import gitbucket.core.model.Profile.profile.blockingApi._ import gitbucket.core.view.helpers.{isRenderable, renderMarkup} import org.eclipse.jgit.api.Git import org.eclipse.jgit.revwalk.RevWalk @@ -315,7 +317,10 @@ data.auto_init ) Await.result(f, Duration.Inf) - val repository = getRepository(owner, data.name).get + + val repository = Database() withTransaction { session => + getRepository(owner, data.name)(session).get + } JsonFormat(ApiRepository(repository, ApiUser(getAccountByUserName(owner).get))) } else { ApiError( @@ -806,7 +811,7 @@ ApiCommits( repositoryName = RepositoryName(repository), commitInfo = commitInfo, - diffs = JGitUtil.getDiffs(git, Some(commitInfo.parents.head), commitInfo.id, false, true), + diffs = JGitUtil.getDiffs(git, commitInfo.parents.headOption, commitInfo.id, false, true), author = getAccount(commitInfo.authorName, commitInfo.authorEmailAddress), committer = getAccount(commitInfo.committerName, commitInfo.committerEmailAddress), commentCount = getCommitComment(repository.owner, repository.name, sha).size diff --git a/src/main/scala/gitbucket/core/controller/ControllerBase.scala b/src/main/scala/gitbucket/core/controller/ControllerBase.scala index d1b034e..107f4f1 100644 --- a/src/main/scala/gitbucket/core/controller/ControllerBase.scala +++ b/src/main/scala/gitbucket/core/controller/ControllerBase.scala @@ -254,7 +254,7 @@ repository: RepositoryService.RepositoryInfo ): Unit = { JGitUtil.getObjectLoaderFromId(git, objectId) { loader => - contentType = FileUtil.getMimeType(path) + contentType = FileUtil.getSafeMimeType(path) if (loader.isLarge) { response.setContentLength(loader.getSize.toInt) diff --git a/src/main/scala/gitbucket/core/controller/DashboardController.scala b/src/main/scala/gitbucket/core/controller/DashboardController.scala index 2c8a982..ce0de3a 100644 --- a/src/main/scala/gitbucket/core/controller/DashboardController.scala +++ b/src/main/scala/gitbucket/core/controller/DashboardController.scala @@ -21,6 +21,16 @@ trait DashboardControllerBase extends ControllerBase { self: IssuesService with PullRequestService with RepositoryService with AccountService with UsersAuthenticator => + get("/dashboard/repos")(usersOnly { + val userName = context.loginAccount.get.userName + + html.repos( + getGroupNames(userName), + getVisibleRepositories(None, withoutPhysicalInfo = true), + getUserRepositories(userName, withoutPhysicalInfo = true) + ) + }) + get("/dashboard/issues")(usersOnly { searchIssues("created_by") }) @@ -83,8 +93,7 @@ }, filter, getGroupNames(userName), - Nil, - getUserRepositories(userName, withoutPhysicalInfo = true) + getVisibleRepositories(None, withoutPhysicalInfo = true) ) } @@ -109,8 +118,7 @@ }, filter, getGroupNames(userName), - Nil, - getUserRepositories(userName, withoutPhysicalInfo = true) + getVisibleRepositories(None, withoutPhysicalInfo = true) ) } diff --git a/src/main/scala/gitbucket/core/controller/IndexController.scala b/src/main/scala/gitbucket/core/controller/IndexController.scala index 528aebf..d7b3f5a 100644 --- a/src/main/scala/gitbucket/core/controller/IndexController.scala +++ b/src/main/scala/gitbucket/core/controller/IndexController.scala @@ -9,7 +9,7 @@ import gitbucket.core.service._ import gitbucket.core.util.Implicits._ import gitbucket.core.util.SyntaxSugars._ -import gitbucket.core.util.{Keys, LDAPUtil, ReferrerAuthenticator, UsersAuthenticator} +import gitbucket.core.util._ import org.scalatra.Ok import org.scalatra.forms._ @@ -64,8 +64,7 @@ val visibleOwnerSet: Set[String] = Set(account.userName) ++ getGroupsByUserName(account.userName) gitbucket.core.html.index( getRecentActivitiesByOwners(visibleOwnerSet), - Nil, - getUserRepositories(account.userName, withoutPhysicalInfo = true), + getVisibleRepositories(None, withoutPhysicalInfo = true), showBannerToCreatePersonalAccessToken = hasAccountFederation(account.userName) && !hasAccessToken( account.userName ) @@ -75,7 +74,6 @@ gitbucket.core.html.index( getRecentActivities(), getVisibleRepositories(None, withoutPhysicalInfo = true), - Nil, showBannerToCreatePersonalAccessToken = false ) } @@ -208,7 +206,7 @@ } .map { t => Map( - "label" -> s"@${t.userName} ${t.fullName}", + "label" -> s"@${StringUtil.escapeHtml(t.userName)} ${StringUtil.escapeHtml(t.fullName)}", "value" -> t.userName ) } @@ -273,18 +271,7 @@ val repositories = visibleRepositories.filter { repository => repository.name.toLowerCase.indexOf(query) >= 0 || repository.owner.toLowerCase.indexOf(query) >= 0 } - context.loginAccount - .map { account => - gitbucket.core.search.html.repositories( - query, - repositories, - Nil, - getUserRepositories(account.userName, withoutPhysicalInfo = true) - ) - } - .getOrElse { - gitbucket.core.search.html.repositories(query, repositories, visibleRepositories, Nil) - } + gitbucket.core.search.html.repositories(query, repositories, visibleRepositories) } } diff --git a/src/main/scala/gitbucket/core/controller/IssuesController.scala b/src/main/scala/gitbucket/core/controller/IssuesController.scala index 8fb441a..90676e6 100644 --- a/src/main/scala/gitbucket/core/controller/IssuesController.scala +++ b/src/main/scala/gitbucket/core/controller/IssuesController.scala @@ -154,15 +154,25 @@ ajaxPost("/:owner/:repository/issues/edit_title/:id", issueTitleEditForm)(readableUsersOnly { (title, repository) => defining(repository.owner, repository.name) { case (owner, name) => - getIssue(owner, name, params("id")).map { issue => - if (isEditableContent(owner, name, issue.openedUserName)) { - // update issue - updateIssue(owner, name, issue.issueId, title, issue.content) - // extract references and create refer comment - createReferComment(owner, name, issue.copy(title = title), title, context.loginAccount.get) - - redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}") - } else Unauthorized() + getIssue(owner, name, params("id")).map { + issue => + if (isEditableContent(owner, name, issue.openedUserName)) { + if (issue.title != title) { + // update issue + updateIssue(owner, name, issue.issueId, title, issue.content) + // extract references and create refer comment + createReferComment(owner, name, issue.copy(title = title), title, context.loginAccount.get) + createComment( + owner, + name, + context.loginAccount.get.userName, + issue.issueId, + issue.title + "\r\n" + title, + "change_title" + ) + } + redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}") + } else Unauthorized() } getOrElse NotFound() } }) @@ -396,7 +406,7 @@ 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.getMimeType(file.getName), file) + RawData(FileUtil.getSafeMimeType(file.getName), file) } case _ => None }) getOrElse NotFound() diff --git a/src/main/scala/gitbucket/core/controller/PullRequestsController.scala b/src/main/scala/gitbucket/core/controller/PullRequestsController.scala index 3cbd887..72bedbd 100644 --- a/src/main/scala/gitbucket/core/controller/PullRequestsController.scala +++ b/src/main/scala/gitbucket/core/controller/PullRequestsController.scala @@ -113,7 +113,7 @@ val name = repository.name getPullRequest(owner, name, issueId) map { case (issue, pullreq) => - val (commits, _) = + val (commits, diffs) = getRequestCompareInfo(owner, name, pullreq.commitIdFrom, owner, name, pullreq.commitIdTo) html.conversation( @@ -121,6 +121,7 @@ pullreq, commits.flatten, getPullRequestComments(owner, name, issue.issueId, commits.flatten), + diffs.size, getIssueLabels(owner, name, issueId), getAssignableUserNames(owner, name), getMilestonesWithIssueCount(owner, name), @@ -157,23 +158,25 @@ }) get("/:owner/:repository/pull/:id/commits")(referrersOnly { repository => - params("id").toIntOpt.flatMap { issueId => - val owner = repository.owner - val name = repository.name - getPullRequest(owner, name, issueId) map { - case (issue, pullreq) => - val (commits, _) = - getRequestCompareInfo(owner, name, pullreq.commitIdFrom, owner, name, pullreq.commitIdTo) + params("id").toIntOpt.flatMap { + issueId => + val owner = repository.owner + val name = repository.name + getPullRequest(owner, name, issueId) map { + case (issue, pullreq) => + val (commits, diffs) = + getRequestCompareInfo(owner, name, pullreq.commitIdFrom, owner, name, pullreq.commitIdTo) - html.commits( - issue, - pullreq, - commits, - getPullRequestComments(owner, name, issue.issueId, commits.flatten), - isManageable(repository), - repository - ) - } + html.commits( + issue, + pullreq, + commits, + getPullRequestComments(owner, name, issue.issueId, commits.flatten), + diffs.size, + isManageable(repository), + repository + ) + } } getOrElse NotFound() }) @@ -350,7 +353,16 @@ // close issue by commit message if (pullreq.requestBranch == repository.repository.defaultBranch) { commits.map { commit => - closeIssuesFromMessage(commit.fullMessage, loginAccount.userName, owner, name) + closeIssuesFromMessage(commit.fullMessage, loginAccount.userName, owner, name).foreach { + issueId => + getIssue(repository.owner, repository.name, issueId.toString).map { issue => + callIssuesWebHook("closed", repository, issue, baseUrl, loginAccount) + PluginRegistry().getIssueHooks + .foreach( + _.closedByCommitComment(issue, repository, commit.fullMessage, loginAccount) + ) + } + } } } @@ -455,15 +467,35 @@ val defaultBranch = getRepository(owner, name).get.repository.defaultBranch if (pullreq.branch == defaultBranch) { commits.flatten.foreach { commit => - closeIssuesFromMessage(commit.fullMessage, loginAccount.userName, owner, name) + closeIssuesFromMessage(commit.fullMessage, loginAccount.userName, owner, name).foreach { + issueId => + getIssue(owner, name, issueId.toString).map { issue => + callIssuesWebHook("closed", repository, issue, baseUrl, loginAccount) + PluginRegistry().getIssueHooks + .foreach(_.closedByCommitComment(issue, repository, commit.fullMessage, loginAccount)) + } + } } + val issueContent = issue.title + " " + issue.content.getOrElse("") closeIssuesFromMessage( - issue.title + " " + issue.content.getOrElse(""), + issueContent, loginAccount.userName, owner, name - ) - closeIssuesFromMessage(form.message, loginAccount.userName, owner, name) + ).foreach { issueId => + getIssue(owner, name, issueId.toString).map { issue => + callIssuesWebHook("closed", repository, issue, baseUrl, loginAccount) + PluginRegistry().getIssueHooks + .foreach(_.closedByCommitComment(issue, repository, issueContent, loginAccount)) + } + } + closeIssuesFromMessage(form.message, loginAccount.userName, owner, name).foreach { issueId => + getIssue(owner, name, issueId.toString).map { issue => + callIssuesWebHook("closed", repository, issue, baseUrl, loginAccount) + PluginRegistry().getIssueHooks + .foreach(_.closedByCommitComment(issue, repository, issueContent, loginAccount)) + } + } } updatePullRequests(owner, name, pullreq.branch) @@ -749,6 +781,10 @@ }) ajaxGet("/:owner/:repository/pulls/proposals")(readableUsersOnly { repository => + val thresholdTime = System.currentTimeMillis() - (1000 * 60 * 60) + val mailAddresses = + context.loginAccount.map(x => Seq(x.mailAddress) ++ getAccountExtraMailAddresses(x.userName)).getOrElse(Nil) + val branches = JGitUtil .getBranches( owner = repository.owner, @@ -756,8 +792,14 @@ defaultBranch = repository.repository.defaultBranch, origin = repository.repository.originUserName.isEmpty ) - .filter(x => x.mergeInfo.map(_.ahead).getOrElse(0) > 0 && x.mergeInfo.map(_.behind).getOrElse(0) == 0) - .sortBy(br => (br.mergeInfo.isEmpty, br.commitTime)) + .filter { x => + x.mergeInfo.map(_.ahead).getOrElse(0) > 0 && x.mergeInfo.map(_.behind).getOrElse(0) == 0 && + x.commitTime.getTime > thresholdTime && + mailAddresses.contains(x.committerEmailAddress) + } + .sortBy { br => + (br.mergeInfo.isEmpty, br.commitTime) + } .map(_.name) .reverse diff --git a/src/main/scala/gitbucket/core/controller/ReleasesController.scala b/src/main/scala/gitbucket/core/controller/ReleasesController.scala index 2b37a49..ed44860 100644 --- a/src/main/scala/gitbucket/core/controller/ReleasesController.scala +++ b/src/main/scala/gitbucket/core/controller/ReleasesController.scala @@ -79,7 +79,7 @@ } yield { response.setHeader("Content-Disposition", s"attachment; filename=${asset.label}") RawData( - FileUtil.getMimeType(asset.label), + FileUtil.getSafeMimeType(asset.label), new File(getReleaseFilesDir(repository.owner, repository.name), FileUtil.checkFilename(tagName + "/" + fileId)) ) }).getOrElse(NotFound()) diff --git a/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala b/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala index f4660ae..ce03dc8 100644 --- a/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala +++ b/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala @@ -1,8 +1,8 @@ package gitbucket.core.controller import java.io.File -import javax.servlet.http.{HttpServletRequest, HttpServletResponse} +import javax.servlet.http.{HttpServletRequest, HttpServletResponse} import gitbucket.core.plugin.PluginRegistry import gitbucket.core.repo.html import gitbucket.core.helper @@ -17,6 +17,13 @@ import gitbucket.core.service.WebHookService._ import gitbucket.core.view import gitbucket.core.view.helpers +import org.apache.commons.compress.archivers.{ArchiveEntry, ArchiveOutputStream} +import org.apache.commons.compress.archivers.tar.{TarArchiveEntry, TarArchiveOutputStream} +import org.apache.commons.compress.archivers.zip.{ZipArchiveEntry, ZipArchiveOutputStream} +import org.apache.commons.compress.compressors.bzip2.BZip2CompressorOutputStream +import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream +import org.apache.commons.compress.compressors.xz.XZCompressorOutputStream +import org.apache.commons.compress.utils.IOUtils import org.scalatra.forms._ import org.apache.commons.io.FileUtils import org.eclipse.jgit.api.{ArchiveCommand, Git} @@ -25,6 +32,8 @@ import org.eclipse.jgit.errors.MissingObjectException import org.eclipse.jgit.lib._ import org.eclipse.jgit.transport.{ReceiveCommand, ReceivePack} +import org.eclipse.jgit.treewalk.TreeWalk +import org.eclipse.jgit.treewalk.filter.PathFilter import org.json4s.jackson.Serialization import org.scalatra._ import org.scalatra.i18n.Messages @@ -619,7 +628,8 @@ newLineNumber, issueId, hasWritePermission = hasDeveloperRole(repository.owner, repository.name, context.loginAccount), - repository = repository + repository = repository, + focus = true ) }) @@ -705,6 +715,7 @@ enableRefsLink = true, enableAnchor = true, enableLineBreaks = true, + enableTaskList = true, hasWritePermission = true ) ) @@ -812,16 +823,54 @@ }) /** - * Download repository contents as an archive. + * Download repository contents as a zip archive as compatible URL. */ - get("/:owner/:repository/archive/*")(referrersOnly { repository => - multiParams("splat").head match { - case name if name.endsWith(".zip") => - archiveRepository(name, ".zip", repository) - case name if name.endsWith(".tar.gz") => - archiveRepository(name, ".tar.gz", repository) - case _ => BadRequest() - } + get("/:owner/:repository/archive/:branch.zip")(referrersOnly { repository => + val branch = params("branch") + archiveRepository(branch, branch + ".zip", repository, "") + }) + + /** + * Download repository contents as a tar.gz archive as compatible URL. + */ + get("/:owner/:repository/archive/:branch.tar.gz")(referrersOnly { repository => + val branch = params("branch") + archiveRepository(branch, branch + ".tar.gz", repository, "") + }) + + /** + * Download repository contents as a tar.bz2 archive as compatible URL. + */ + get("/:owner/:repository/archive/:branch.tar.bz2")(referrersOnly { repository => + val branch = params("branch") + archiveRepository(branch, branch + ".tar.bz2", repository, "") + }) + + /** + * Download repository contents as a tar.xz archive as compatible URL. + */ + get("/:owner/:repository/archive/:branch.tar.xz")(referrersOnly { repository => + val branch = params("branch") + archiveRepository(branch, branch + ".tar.xz", repository, "") + }) + + /** + * Download all repository contents as an archive. + */ + get("/:owner/:repository/archive/:branch/:name")(referrersOnly { repository => + val branch = params("branch") + val name = params("name") + archiveRepository(branch, name, repository, "") + }) + + /** + * Download repositories subtree contents as an archive. + */ + get("/:owner/:repository/archive/:branch/*/:name")(referrersOnly { repository => + val branch = params("branch") + val name = params("name") + val path = multiParams("splat").head + archiveRepository(branch, name, repository, path) }) get("/:owner/:repository/network/members")(referrersOnly { repository => @@ -1015,7 +1064,14 @@ // close issue by commit message if (branch == repository.repository.defaultBranch) { - closeIssuesFromMessage(message, loginAccount.userName, repository.owner, repository.name) + closeIssuesFromMessage(message, loginAccount.userName, repository.owner, repository.name).foreach { + issueId => + getIssue(repository.owner, repository.name, issueId.toString).map { issue => + callIssuesWebHook("closed", repository, issue, baseUrl, loginAccount) + PluginRegistry().getIssueHooks + .foreach(_.closedByCommitComment(issue, repository, message, loginAccount)) + } + } } // call post commit hook @@ -1109,26 +1165,97 @@ } } - private def archiveRepository(name: String, suffix: String, repository: RepositoryService.RepositoryInfo): Unit = { - val revision = name.stripSuffix(suffix) + private def archiveRepository( + revision: String, + filename: String, + repository: RepositoryService.RepositoryInfo, + path: String + ) = { + def archive(archiveFormat: String, archive: ArchiveOutputStream)( + entryCreator: (String, Long, Int) => ArchiveEntry + ): Unit = { + using(Git.open(getRepositoryDir(repository.owner, repository.name))) { git => + val oid = git.getRepository.resolve(revision) + val revCommit = JGitUtil.getRevCommitFromId(git, oid) + val sha1 = oid.getName() + val repositorySuffix = (if (sha1.startsWith(revision)) sha1 else revision).replace('/', '-') + val pathSuffix = if (path.isEmpty) "" else '-' + path.replace('/', '-') + val baseName = repository.name + "-" + repositorySuffix + pathSuffix - using(Git.open(getRepositoryDir(repository.owner, repository.name))) { git => - val oid = git.getRepository.resolve(revision) - val revCommit = JGitUtil.getRevCommitFromId(git, oid) - val sha1 = oid.getName() - val repositorySuffix = (if (sha1.startsWith(revision)) sha1 else revision).replace('/', '-') - val filename = repository.name + "-" + repositorySuffix + suffix + using(new TreeWalk(git.getRepository)) { treeWalk => + treeWalk.addTree(revCommit.getTree) + treeWalk.setRecursive(true) + if (!path.isEmpty) { + treeWalk.setFilter(PathFilter.create(path)) + } + if (treeWalk != null) { + while (treeWalk.next()) { + val entryPath = + if (path.isEmpty) baseName + "/" + treeWalk.getPathString + else path.split("/").last + treeWalk.getPathString.substring(path.length) + val size = JGitUtil.getFileSize(git, repository, treeWalk) + val mode = treeWalk.getFileMode.getBits + val entry: ArchiveEntry = entryCreator(entryPath, size, mode) + JGitUtil.openFile(git, repository, revCommit.getTree, treeWalk.getPathString) { in => + archive.putArchiveEntry(entry) + IOUtils.copy(in, archive) + archive.closeArchiveEntry() + } + } + } + } + } + } - contentType = "application/octet-stream" - response.setHeader("Content-Disposition", s"attachment; filename=${filename}") - response.setBufferSize(1024 * 1024); + val suffix = + path.split("/").lastOption.collect { case x if x.length > 0 => "-" + x.replace('/', '_') }.getOrElse("") + val zipRe = """(.+)\.zip$""".r + val tarRe = """(.+)\.tar\.(gz|bz2|xz)$""".r - git.archive - .setFormat(suffix.tail) - .setPrefix(repository.name + "-" + repositorySuffix + "/") - .setTree(revCommit) - .setOutputStream(response.getOutputStream) - .call() + filename match { + case zipRe(branch) => + response.setHeader( + "Content-Disposition", + s"attachment; filename=${repository.name}-${branch}${suffix}.zip" + ) + contentType = "application/octet-stream" + response.setBufferSize(1024 * 1024); + using(new ZipArchiveOutputStream(response.getOutputStream)) { zip => + archive(".zip", zip) { (path, size, mode) => + val entry = new ZipArchiveEntry(path) + entry.setSize(size) + entry.setUnixMode(mode) + entry + } + } + () + case tarRe(branch, compressor) => + response.setHeader( + "Content-Disposition", + s"attachment; filename=${repository.name}-${branch}${suffix}.tar.${compressor}" + ) + contentType = "application/octet-stream" + response.setBufferSize(1024 * 1024) + using(compressor match { + case "gz" => new GzipCompressorOutputStream(response.getOutputStream) + case "bz2" => new BZip2CompressorOutputStream(response.getOutputStream) + case "xz" => new XZCompressorOutputStream(response.getOutputStream) + }) { compressorOutputStream => + using(new TarArchiveOutputStream(compressorOutputStream)) { tar => + tar.setBigNumberMode(TarArchiveOutputStream.BIGNUMBER_STAR) + tar.setLongFileMode(TarArchiveOutputStream.LONGFILE_GNU) + tar.setAddPaxHeadersForNonAsciiNames(true) + archive(".tar.gz", tar) { (path, size, mode) => + val entry = new TarArchiveEntry(path) + entry.setSize(size) + entry.setMode(mode) + entry + } + } + } + () + case _ => + BadRequest() } } diff --git a/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala b/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala index acd30d4..69f0223 100644 --- a/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala +++ b/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala @@ -90,7 +90,8 @@ "jwsAlgorithm" -> trim(label("Signature algorithm", optional(text()))) )(OIDC.apply) ), - "skinName" -> trim(label("AdminLTE skin name", text(required))) + "skinName" -> trim(label("AdminLTE skin name", text(required))), + "showMailAddress" -> trim(label("Show mail address", boolean())) )(SystemSettings.apply).verifying { settings => Vector( if (settings.ssh && settings.baseUrl.isEmpty) { @@ -416,7 +417,7 @@ post("/admin/users/_newuser", newUserForm)(adminOnly { form => createAccount( form.userName, - sha1(form.password), + pbkdf2_sha256(form.password), form.fullName, form.mailAddress, form.isAdmin, @@ -456,7 +457,7 @@ updateAccount( account.copy( - password = form.password.map(sha1).getOrElse(account.password), + password = form.password.map(pbkdf2_sha256).getOrElse(account.password), fullName = form.fullName, mailAddress = form.mailAddress, isAdmin = form.isAdmin, diff --git a/src/main/scala/gitbucket/core/plugin/IssueHook.scala b/src/main/scala/gitbucket/core/plugin/IssueHook.scala index 12d050a..1b22971 100644 --- a/src/main/scala/gitbucket/core/plugin/IssueHook.scala +++ b/src/main/scala/gitbucket/core/plugin/IssueHook.scala @@ -1,7 +1,7 @@ package gitbucket.core.plugin import gitbucket.core.controller.Context -import gitbucket.core.model.Issue +import gitbucket.core.model.{Account, Issue} import gitbucket.core.service.RepositoryService.RepositoryInfo import gitbucket.core.model.Profile._ import profile.api._ @@ -15,6 +15,19 @@ ): Unit = () def closed(issue: Issue, repository: RepositoryInfo)(implicit session: Session, context: Context): Unit = () def reopened(issue: Issue, repository: RepositoryInfo)(implicit session: Session, context: Context): Unit = () + def assigned( + issue: Issue, + repository: RepositoryInfo, + assigner: Option[String], + assigned: Option[String], + oldAssigned: Option[String] + )( + implicit session: Session, + context: Context + ): Unit = () + def closedByCommitComment(issue: Issue, repository: RepositoryInfo, message: String, pusher: Account)( + implicit session: Session + ): Unit = () } diff --git a/src/main/scala/gitbucket/core/service/AccessTokenService.scala b/src/main/scala/gitbucket/core/service/AccessTokenService.scala index 102240d..8976ce2 100644 --- a/src/main/scala/gitbucket/core/service/AccessTokenService.scala +++ b/src/main/scala/gitbucket/core/service/AccessTokenService.scala @@ -5,13 +5,13 @@ import gitbucket.core.model.{AccessToken, Account} import gitbucket.core.util.StringUtil -import scala.util.Random +import java.security.SecureRandom trait AccessTokenService { def makeAccessTokenString: String = { val bytes = new Array[Byte](20) - Random.nextBytes(bytes) + AccessTokenService.secureRandom.nextBytes(bytes) bytes.map("%02x".format(_)).mkString } @@ -55,4 +55,6 @@ } -object AccessTokenService extends AccessTokenService +object AccessTokenService extends AccessTokenService { + private val secureRandom = new SecureRandom() +} diff --git a/src/main/scala/gitbucket/core/service/AccountService.scala b/src/main/scala/gitbucket/core/service/AccountService.scala index 8f0ba63..ea851b1 100644 --- a/src/main/scala/gitbucket/core/service/AccountService.scala +++ b/src/main/scala/gitbucket/core/service/AccountService.scala @@ -33,7 +33,16 @@ * Authenticate by internal database. */ private def defaultAuthentication(userName: String, password: String)(implicit s: Session) = { + val pbkdf2re = """^\$pbkdf2-sha256\$(\d+)\$([0-9a-zA-Z+/=]+)\$([0-9a-zA-Z+/=]+)$""".r getAccountByUserName(userName).collect { + case account if !account.isGroupAccount => + account.password match { + case pbkdf2re(iter, salt, hash) if (pbkdf2_sha256(iter.toInt, salt, password) == hash) => Some(account) + case p if p == sha1(password) => + updateAccount(account.copy(password = pbkdf2_sha256(password))) + Some(account) + case _ => None + } case account if (!account.isGroupAccount && account.password == sha1(password)) => Some(account) } getOrElse None } diff --git a/src/main/scala/gitbucket/core/service/IssuesService.scala b/src/main/scala/gitbucket/core/service/IssuesService.scala index c9854ea..05255c4 100644 --- a/src/main/scala/gitbucket/core/service/IssuesService.scala +++ b/src/main/scala/gitbucket/core/service/IssuesService.scala @@ -6,20 +6,21 @@ import gitbucket.core.util.SyntaxSugars._ import gitbucket.core.controller.Context import gitbucket.core.model.{ + Account, + CommitState, Issue, - PullRequest, IssueComment, IssueLabel, Label, - Account, + PullRequest, Repository, - CommitState, Role } import gitbucket.core.model.Profile._ import gitbucket.core.model.Profile.profile._ import gitbucket.core.model.Profile.profile.blockingApi._ import gitbucket.core.model.Profile.dateColumnType +import gitbucket.core.plugin.PluginRegistry trait IssuesService { self: AccountService with RepositoryService with LabelsService with PrioritiesService with MilestonesService => @@ -511,20 +512,24 @@ assignedUserName: Option[String], insertComment: Boolean = false )(implicit context: Context, s: Session): Int = { + val oldAssigned = getIssue(owner, repository, s"${issueId}").get.assignedUserName + val assigned = assignedUserName + val assigner = context.loginAccount.map(_.userName) if (insertComment) { - val oldAssigned = getIssue(owner, repository, s"${issueId}").get.assignedUserName.getOrElse("Not assigned") - val assigned = assignedUserName.getOrElse("Not assigned") IssueComments insert IssueComment( userName = owner, repositoryName = repository, issueId = issueId, action = "assign", - commentedUserName = context.loginAccount.map(_.userName).getOrElse("Unknown user"), - content = s"${oldAssigned}:${assigned}", + commentedUserName = assigner.getOrElse("Unknown user"), + content = s"""${oldAssigned.getOrElse("Not assigned")}:${assigned.getOrElse("Not assigned")}""", registeredDate = currentDate, updatedDate = currentDate ) } + for (issue <- getIssue(owner, repository, issueId.toString); repo <- getRepository(owner, repository)) { + PluginRegistry().getIssueHooks.foreach(_.assigned(issue, repo, assigner, assigned, oldAssigned)) + } Issues .filter(_.byPrimaryKey(owner, repository, issueId)) .map(t => (t.assignedUserName ?, t.updatedDate)) diff --git a/src/main/scala/gitbucket/core/service/RepositoryService.scala b/src/main/scala/gitbucket/core/service/RepositoryService.scala index f4ab4be..70e2387 100644 --- a/src/main/scala/gitbucket/core/service/RepositoryService.scala +++ b/src/main/scala/gitbucket/core/service/RepositoryService.scala @@ -338,7 +338,7 @@ repository.originUserName.getOrElse(repository.userName), repository.originRepositoryName.getOrElse(repository.repositoryName) ), - getRepositoryManagers(repository.userName) + getRepositoryManagers(repository.userName, repository.repositoryName) ) } } @@ -407,7 +407,7 @@ if (withoutPhysicalInfo) { Nil } else { - getRepositoryManagers(repository.userName) + getRepositoryManagers(repository.userName, repository.repositoryName) } ) } @@ -485,18 +485,22 @@ if (withoutPhysicalInfo) { Nil } else { - getRepositoryManagers(repository.userName) + getRepositoryManagers(repository.userName, repository.repositoryName) } ) } } - private def getRepositoryManagers(userName: String)(implicit s: Session): Seq[String] = - if (getAccountByUserName(userName).exists(_.isGroupAccount)) { - getGroupMembers(userName).collect { case x if (x.isManager) => x.userName } - } else { - Seq(userName) - } + /** + * TODO It seems to be able to improve performance. For example, RequestCache can be used for getAccountByUserName call. + */ + private def getRepositoryManagers(userName: String, repositoryName: String)(implicit s: Session): Seq[String] = { + (if (getAccountByUserName(userName).exists(_.isGroupAccount)) { + getGroupMembers(userName).collect { case x if (x.isManager) => x.userName } + } else { + Seq(userName) + }) ++ getCollaboratorUserNames(userName, repositoryName, Seq(Role.ADMIN)) + } /** * Updates the last activity date of the repository. diff --git a/src/main/scala/gitbucket/core/service/SystemSettingsService.scala b/src/main/scala/gitbucket/core/service/SystemSettingsService.scala index 4141822..7ac861e 100644 --- a/src/main/scala/gitbucket/core/service/SystemSettingsService.scala +++ b/src/main/scala/gitbucket/core/service/SystemSettingsService.scala @@ -68,6 +68,7 @@ } } props.setProperty(SkinName, settings.skinName.toString) + props.setProperty(ShowMailAddress, settings.showMailAddress.toString) using(new java.io.FileOutputStream(GitBucketConf)) { out => props.store(out, null) } @@ -144,7 +145,8 @@ } else { None }, - getValue(props, SkinName, "skin-blue") + getValue(props, SkinName, "skin-blue"), + getValue(props, ShowMailAddress, false) ) } } @@ -174,7 +176,8 @@ ldap: Option[Ldap], oidcAuthentication: Boolean, oidc: Option[OIDC], - skinName: String + skinName: String, + showMailAddress: Boolean ) { def baseUrl(request: HttpServletRequest): String = @@ -283,6 +286,7 @@ private val OidcClientSecret = "oidc.client_secret" private val OidcJwsAlgorithm = "oidc.jws_algorithm" private val SkinName = "skinName" + private val ShowMailAddress = "showMailAddress" private def getValue[A: ClassTag](props: java.util.Properties, key: String, default: A): A = { getSystemProperty(key).getOrElse(getEnvironmentVariable(key).getOrElse { diff --git a/src/main/scala/gitbucket/core/servlet/GitRepositoryServlet.scala b/src/main/scala/gitbucket/core/servlet/GitRepositoryServlet.scala index cfc5f3b..bbfc469 100644 --- a/src/main/scala/gitbucket/core/servlet/GitRepositoryServlet.scala +++ b/src/main/scala/gitbucket/core/servlet/GitRepositoryServlet.scala @@ -300,6 +300,8 @@ closeIssuesFromMessage(commit.fullMessage, pusher, owner, repository).foreach { issueId => getIssue(owner, repository, issueId.toString).map { issue => callIssuesWebHook("closed", repositoryInfo, issue, baseUrl, pusherAccount) + PluginRegistry().getIssueHooks + .foreach(_.closedByCommitComment(issue, repositoryInfo, commit.fullMessage, pusherAccount)) } } } diff --git a/src/main/scala/gitbucket/core/servlet/PluginAssetsServlet.scala b/src/main/scala/gitbucket/core/servlet/PluginAssetsServlet.scala index 016d2ea..3e891af 100644 --- a/src/main/scala/gitbucket/core/servlet/PluginAssetsServlet.scala +++ b/src/main/scala/gitbucket/core/servlet/PluginAssetsServlet.scala @@ -26,7 +26,7 @@ try { val bytes = IOUtils.toByteArray(in) resp.setContentLength(bytes.length) - resp.setContentType(FileUtil.getContentType(path, bytes)) + resp.setContentType(FileUtil.getMimeType(path, bytes)) resp.setHeader("Cache-Control", "max-age=3600") resp.getOutputStream.write(bytes) } finally { diff --git a/src/main/scala/gitbucket/core/util/FileUtil.scala b/src/main/scala/gitbucket/core/util/FileUtil.scala index 0a668fe..ab42edd 100644 --- a/src/main/scala/gitbucket/core/util/FileUtil.scala +++ b/src/main/scala/gitbucket/core/util/FileUtil.scala @@ -16,7 +16,7 @@ } } - def getContentType(name: String, bytes: Array[Byte]): String = { + def getMimeType(name: String, bytes: Array[Byte]): String = { defining(getMimeType(name)) { mimeType => if (mimeType == "application/octet-stream" && isText(bytes)) { "text/plain" @@ -26,6 +26,10 @@ } } + def getSafeMimeType(name: String): String = { + getMimeType(name).replace("text/html", "text/plain") + } + def isImage(name: String): Boolean = getMimeType(name).startsWith("image/") def isLarge(size: Long): Boolean = (size > 1024 * 1000) diff --git a/src/main/scala/gitbucket/core/util/JGitUtil.scala b/src/main/scala/gitbucket/core/util/JGitUtil.scala index b6c8273..b9f0830 100644 --- a/src/main/scala/gitbucket/core/util/JGitUtil.scala +++ b/src/main/scala/gitbucket/core/util/JGitUtil.scala @@ -1,6 +1,6 @@ package gitbucket.core.util -import java.io.ByteArrayOutputStream +import java.io.{ByteArrayOutputStream, File, FileInputStream, InputStream} import gitbucket.core.service.RepositoryService import org.eclipse.jgit.api.Git @@ -1220,4 +1220,61 @@ Option(git.getRepository.resolve(revstr)).map(ObjectId.toString(_)) } } + + def getFileSize(git: Git, repository: RepositoryService.RepositoryInfo, treeWalk: TreeWalk): Long = { + val attrs = treeWalk.getAttributes + val loader = git.getRepository.open(treeWalk.getObjectId(0)) + if (attrs.containsKey("filter") && attrs.get("filter").getValue == "lfs") { + val lfsAttrs = getLfsAttributes(loader) + lfsAttrs.get("size").map(_.toLong).get + } else { + loader.getSize + } + } + + def getFileSize(git: Git, repository: RepositoryService.RepositoryInfo, tree: RevTree, path: String): Long = { + using(TreeWalk.forPath(git.getRepository, path, tree)) { treeWalk => + getFileSize(git, repository, treeWalk) + } + } + + def openFile[T](git: Git, repository: RepositoryService.RepositoryInfo, treeWalk: TreeWalk)( + f: InputStream => T + ): T = { + val attrs = treeWalk.getAttributes + val loader = git.getRepository.open(treeWalk.getObjectId(0)) + if (attrs.containsKey("filter") && attrs.get("filter").getValue == "lfs") { + val lfsAttrs = getLfsAttributes(loader) + if (lfsAttrs.nonEmpty) { + val oid = lfsAttrs("oid").split(":")(1) + + val file = new File(FileUtil.getLfsFilePath(repository.owner, repository.name, oid)) + using(new FileInputStream(FileUtil.getLfsFilePath(repository.owner, repository.name, oid))) { in => + f(in) + } + } else { + throw new NoSuchElementException("LFS attribute is empty.") + } + } else { + using(loader.openStream()) { in => + f(in) + } + } + } + + def openFile[T](git: Git, repository: RepositoryService.RepositoryInfo, tree: RevTree, path: String)( + f: InputStream => T + ): T = { + using(TreeWalk.forPath(git.getRepository, path, tree)) { treeWalk => + openFile(git, repository, treeWalk)(f) + } + } + + private def getLfsAttributes(loader: ObjectLoader): Map[String, String] = { + val bytes = loader.getCachedBytes + val text = new String(bytes, "UTF-8") + + JGitUtil.getLfsObjects(text) + } + } diff --git a/src/main/scala/gitbucket/core/util/StringUtil.scala b/src/main/scala/gitbucket/core/util/StringUtil.scala index bbbe5b4..b470eeb 100644 --- a/src/main/scala/gitbucket/core/util/StringUtil.scala +++ b/src/main/scala/gitbucket/core/util/StringUtil.scala @@ -1,10 +1,13 @@ package gitbucket.core.util import java.net.{URLDecoder, URLEncoder} +import java.security.SecureRandom import java.util.{Base64, UUID} import org.mozilla.universalchardet.UniversalDetector import SyntaxSugars._ +import javax.crypto.SecretKeyFactory +import javax.crypto.spec.PBEKeySpec import org.apache.commons.io.input.BOMInputStream import org.apache.commons.io.IOUtils @@ -16,6 +19,32 @@ UUID.randomUUID().toString.substring(0, 16) } + def base64Encode(value: Array[Byte]): String = { + Base64.getEncoder.encodeToString(value) + } + + def base64Decode(value: String): Array[Byte] = { + Base64.getDecoder.decode(value) + } + + def pbkdf2_sha256(iter: Int, salt: String, value: String): String = { + val keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256") + val ks = new PBEKeySpec(value.toCharArray, base64Decode(salt), iter, 256) + val s = keyFactory.generateSecret(ks) + base64Encode(s.getEncoded) + } + + def pbkdf2_sha256(value: String) = { + val keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256") + val secureRandom = new SecureRandom + val salt: Array[Byte] = new Array(32) + secureRandom.nextBytes(salt) + val iter = 100000 + val ks = new PBEKeySpec(value.toCharArray, salt, iter, 256) + val s = keyFactory.generateSecret(ks) + s"""$$pbkdf2-sha256$$${iter}$$${base64Encode(salt)}$$${base64Encode(s.getEncoded)}""" + } + def sha1(value: String): String = defining(java.security.MessageDigest.getInstance("SHA-1")) { md => md.update(value.getBytes) diff --git a/src/main/scala/gitbucket/core/view/helpers.scala b/src/main/scala/gitbucket/core/view/helpers.scala index 848981a..4e757c7 100644 --- a/src/main/scala/gitbucket/core/view/helpers.scala +++ b/src/main/scala/gitbucket/core/view/helpers.scala @@ -250,12 +250,12 @@ * Generates the url to the repository. */ def url(repository: RepositoryService.RepositoryInfo)(implicit context: Context): String = - s"${context.path}/${repository.owner}/${repository.name}" + s"${context.path}/${encodeRefName(repository.owner)}/${encodeRefName(repository.name)}" /** * Generates the url to the account page. */ - def url(userName: String)(implicit context: Context): String = s"${context.path}/${userName}" + def url(userName: String)(implicit context: Context): String = s"${context.path}/${encodeRefName(userName)}" /** * Returns the url to the root of assets. @@ -273,7 +273,7 @@ * If user does not exist or disabled, this method returns user name as text without link. */ def user(userName: String, mailAddress: String = "", styleClass: String = "")(implicit context: Context): Html = - userWithContent(userName, mailAddress, styleClass)(Html(userName)) + userWithContent(userName, mailAddress, styleClass)(Html(StringUtil.escapeHtml(userName))) /** * Generates the avatar link to the account page. @@ -316,44 +316,6 @@ */ def isPast(date: Date): Boolean = System.currentTimeMillis > date.getTime - /** - * Returns file type for AceEditor. - */ - def editorType(fileName: String): String = { - fileName.toLowerCase match { - case x if (x.endsWith(".bat")) => "batchfile" - case x if (x.endsWith(".java")) => "java" - case x if (x.endsWith(".scala")) => "scala" - case x if (x.endsWith(".js")) => "javascript" - case x if (x.endsWith(".css")) => "css" - case x if (x.endsWith(".md")) => "markdown" - case x if (x.endsWith(".html")) => "html" - case x if (x.endsWith(".xml")) => "xml" - case x if (x.endsWith(".c")) => "c_cpp" - case x if (x.endsWith(".cpp")) => "c_cpp" - case x if (x.endsWith(".coffee")) => "coffee" - case x if (x.endsWith(".ejs")) => "ejs" - case x if (x.endsWith(".hs")) => "haskell" - case x if (x.endsWith(".json")) => "json" - case x if (x.endsWith(".jsp")) => "jsp" - case x if (x.endsWith(".jsx")) => "jsx" - case x if (x.endsWith(".cl")) => "lisp" - case x if (x.endsWith(".clojure")) => "lisp" - case x if (x.endsWith(".lua")) => "lua" - case x if (x.endsWith(".php")) => "php" - case x if (x.endsWith(".py")) => "python" - case x if (x.endsWith(".rdoc")) => "rdoc" - case x if (x.endsWith(".rhtml")) => "rhtml" - case x if (x.endsWith(".ruby")) => "ruby" - case x if (x.endsWith(".sh")) => "sh" - case x if (x.endsWith(".sql")) => "sql" - case x if (x.endsWith(".tcl")) => "tcl" - case x if (x.endsWith(".vbs")) => "vbscript" - case x if (x.endsWith(".yml")) => "yaml" - case _ => "plain_text" - } - } - def pre(value: Html): Html = Html(s"
${value.body.trim.split("\n").map(_.trim).mkString("\n")}
") /** diff --git a/src/main/twirl/gitbucket/core/account/activity.scala.html b/src/main/twirl/gitbucket/core/account/activity.scala.html index e3b1307..f0d2cdc 100644 --- a/src/main/twirl/gitbucket/core/account/activity.scala.html +++ b/src/main/twirl/gitbucket/core/account/activity.scala.html @@ -1,8 +1,9 @@ @(account: gitbucket.core.model.Account, groupNames: List[String], - activities: List[gitbucket.core.model.Activity])(implicit context: gitbucket.core.controller.Context) + activities: List[gitbucket.core.model.Activity], + extraMailAddresses: List[String])(implicit context: gitbucket.core.controller.Context) @import gitbucket.core.view.helpers -@gitbucket.core.account.html.main(account, groupNames, "activity"){ +@gitbucket.core.account.html.main(account, groupNames, "activity", extraMailAddresses){
activities
diff --git a/src/main/twirl/gitbucket/core/account/main.scala.html b/src/main/twirl/gitbucket/core/account/main.scala.html index 3a0c588..4c2ebc0 100644 --- a/src/main/twirl/gitbucket/core/account/main.scala.html +++ b/src/main/twirl/gitbucket/core/account/main.scala.html @@ -1,4 +1,4 @@ -@(account: gitbucket.core.model.Account, groupNames: List[String], active: String, +@(account: gitbucket.core.model.Account, groupNames: List[String], active: String, extraMailAddresses: List[String], isGroupManager: Boolean = false)(body: Html)(implicit context: gitbucket.core.controller.Context) @import gitbucket.core.view.helpers @gitbucket.core.html.main(account.userName){ @@ -20,6 +20,16 @@ @account.url

} + @if(context.settings.showMailAddress){ +

+ @account.mailAddress +

+ @extraMailAddresses.map{ mail => +

+ @mail +

+ } + }

Joined on @helpers.date(account.registeredDate)

diff --git a/src/main/twirl/gitbucket/core/account/members.scala.html b/src/main/twirl/gitbucket/core/account/members.scala.html index 996dc1d..ce8edc9 100644 --- a/src/main/twirl/gitbucket/core/account/members.scala.html +++ b/src/main/twirl/gitbucket/core/account/members.scala.html @@ -1,16 +1,16 @@ -@(account: gitbucket.core.model.Account, members: List[gitbucket.core.model.GroupMember], isGroupManager: Boolean)(implicit context: gitbucket.core.controller.Context) +@(account: gitbucket.core.model.Account, members: List[gitbucket.core.model.GroupMember], extraMailAddresses: List[String], isGroupManager: Boolean)(implicit context: gitbucket.core.controller.Context) @import gitbucket.core.view.helpers -@gitbucket.core.account.html.main(account, Nil, "members", isGroupManager){ +@gitbucket.core.account.html.main(account, Nil, "members", extraMailAddresses, isGroupManager){ @if(members.isEmpty){ No members } else { @members.map { member =>
- @helpers.avatar(member.userName, 20) @member.userName + @helpers.avatarLink(member.userName, 20) @member.userName @if(member.isManager){ (Manager) }
} } -} \ No newline at end of file +} diff --git a/src/main/twirl/gitbucket/core/account/newrepo.scala.html b/src/main/twirl/gitbucket/core/account/newrepo.scala.html index fa0f307..0c853bd 100644 --- a/src/main/twirl/gitbucket/core/account/newrepo.scala.html +++ b/src/main/twirl/gitbucket/core/account/newrepo.scala.html @@ -32,7 +32,7 @@
Repository name
- +
diff --git a/src/main/twirl/gitbucket/core/account/repositories.scala.html b/src/main/twirl/gitbucket/core/account/repositories.scala.html index b4c7acf..1d074f9 100644 --- a/src/main/twirl/gitbucket/core/account/repositories.scala.html +++ b/src/main/twirl/gitbucket/core/account/repositories.scala.html @@ -1,8 +1,9 @@ @(account: gitbucket.core.model.Account, groupNames: List[String], repositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo], + extraMailAddresses: List[String], isGroupManager: Boolean)(implicit context: gitbucket.core.controller.Context) @import gitbucket.core.view.helpers -@gitbucket.core.account.html.main(account, groupNames, "repositories", isGroupManager){ +@gitbucket.core.account.html.main(account, groupNames, "repositories", extraMailAddresses, isGroupManager){ @if(repositories.isEmpty){ No repositories } else { diff --git a/src/main/twirl/gitbucket/core/admin/settings_system.scala.html b/src/main/twirl/gitbucket/core/admin/settings_system.scala.html index 7d787fa..c768d6b 100644 --- a/src/main/twirl/gitbucket/core/admin/settings_system.scala.html +++ b/src/main/twirl/gitbucket/core/admin/settings_system.scala.html @@ -9,6 +9,10 @@ Value + GITBUCKET_VERSION + @gitbucket.core.GitBucketCoreModule.getVersions.last.getVersion + + GITBUCKET_HOME @gitbucket.core.util.Directory.GitBucketHome @@ -132,6 +136,21 @@ + + +
+ +
+ + +
+
diff --git a/src/main/twirl/gitbucket/core/admin/userlist.scala.html b/src/main/twirl/gitbucket/core/admin/userlist.scala.html index 131f357..85d694c 100644 --- a/src/main/twirl/gitbucket/core/admin/userlist.scala.html +++ b/src/main/twirl/gitbucket/core/admin/userlist.scala.html @@ -26,7 +26,7 @@ }
- @helpers.avatar(account.userName, 20) + @helpers.avatarLink(account.userName, 20) @account.userName @if(account.isGroupAccount){ (Group) @@ -39,7 +39,7 @@ } @if(account.isGroupAccount){ @members(account.userName).map { userName => - @helpers.avatar(userName, 20, tooltip = true) + @helpers.avatarLink(userName, 20, tooltip = true) } }
diff --git a/src/main/twirl/gitbucket/core/dashboard/issues.scala.html b/src/main/twirl/gitbucket/core/dashboard/issues.scala.html index f23e576..c2e92ed 100644 --- a/src/main/twirl/gitbucket/core/dashboard/issues.scala.html +++ b/src/main/twirl/gitbucket/core/dashboard/issues.scala.html @@ -5,10 +5,9 @@ condition: gitbucket.core.service.IssuesService.IssueSearchCondition, filter: String, groups: List[String], - recentRepositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo], - userRepositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo])(implicit context: gitbucket.core.controller.Context) + recentRepositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo])(implicit context: gitbucket.core.controller.Context) @gitbucket.core.html.main("Issues"){ - @gitbucket.core.dashboard.html.sidebar(recentRepositories, userRepositories){ + @gitbucket.core.dashboard.html.sidebar(recentRepositories){ @gitbucket.core.dashboard.html.tab("issues")
@gitbucket.core.dashboard.html.issuesnavi("issues", filter, openCount, closedCount, condition) diff --git a/src/main/twirl/gitbucket/core/dashboard/pulls.scala.html b/src/main/twirl/gitbucket/core/dashboard/pulls.scala.html index 3d05a84..b3cf909 100644 --- a/src/main/twirl/gitbucket/core/dashboard/pulls.scala.html +++ b/src/main/twirl/gitbucket/core/dashboard/pulls.scala.html @@ -5,10 +5,9 @@ condition: gitbucket.core.service.IssuesService.IssueSearchCondition, filter: String, groups: List[String], - recentRepositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo], - userRepositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo])(implicit context: gitbucket.core.controller.Context) + recentRepositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo])(implicit context: gitbucket.core.controller.Context) @gitbucket.core.html.main("Pull requests"){ - @gitbucket.core.dashboard.html.sidebar(recentRepositories, userRepositories){ + @gitbucket.core.dashboard.html.sidebar(recentRepositories){ @gitbucket.core.dashboard.html.tab("pulls")
@gitbucket.core.dashboard.html.issuesnavi("pulls", filter, openCount, closedCount, condition) diff --git a/src/main/twirl/gitbucket/core/dashboard/repos.scala.html b/src/main/twirl/gitbucket/core/dashboard/repos.scala.html new file mode 100644 index 0000000..a3ac8c6 --- /dev/null +++ b/src/main/twirl/gitbucket/core/dashboard/repos.scala.html @@ -0,0 +1,71 @@ +@(groups: List[String], + recentRepositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo], + userRepositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo])(implicit context: gitbucket.core.controller.Context) +@import gitbucket.core.view.helpers +@gitbucket.core.html.main("Repositories"){ + @gitbucket.core.dashboard.html.sidebar(recentRepositories){ + @gitbucket.core.dashboard.html.tab("repos") +
+
+ + +
+ @if(userRepositories.isEmpty){ + No repositories + } else { + @userRepositories.map { repository => +
+
+ @gitbucket.core.helper.html.repositoryicon(repository, true) +
+
+
+ @repository.owner/@repository.name + @if(repository.repository.isPrivate){ + + } +
+ @if(repository.repository.originUserName.isDefined){ + + } + @if(repository.repository.description.isDefined){ +
@repository.repository.description
+ } +
Updated @gitbucket.core.helper.html.datetimeago(repository.repository.lastActivityDate)
+
+
+ } + } +
+ } +} + diff --git a/src/main/twirl/gitbucket/core/dashboard/sidebar.scala.html b/src/main/twirl/gitbucket/core/dashboard/sidebar.scala.html index 9f59a2f..ee7feb0 100644 --- a/src/main/twirl/gitbucket/core/dashboard/sidebar.scala.html +++ b/src/main/twirl/gitbucket/core/dashboard/sidebar.scala.html @@ -1,29 +1,8 @@ -@(recentRepositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo], - userRepositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo])(body: Html)(implicit context: gitbucket.core.controller.Context) +@(recentRepositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo])(body: Html)(implicit context: gitbucket.core.controller.Context) @import gitbucket.core.view.helpers
diff --git a/src/main/twirl/gitbucket/core/dashboard/tab.scala.html b/src/main/twirl/gitbucket/core/dashboard/tab.scala.html index 597509f..ee2c17d 100644 --- a/src/main/twirl/gitbucket/core/dashboard/tab.scala.html +++ b/src/main/twirl/gitbucket/core/dashboard/tab.scala.html @@ -2,6 +2,7 @@