diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..4479460 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,7 @@ +# Guideline for Issues + +- At first, See [FAQ](https://github.com/gitbucket/gitbucket/wiki/FAQ) and check issues whether there is a same question or request in the past. +- If you can't find same question and report, send it to [gitter room](https://gitter.im/gitbucket/gitbucket) before raising an issue. +- We can also support in Japaneses other than English at [gitter room for Japanese](https://gitter.im/gitbucket/gitbucket_ja). +- Write an issue in English. At least, write subject in English. +- First priority of GitBucket is easy installation and reproduce GitHub behavior, so we might reject if your request is against it. diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..4642490 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,19 @@ +### Before submitting an issue to Gitbucket I have first: + +- [] read the [contribution guidelines](https://github.com/gitbucket/gitbucket/blob/master/.github/CONTRIBUTING.md) +- [] searched for similar already existing issue +- [] read the documentation and [wiki](https://github.com/gitbucket/gitbucket/wiki) + +*(if you have performed all the above, remove the paragraph and continue describing the issue with template below)* + +## Issue +**Impacted version**: xxxx + +**Deployment mode**: *explain here how you use gitbucket : standalone app, under webcontainer (which one), with an http frontend (nginx, httpd, ...)* + +**Problem description**: +- *be as explicit has you can* +- *describe the problem and its symptoms* +- *explain how to reproduce* +- *attach whatever information that can help understanding the context (screen capture, log files)* +- *do your best to use a correct english (re-read yourself)* diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..c59bec8 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,8 @@ +### Before submitting a pull-request to Gitbucket I have first: + +- [] read the [contribution guidelines](https://github.com/gitbucket/gitbucket/blob/master/.github/CONTRIBUTING.md) +- [] rebased my branch over master +- [] verified that project is compiling +- [] verified that tests are passing +- [] squashed my commits as appropriate *(keep several commits if it is relevant to understand the PR)* +- [] [marked as closed](https://help.github.com/articles/closing-issues-via-commit-messages/) all issue ID that this PR should correct diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index d7234da..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,7 +0,0 @@ -# Guideline for Issues - -- If you have any question about GitBucket, send it to [gitter room](https://gitter.im/gitbucket/gitbucket) before raise an issue. -- Make sure check whether there is a same question or request in the past. -- When raise a new issue, write subject in **English** at least. -- We can also support in Japaneses other than English at [gitter room for Japanese](https://gitter.im/gitbucket/gitbucket_ja). -- First priority of GitBucket is easy installation and reproduce GitHub behavior, so we might reject if your request is against it. diff --git a/README.md b/README.md index 42150b4..fc3338b 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,22 @@ Release Notes -------- +### 3.12 - 27 Feb 2016 +- New GitHub UI +- Improve mobile view +- Improve printing style +- Individual URL for pull request tabs +- SSH host configuration is separated from HTTP base URL + +### 3.11 - 30 Jan 2016 +- Upgrade Scalatra to 2.4 +- Sidebar and Footer for Wiki +- Branch protection and receive hook extension point for plug-in +- Limit recent updated repositories list +- Issue actions look-alike GitHub +- Web API for labels +- Requires Java 8 + ### 3.10 - 30 Dec 2015 - Move to Bootstrap3 - New URL for raw contents (`raw/master/doc/activity.md` instead of `blob/master/doc/activity.md?raw=true`) diff --git a/build.sbt b/build.sbt index 898e785..60da603 100644 --- a/build.sbt +++ b/build.sbt @@ -1,6 +1,6 @@ val Organization = "gitbucket" val Name = "gitbucket" -val GitBucketVersion = "3.11.0-SNAPSHOT" +val GitBucketVersion = "3.12.0" val ScalatraVersion = "2.4.0" val JettyVersion = "9.3.6.v20151106" @@ -10,13 +10,12 @@ organization := Organization name := Name version := GitBucketVersion -scalaVersion := "2.11.6" +scalaVersion := "2.11.7" // dependency settings resolvers ++= Seq( Classpaths.typesafeReleases, - "amateras-repo" at "http://amateras.sourceforge.jp/mvn/", - "amateras-snapshot-repo" at "http://amateras.sourceforge.jp/mvn-snapshot/" + "sonatype-snapshot" at "https://oss.sonatype.org/content/repositories/snapshots/" ) libraryDependencies ++= Seq( "org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "4.1.1.201511131810-r", @@ -26,8 +25,8 @@ "org.json4s" %% "json4s-jackson" % "3.3.0", "io.github.gitbucket" %% "scalatra-forms" % "1.0.0", "commons-io" % "commons-io" % "2.4", - "io.github.gitbucket" % "markedj" % "1.0.6", "io.github.gitbucket" % "solidbase" % "1.0.0-SNAPSHOT", + "io.github.gitbucket" % "markedj" % "1.0.7-SNAPSHOT", "org.apache.commons" % "commons-compress" % "1.10", "org.apache.commons" % "commons-email" % "1.4", "org.apache.httpcomponents" % "httpclient" % "4.5.1", @@ -40,12 +39,13 @@ "com.mchange" % "c3p0" % "0.9.5.2", "com.typesafe" % "config" % "1.3.0", "com.typesafe.akka" %% "akka-actor" % "2.3.14", + "fr.brouillard.oss.security.xhub" % "xhub4j-core" % "1.0.0", "com.enragedginger" %% "akka-quartz-scheduler" % "1.4.0-akka-2.3.x" exclude("c3p0","c3p0"), "org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "provided", "javax.servlet" % "javax.servlet-api" % "3.1.0" % "provided", "junit" % "junit" % "4.12" % "test", - "org.scalatra" %% "scalatra-specs2" % ScalatraVersion % "test", - "org.specs2" %% "specs2-junit" % "3.6.6" % "test" + "org.scalatra" %% "scalatra-scalatest" % ScalatraVersion % "test", + "org.scalaz" %% "scalaz-core" % "7.2.0" % "test" ) // Twirl settings @@ -53,7 +53,7 @@ // Compiler settings scalacOptions := Seq("-deprecation", "-language:postfixOps") -javacOptions in compile ++= Seq("-target", "7", "-source", "7") +javacOptions in compile ++= Seq("-target", "8", "-source", "8") javaOptions in Jetty += "-Dlogback.configurationFile=/logback-dev.xml" testOptions in Test += Tests.Argument(TestFrameworks.Specs2, "junitxml", "console") javaOptions in Test += "-Dgitbucket.home=target/gitbucket_home_for_test" @@ -83,36 +83,36 @@ // Create executable war file val executableConfig = config("executable").hide Keys.ivyConfigurations += executableConfig -libraryDependencies ++= Seq( - "org.eclipse.jetty" % "jetty-security" % JettyVersion % "executable", - "org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "executable", - "org.eclipse.jetty" % "jetty-continuation" % JettyVersion % "executable", - "org.eclipse.jetty" % "jetty-server" % JettyVersion % "executable", - "org.eclipse.jetty" % "jetty-xml" % JettyVersion % "executable", - "org.eclipse.jetty" % "jetty-http" % JettyVersion % "executable", - "org.eclipse.jetty" % "jetty-servlet" % JettyVersion % "executable", - "org.eclipse.jetty" % "jetty-io" % JettyVersion % "executable", - "org.eclipse.jetty" % "jetty-util" % JettyVersion % "executable" +libraryDependencies ++= Seq( + "org.eclipse.jetty" % "jetty-security" % JettyVersion % "executable", + "org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "executable", + "org.eclipse.jetty" % "jetty-continuation" % JettyVersion % "executable", + "org.eclipse.jetty" % "jetty-server" % JettyVersion % "executable", + "org.eclipse.jetty" % "jetty-xml" % JettyVersion % "executable", + "org.eclipse.jetty" % "jetty-http" % JettyVersion % "executable", + "org.eclipse.jetty" % "jetty-servlet" % JettyVersion % "executable", + "org.eclipse.jetty" % "jetty-io" % JettyVersion % "executable", + "org.eclipse.jetty" % "jetty-util" % JettyVersion % "executable" ) -val executableKey = TaskKey[File]("executable") -executableKey := { +val executableKey = TaskKey[File]("executable") +executableKey := { import org.apache.ivy.util.ChecksumHelper import java.util.jar.{ Manifest => JarManifest } import java.util.jar.Attributes.{ Name => AttrName } - val workDir = Keys.target.value / "executable" - val warName = Keys.name.value + ".war" + val workDir = Keys.target.value / "executable" + val warName = Keys.name.value + ".war" - val log = streams.value.log + val log = streams.value.log log info s"building executable webapp in ${workDir}" // initialize temp directory - val temp = workDir / "webapp" + val temp = workDir / "webapp" IO delete temp // include jetty classes - val jettyJars = Keys.update.value select configurationFilter(name = executableConfig.name) + val jettyJars = Keys.update.value select configurationFilter(name = executableConfig.name) jettyJars foreach { jar => IO unzip (jar, temp, (name:String) => (name startsWith "javax/") || @@ -121,31 +121,34 @@ } // include original war file - val warFile = (Keys.`package`).value + val warFile = (Keys.`package`).value IO unzip (warFile, temp) // include launcher classes - val classDir = (Keys.classDirectory in Compile).value - val launchClasses = Seq("JettyLauncher.class" /*, "HttpsSupportConnector.class" */) + val classDir = (Keys.classDirectory in Compile).value + val launchClasses = Seq("JettyLauncher.class" /*, "HttpsSupportConnector.class" */) launchClasses foreach { name => IO copyFile (classDir / name, temp / name) } // zip it up IO delete (temp / "META-INF" / "MANIFEST.MF") - val contentMappings = (temp.*** --- PathFinder(temp)).get pair relativeTo(temp) - val manifest = new JarManifest - manifest.getMainAttributes put (AttrName.MANIFEST_VERSION, "1.0") - manifest.getMainAttributes put (AttrName.MAIN_CLASS, "JettyLauncher") - val outputFile = workDir / warName + val contentMappings = (temp.*** --- PathFinder(temp)).get pair relativeTo(temp) + val manifest = new JarManifest + manifest.getMainAttributes put (AttrName.MANIFEST_VERSION, "1.0") + manifest.getMainAttributes put (AttrName.MAIN_CLASS, "JettyLauncher") + val outputFile = workDir / warName IO jar (contentMappings, outputFile, manifest) // generate checksums - Seq("md5", "sha1") foreach { algorithm => - IO.write( - workDir / (warName + "." + algorithm), - ChecksumHelper computeAsString (outputFile, algorithm) - ) + Seq( + "md5" -> "MD5", + "sha1" -> "SHA-1", + "sha256" -> "SHA-256" + ) + .foreach { case (extension, algorithm) => + val checksumFile = workDir / (warName + "." + extension) + Checksums generate (outputFile, checksumFile, algorithm) } // done @@ -154,7 +157,7 @@ } /* Keys.artifact in (Compile, executableKey) ~= { - _ copy (`type` = "war", extension = "war")) + _ copy (`type` = "war", extension = "war")) } addArtifact(Keys.artifact in (Compile, executableKey), executableKey) */ diff --git a/doc/how_to_run.md b/doc/how_to_run.md index 0e18a8f..ea31909 100644 --- a/doc/how_to_run.md +++ b/doc/how_to_run.md @@ -27,7 +27,16 @@ To build executable war file, run -* Windows: Not available -* Linux: `./release/make-release-war.sh` +``` +$ sbt executable +``` -at the top of the source tree. It generates executable `gitbucket.war` into `target/scala-2.11`. We release this war file as release artifact. +at the top of the source tree. It generates executable `gitbucket.war` into `target/executable`. We release this war file as release artifact. + +Run tests spec +--------- +To run the full serie of tests, run the following command: + +``` +sbt test +``` diff --git a/doc/release.md b/doc/release.md index da0c003..98ba383 100644 --- a/doc/release.md +++ b/doc/release.md @@ -6,15 +6,14 @@ Note to update version number in files below: -### project/build.scala +### build.sbt ```scala -object MyBuild extends Build { - val Organization = "gitbucket" - val Name = "gitbucket" - val Version = "3.3.0" // <---- update version!! - val ScalaVersion = "2.11.6" - val ScalatraVersion = "2.3.1" +val Organization = "gitbucket" +val Name = "gitbucket" +val GitBucketVersion = "3.12.0" // <---- update version!! +val ScalatraVersion = "2.4.0" +val JettyVersion = "9.3.6.v20151106" ``` ### src/main/scala/gitbucket/core/servlet/AutoUpdate.scala @@ -26,8 +25,8 @@ * The history of versions. A head of this sequence is the current GitBucket version. */ val versions = Seq( - new Version(3, 3), // <---- add this line!! - new Version(3, 2), + new Version(3, 12), // <---- add this line!! + new Version(3, 11), ``` Generate release files diff --git a/project/Checksums.scala b/project/Checksums.scala new file mode 100644 index 0000000..dc9d849 --- /dev/null +++ b/project/Checksums.scala @@ -0,0 +1,34 @@ +import java.security.MessageDigest; +import scala.annotation._ +import sbt._ +import sbt.Using._ + +object Checksums { + private val bufferSize = 2048 + + def generate(source:File, target:File, algorithm:String):Unit = + IO write (target, compute(source, algorithm)) + + def compute(file:File, algorithm:String):String = + hex(raw(file, algorithm)) + + def raw(file:File, algorithm:String):Array[Byte] = + (Using fileInputStream file) { is => + val md = MessageDigest getInstance algorithm + val buf = new Array[Byte](bufferSize) + md.reset() + @tailrec + def loop() { + val len = is read buf + if (len != -1) { + md update (buf, 0, len) + loop() + } + } + loop() + md.digest() + } + + def hex(bytes:Array[Byte]):String = + bytes map { it => "%02x" format (it.toInt & 0xff) } mkString "" +} diff --git a/project/build.properties b/project/build.properties index a6e117b..817bc38 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=0.13.8 +sbt.version=0.13.9 diff --git a/release/env.sh b/release/env.sh index 348db43..5892171 100644 --- a/release/env.sh +++ b/release/env.sh @@ -1,3 +1,3 @@ #!/bin/sh -export GITBUCKET_VERSION=`cat ../project/build.scala | grep 'val Version' | cut -d \" -f 2` +export GITBUCKET_VERSION=`cat ../build.sbt | grep 'val GitBucketVersion' | cut -d \" -f 2` echo "GITBUCKET_VERSION: $GITBUCKET_VERSION" diff --git a/sbt-launch-0.13.8.jar b/sbt-launch-0.13.8.jar deleted file mode 100644 index 0d9dd94..0000000 --- a/sbt-launch-0.13.8.jar +++ /dev/null Binary files differ diff --git a/sbt-launch-0.13.9.jar b/sbt-launch-0.13.9.jar new file mode 100644 index 0000000..c065b47 --- /dev/null +++ b/sbt-launch-0.13.9.jar Binary files differ diff --git a/sbt.bat b/sbt.bat index 3b0c31e..2c05152 100644 --- a/sbt.bat +++ b/sbt.bat @@ -1,2 +1,2 @@ set SCRIPT_DIR=%~dp0 -java %JAVA_OPTS% -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar "%SCRIPT_DIR%\sbt-launch-0.13.8.jar" %* +java %JAVA_OPTS% -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar "%SCRIPT_DIR%\sbt-launch-0.13.9.jar" %* diff --git a/sbt.sh b/sbt.sh index a9247ab..ea58042 100755 --- a/sbt.sh +++ b/sbt.sh @@ -1,2 +1,2 @@ #!/bin/sh -java $JAVA_OPTS -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar `dirname $0`/sbt-launch-0.13.8.jar "$@" +java $JAVA_OPTS -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar `dirname $0`/sbt-launch-0.13.9.jar "$@" diff --git a/src/main/resources/update/3_13.sql b/src/main/resources/update/3_13.sql new file mode 100644 index 0000000..8efe1a3 --- /dev/null +++ b/src/main/resources/update/3_13.sql @@ -0,0 +1 @@ +ALTER TABLE WEB_HOOK ADD COLUMN TOKEN VARCHAR(100); \ No newline at end of file diff --git a/src/main/scala/ScalatraBootstrap.scala b/src/main/scala/ScalatraBootstrap.scala index 5947977..b266261 100644 --- a/src/main/scala/ScalatraBootstrap.scala +++ b/src/main/scala/ScalatraBootstrap.scala @@ -27,12 +27,9 @@ } context.mount(new IndexController, "/") - context.mount(new SearchController, "/") context.mount(new FileUploadController, "/upload") context.mount(new DashboardController, "/*") - context.mount(new UserManagementController, "/*") context.mount(new SystemSettingsController, "/*") - context.mount(new PluginsController, "/*") context.mount(new AccountController, "/*") context.mount(new RepositoryViewerController, "/*") context.mount(new WikiController, "/*") diff --git a/src/main/scala/gitbucket/core/controller/AccountController.scala b/src/main/scala/gitbucket/core/controller/AccountController.scala index 65212e3..54bdc58 100644 --- a/src/main/scala/gitbucket/core/controller/AccountController.scala +++ b/src/main/scala/gitbucket/core/controller/AccountController.scala @@ -133,7 +133,7 @@ val members = getGroupMembers(account.userName) gitbucket.core.account.html.repositories(account, if(account.isGroupAccount) Nil else getGroupsByUserName(userName), - getVisibleRepositories(context.loginAccount, context.baseUrl, Some(userName)), + getVisibleRepositories(context.loginAccount, Some(userName)), context.loginAccount.exists(x => members.exists { member => member.userName == x.userName && member.isManager })) } } @@ -366,7 +366,7 @@ */ post("/new", newRepositoryForm)(usersOnly { form => LockUtil.lock(s"${form.owner}/${form.name}"){ - if(getRepository(form.owner, form.name, context.baseUrl).isEmpty){ + if(getRepository(form.owner, form.name).isEmpty){ createRepository(form.owner, form.name, form.description, form.isPrivate, form.createReadme) } @@ -385,9 +385,9 @@ data <- extractFromJsonBody[CreateARepository] if data.isValid } yield { LockUtil.lock(s"${owner}/${data.name}") { - if(getRepository(owner, data.name, context.baseUrl).isEmpty){ + if(getRepository(owner, data.name).isEmpty){ createRepository(owner, data.name, data.description, data.`private`, data.auto_init) - val repository = getRepository(owner, data.name, context.baseUrl).get + val repository = getRepository(owner, data.name).get JsonFormat(ApiRepository(repository, ApiUser(getAccountByUserName(owner).get))) } else { ApiError( @@ -409,9 +409,9 @@ data <- extractFromJsonBody[CreateARepository] if data.isValid } yield { LockUtil.lock(s"${groupName}/${data.name}") { - if(getRepository(groupName, data.name, context.baseUrl).isEmpty){ + if(getRepository(groupName, data.name).isEmpty){ createRepository(groupName, data.name, data.description, data.`private`, data.auto_init) - val repository = getRepository(groupName, data.name, context.baseUrl).get + val repository = getRepository(groupName, data.name).get JsonFormat(ApiRepository(repository, ApiUser(getAccountByUserName(groupName).get))) } else { ApiError( @@ -447,7 +447,7 @@ val accountName = form.accountName LockUtil.lock(s"${accountName}/${repository.name}"){ - if(getRepository(accountName, repository.name, baseUrl).isDefined || + if(getRepository(accountName, repository.name).isDefined || (accountName != loginUserName && !getGroupsByUserName(loginUserName).contains(accountName))){ // redirect to the repository if repository already exists redirect(s"/${accountName}/${repository.name}") diff --git a/src/main/scala/gitbucket/core/controller/ControllerBase.scala b/src/main/scala/gitbucket/core/controller/ControllerBase.scala index 5d449ba..a336509 100644 --- a/src/main/scala/gitbucket/core/controller/ControllerBase.scala +++ b/src/main/scala/gitbucket/core/controller/ControllerBase.scala @@ -180,7 +180,6 @@ * Context object for the current request. */ case class Context(settings: SystemSettingsService.SystemSettings, loginAccount: Option[Account], request: HttpServletRequest){ - val path = settings.baseUrl.getOrElse(request.getContextPath) val currentPath = request.getRequestURI.substring(request.getContextPath.length) val baseUrl = settings.baseUrl(request) diff --git a/src/main/scala/gitbucket/core/controller/DashboardController.scala b/src/main/scala/gitbucket/core/controller/DashboardController.scala index 713e9c0..1516c0f 100644 --- a/src/main/scala/gitbucket/core/controller/DashboardController.scala +++ b/src/main/scala/gitbucket/core/controller/DashboardController.scala @@ -94,7 +94,7 @@ val userName = context.loginAccount.get.userName val condition = getOrCreateCondition(Keys.Session.DashboardIssues, filter, userName) - val userRepos = getUserRepositories(userName, context.baseUrl, true).map(repo => repo.owner -> repo.name) + val userRepos = getUserRepositories(userName, true).map(repo => repo.owner -> repo.name) val page = IssueSearchCondition.page(request) html.issues( diff --git a/src/main/scala/gitbucket/core/controller/IndexController.scala b/src/main/scala/gitbucket/core/controller/IndexController.scala index f264131..5c83cbb 100644 --- a/src/main/scala/gitbucket/core/controller/IndexController.scala +++ b/src/main/scala/gitbucket/core/controller/IndexController.scala @@ -2,35 +2,46 @@ import gitbucket.core.api._ import gitbucket.core.helper.xml -import gitbucket.core.html import gitbucket.core.model.Account -import gitbucket.core.service.{RepositoryService, ActivityService, AccountService} +import gitbucket.core.service.{RepositoryService, ActivityService, AccountService, RepositorySearchService, IssuesService} import gitbucket.core.util.Implicits._ -import gitbucket.core.util.{LDAPUtil, Keys, UsersAuthenticator} +import gitbucket.core.util.ControlUtil._ +import gitbucket.core.util.{LDAPUtil, Keys, UsersAuthenticator, ReferrerAuthenticator, StringUtil} import io.github.gitbucket.scalatra.forms._ class IndexController extends IndexControllerBase - with RepositoryService with ActivityService with AccountService with UsersAuthenticator + with RepositoryService with ActivityService with AccountService with RepositorySearchService with IssuesService + with UsersAuthenticator with ReferrerAuthenticator trait IndexControllerBase extends ControllerBase { - self: RepositoryService with ActivityService with AccountService with UsersAuthenticator => + self: RepositoryService with ActivityService with AccountService with RepositorySearchService + with UsersAuthenticator with ReferrerAuthenticator => case class SignInForm(userName: String, password: String) - val form = mapping( + val signinForm = mapping( "userName" -> trim(label("Username", text(required))), "password" -> trim(label("Password", text(required))) )(SignInForm.apply) + val searchForm = mapping( + "query" -> trim(text(required)), + "owner" -> trim(text(required)), + "repository" -> trim(text(required)) + )(SearchForm.apply) + + case class SearchForm(query: String, owner: String, repository: String) + + get("/"){ val loginAccount = context.loginAccount if(loginAccount.isEmpty) { - html.index(getRecentActivities(), - getVisibleRepositories(loginAccount, context.baseUrl, withoutPhysicalInfo = true), - loginAccount.map{ account => getUserRepositories(account.userName, context.baseUrl, withoutPhysicalInfo = true) }.getOrElse(Nil) + gitbucket.core.html.index(getRecentActivities(), + getVisibleRepositories(loginAccount, withoutPhysicalInfo = true), + loginAccount.map{ account => getUserRepositories(account.userName, withoutPhysicalInfo = true) }.getOrElse(Nil) ) } else { val loginUserName = loginAccount.get.userName @@ -39,9 +50,9 @@ visibleOwnerSet ++= loginUserGroups - html.index(getRecentActivitiesByOwners(visibleOwnerSet), - getVisibleRepositories(loginAccount, context.baseUrl, withoutPhysicalInfo = true), - loginAccount.map{ account => getUserRepositories(account.userName, context.baseUrl, withoutPhysicalInfo = true) }.getOrElse(Nil) + gitbucket.core.html.index(getRecentActivitiesByOwners(visibleOwnerSet), + getVisibleRepositories(loginAccount, withoutPhysicalInfo = true), + loginAccount.map{ account => getUserRepositories(account.userName, withoutPhysicalInfo = true) }.getOrElse(Nil) ) } } @@ -51,10 +62,10 @@ if(redirect.isDefined && redirect.get.startsWith("/")){ flash += Keys.Flash.Redirect -> redirect.get } - html.signin() + gitbucket.core.html.signin() } - post("/signin", form){ form => + post("/signin", signinForm){ form => authenticate(context.settings, form.userName, form.password) match { case Some(account) => signin(account) case None => redirect("/signin") @@ -119,4 +130,33 @@ // this message is same as github enterprise... org.scalatra.NotFound(ApiError("Rate limiting is not enabled.")) } + + // TODO Move to RepositoryViwerController? + post("/search", searchForm){ form => + redirect(s"/${form.owner}/${form.repository}/search?q=${StringUtil.urlEncode(form.query)}") + } + + // TODO Move to RepositoryViwerController? + get("/:owner/:repository/search")(referrersOnly { repository => + defining(params("q").trim, params.getOrElse("type", "code")){ case (query, target) => + val page = try { + val i = params.getOrElse("page", "1").toInt + if(i <= 0) 1 else i + } catch { + case e: NumberFormatException => 1 + } + + target.toLowerCase match { + case "issue" => gitbucket.core.search.html.issues( + searchIssues(repository.owner, repository.name, query), + countFiles(repository.owner, repository.name, query), + query, page, repository) + + case _ => gitbucket.core.search.html.code( + searchFiles(repository.owner, repository.name, query), + countIssues(repository.owner, repository.name, query), + query, page, repository) + } + } + }) } diff --git a/src/main/scala/gitbucket/core/controller/PluginsController.scala b/src/main/scala/gitbucket/core/controller/PluginsController.scala deleted file mode 100644 index 942e169..0000000 --- a/src/main/scala/gitbucket/core/controller/PluginsController.scala +++ /dev/null @@ -1,11 +0,0 @@ -package gitbucket.core.controller - -import gitbucket.core.admin.plugins.html -import gitbucket.core.plugin.PluginRegistry -import gitbucket.core.util.AdminAuthenticator - -class PluginsController extends ControllerBase with AdminAuthenticator { - get("/admin/plugins")(adminOnly { - html.plugins(PluginRegistry().getPlugins()) - }) -} diff --git a/src/main/scala/gitbucket/core/controller/PullRequestsController.scala b/src/main/scala/gitbucket/core/controller/PullRequestsController.scala index 7a95c42..11141ff 100644 --- a/src/main/scala/gitbucket/core/controller/PullRequestsController.scala +++ b/src/main/scala/gitbucket/core/controller/PullRequestsController.scala @@ -137,7 +137,7 @@ baseOwner <- users.get(repository.owner) headOwner <- users.get(pullRequest.requestUserName) issueUser <- users.get(issue.openedUserName) - headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName, baseUrl) + headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName) } yield { JsonFormat(ApiPullRequest( issue, @@ -196,7 +196,7 @@ issue, pullreq, repository, - getRepository(pullreq.requestUserName, pullreq.requestRepositoryName, context.baseUrl).get) + getRepository(pullreq.requestUserName, pullreq.requestRepositoryName).get) } } getOrElse NotFound }) @@ -229,7 +229,7 @@ if(branchProtection.needStatusCheck(loginAccount.userName)){ flash += "error" -> s"branch ${pullreq.requestBranch} is protected need status check." } else { - val repository = getRepository(owner, name, context.baseUrl).get + val repository = getRepository(owner, name).get LockUtil.lock(s"${owner}/${name}"){ val alias = if(pullreq.repositoryName == pullreq.requestRepositoryName && pullreq.userName == pullreq.requestUserName){ pullreq.branch @@ -310,7 +310,7 @@ pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.commitIdTo) // close issue by content of pull request - val defaultBranch = getRepository(owner, name, context.baseUrl).get.repository.defaultBranch + val defaultBranch = getRepository(owner, name).get.repository.defaultBranch if(pullreq.branch == defaultBranch){ commits.flatten.foreach { commit => closeIssuesFromMessage(commit.fullMessage, loginAccount.userName, owner, name) @@ -343,7 +343,7 @@ val headBranch:Option[String] = params.get("head") (forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match { case (Some(originUserName), Some(originRepositoryName)) => { - getRepository(originUserName, originRepositoryName, context.baseUrl).map { originRepository => + getRepository(originUserName, originRepositoryName).map { originRepository => using( Git.open(getRepositoryDir(originUserName, originRepositoryName)), Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name)) @@ -384,12 +384,12 @@ forkedRepository.repository.originRepositoryName } else { // Sibling repository - getUserRepositories(originOwner, context.baseUrl).find { x => + getUserRepositories(originOwner).find { x => x.repository.originUserName == forkedRepository.repository.originUserName && x.repository.originRepositoryName == forkedRepository.repository.originRepositoryName }.map(_.repository.repositoryName) }; - originRepository <- getRepository(originOwner, originRepositoryName, context.baseUrl) + originRepository <- getRepository(originOwner, originRepositoryName) ) yield { using( Git.open(getRepositoryDir(originRepository.owner, originRepository.name)), @@ -457,7 +457,7 @@ getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_._1 == originOwner).map(_._2) } }; - originRepository <- getRepository(originOwner, originRepositoryName, context.baseUrl) + originRepository <- getRepository(originOwner, originRepositoryName) ) yield { using( Git.open(getRepositoryDir(originRepository.owner, originRepository.name)), diff --git a/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala b/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala index 2c65df6..7867334 100644 --- a/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala +++ b/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala @@ -49,11 +49,12 @@ )(CollaboratorForm.apply) // for web hook url addition - case class WebHookForm(url: String, events: Set[WebHook.Event]) + case class WebHookForm(url: String, events: Set[WebHook.Event], token: Option[String]) def webHookForm(update:Boolean) = mapping( "url" -> trim(label("url", text(required, webHook(update)))), - "events" -> webhookEvents + "events" -> webhookEvents, + "token" -> optional(trim(label("token", text(maxlength(100))))) )(WebHookForm.apply) // for transfer ownership @@ -198,7 +199,7 @@ * Display the web hook edit page. */ get("/:owner/:repository/settings/hooks/new")(ownerOnly { repository => - val webhook = WebHook(repository.owner, repository.name, "") + val webhook = WebHook(repository.owner, repository.name, "", None) html.edithooks(webhook, Set(WebHook.Push), repository, flash.get("info"), true) }) @@ -206,7 +207,7 @@ * Add the web hook URL. */ post("/:owner/:repository/settings/hooks/new", webHookForm(false))(ownerOnly { (form, repository) => - addWebHook(repository.owner, repository.name, form.url, form.events) + addWebHook(repository.owner, repository.name, form.url, form.events, form.token) flash += "info" -> s"Webhook ${form.url} created" redirect(s"/${repository.owner}/${repository.name}/settings/hooks") }) @@ -235,7 +236,8 @@ import scala.concurrent.ExecutionContext.Implicits.global val url = params("url") - val dummyWebHookInfo = WebHook(repository.owner, repository.name, url) + val token = Some(params("token")) + val dummyWebHookInfo = WebHook(repository.owner, repository.name, url, token) val dummyPayload = { val ownerAccount = getAccountByUserName(repository.owner).get val commits = if(repository.commitCount == 0) List.empty else git.log @@ -294,7 +296,7 @@ * Update web hook settings. */ post("/:owner/:repository/settings/hooks/edit", webHookForm(true))(ownerOnly { (form, repository) => - updateWebHook(repository.owner, repository.name, form.url, form.events) + updateWebHook(repository.owner, repository.name, form.url, form.events, form.token) flash += "info" -> s"webhook ${form.url} updated" redirect(s"/${repository.owner}/${repository.name}/settings/hooks") }) diff --git a/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala b/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala index 4abf09c..1007cf9 100644 --- a/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala +++ b/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala @@ -560,8 +560,7 @@ html.forked( getRepository( repository.repository.originUserName.getOrElse(repository.owner), - repository.repository.originRepositoryName.getOrElse(repository.name), - context.baseUrl), + repository.repository.originRepositoryName.getOrElse(repository.name)), getForkedRepositories( repository.repository.originUserName.getOrElse(repository.owner), repository.repository.originRepositoryName.getOrElse(repository.name)), @@ -759,8 +758,6 @@ .setTree(revCommit.getTree) .setOutputStream(response.getOutputStream) .call() - - Unit } } diff --git a/src/main/scala/gitbucket/core/controller/SearchController.scala b/src/main/scala/gitbucket/core/controller/SearchController.scala deleted file mode 100644 index 42b266b..0000000 --- a/src/main/scala/gitbucket/core/controller/SearchController.scala +++ /dev/null @@ -1,51 +0,0 @@ -package gitbucket.core.controller - -import gitbucket.core.search.html -import gitbucket.core.service._ -import gitbucket.core.util.{StringUtil, ControlUtil, ReferrerAuthenticator, Implicits} -import ControlUtil._ -import Implicits._ -import io.github.gitbucket.scalatra.forms._ - -class SearchController extends SearchControllerBase - with RepositoryService with AccountService with ActivityService with RepositorySearchService with IssuesService with ReferrerAuthenticator - -trait SearchControllerBase extends ControllerBase { self: RepositoryService - with ActivityService with RepositorySearchService with ReferrerAuthenticator => - - val searchForm = mapping( - "query" -> trim(text(required)), - "owner" -> trim(text(required)), - "repository" -> trim(text(required)) - )(SearchForm.apply) - - case class SearchForm(query: String, owner: String, repository: String) - - post("/search", searchForm){ form => - redirect(s"/${form.owner}/${form.repository}/search?q=${StringUtil.urlEncode(form.query)}") - } - - get("/:owner/:repository/search")(referrersOnly { repository => - defining(params("q").trim, params.getOrElse("type", "code")){ case (query, target) => - val page = try { - val i = params.getOrElse("page", "1").toInt - if(i <= 0) 1 else i - } catch { - case e: NumberFormatException => 1 - } - - target.toLowerCase match { - case "issue" => html.issues( - searchIssues(repository.owner, repository.name, query), - countFiles(repository.owner, repository.name, query), - query, page, repository) - - case _ => html.code( - searchFiles(repository.owner, repository.name, query), - countIssues(repository.owner, repository.name, query), - query, page, repository) - } - } - }) - -} diff --git a/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala b/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala index 2bc6ece..157a584 100644 --- a/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala +++ b/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala @@ -1,17 +1,24 @@ package gitbucket.core.controller import gitbucket.core.admin.html -import gitbucket.core.service.{AccountService, SystemSettingsService} +import gitbucket.core.service.{AccountService, SystemSettingsService, RepositoryService} import gitbucket.core.util.AdminAuthenticator import gitbucket.core.ssh.SshServer +import gitbucket.core.plugin.PluginRegistry import SystemSettingsService._ +import gitbucket.core.util.Implicits._ +import gitbucket.core.util.ControlUtil._ +import gitbucket.core.util.Directory._ +import gitbucket.core.util.StringUtil._ import io.github.gitbucket.scalatra.forms._ +import org.apache.commons.io.FileUtils +import org.scalatra.i18n.Messages class SystemSettingsController extends SystemSettingsControllerBase - with AccountService with AdminAuthenticator + with AccountService with RepositoryService with AdminAuthenticator -trait SystemSettingsControllerBase extends ControllerBase { - self: AccountService with AdminAuthenticator => +trait SystemSettingsControllerBase extends AccountManagementControllerBase { + self: AccountService with RepositoryService with AdminAuthenticator => private val form = mapping( "baseUrl" -> trim(label("Base URL", optional(text()))), @@ -23,6 +30,7 @@ "notification" -> trim(label("Notification", boolean())), "activityLogLimit" -> trim(label("Limit of activity logs", optional(number()))), "ssh" -> trim(label("SSH access", boolean())), + "sshHost" -> trim(label("SSH host", optional(text()))), "sshPort" -> trim(label("SSH port", optional(number()))), "useSMTP" -> trim(label("SMTP", boolean())), "smtp" -> optionalIfNotChecked("useSMTP", mapping( @@ -50,11 +58,77 @@ "keystore" -> trim(label("Keystore", optional(text()))) )(Ldap.apply)) )(SystemSettings.apply).verifying { settings => - if(settings.ssh && settings.baseUrl.isEmpty){ - Seq("baseUrl" -> "Base URL is required if SSH access is enabled.") - } else Nil + Vector( + if(settings.ssh && settings.baseUrl.isEmpty){ + Some("baseUrl" -> "Base URL is required if SSH access is enabled.") + } else None, + if(settings.ssh && settings.sshHost.isEmpty){ + Some("sshHost" -> "SSH host is required if SSH access is enabled.") + } else None + ).flatten } + private val pluginForm = mapping( + "pluginId" -> list(trim(label("", text()))) + )(PluginForm.apply) + + case class PluginForm(pluginIds: List[String]) + + + case class NewUserForm(userName: String, password: String, fullName: String, + mailAddress: String, isAdmin: Boolean, + url: Option[String], fileId: Option[String]) + + case class EditUserForm(userName: String, password: Option[String], fullName: String, + mailAddress: String, isAdmin: Boolean, url: Option[String], + fileId: Option[String], clearImage: Boolean, isRemoved: Boolean) + + case class NewGroupForm(groupName: String, url: Option[String], fileId: Option[String], + members: String) + + case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String], + members: String, clearImage: Boolean, isRemoved: Boolean) + + + val newUserForm = mapping( + "userName" -> trim(label("Username" ,text(required, maxlength(100), identifier, uniqueUserName))), + "password" -> trim(label("Password" ,text(required, maxlength(20)))), + "fullName" -> trim(label("Full Name" ,text(required, maxlength(100)))), + "mailAddress" -> trim(label("Mail Address" ,text(required, maxlength(100), uniqueMailAddress()))), + "isAdmin" -> trim(label("User Type" ,boolean())), + "url" -> trim(label("URL" ,optional(text(maxlength(200))))), + "fileId" -> trim(label("File ID" ,optional(text()))) + )(NewUserForm.apply) + + val editUserForm = mapping( + "userName" -> trim(label("Username" ,text(required, maxlength(100), identifier))), + "password" -> trim(label("Password" ,optional(text(maxlength(20))))), + "fullName" -> trim(label("Full Name" ,text(required, maxlength(100)))), + "mailAddress" -> trim(label("Mail Address" ,text(required, maxlength(100), uniqueMailAddress("userName")))), + "isAdmin" -> trim(label("User Type" ,boolean())), + "url" -> trim(label("URL" ,optional(text(maxlength(200))))), + "fileId" -> trim(label("File ID" ,optional(text()))), + "clearImage" -> trim(label("Clear image" ,boolean())), + "removed" -> trim(label("Disable" ,boolean(disableByNotYourself("userName")))) + )(EditUserForm.apply) + + val newGroupForm = mapping( + "groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName))), + "url" -> trim(label("URL" ,optional(text(maxlength(200))))), + "fileId" -> trim(label("File ID" ,optional(text()))), + "members" -> trim(label("Members" ,text(required, members))) + )(NewGroupForm.apply) + + val editGroupForm = mapping( + "groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier))), + "url" -> trim(label("URL" ,optional(text(maxlength(200))))), + "fileId" -> trim(label("File ID" ,optional(text()))), + "members" -> trim(label("Members" ,text(required, members))), + "clearImage" -> trim(label("Clear image" ,boolean())), + "removed" -> trim(label("Disable" ,boolean())) + )(EditGroupForm.apply) + + get("/admin/system")(adminOnly { html.system(flash.get("info")) }) @@ -62,20 +136,155 @@ post("/admin/system", form)(adminOnly { form => saveSystemSettings(form) - if(form.ssh && SshServer.isActive && context.settings.sshPort != form.sshPort){ + if (form.sshAddress != context.settings.sshAddress) { SshServer.stop() - } - - if(form.ssh && !SshServer.isActive && form.baseUrl.isDefined){ - SshServer.start( - form.sshPort.getOrElse(SystemSettingsService.DefaultSshPort), - form.baseUrl.get) - } else if(!form.ssh && SshServer.isActive){ - SshServer.stop() + for { + sshAddress <- form.sshAddress + baseUrl <- form.baseUrl + } + SshServer.start(sshAddress, baseUrl) } flash += "info" -> "System settings has been updated." redirect("/admin/system") }) + get("/admin/plugins")(adminOnly { + html.plugins(PluginRegistry().getPlugins()) + }) + + + get("/admin/users")(adminOnly { + val includeRemoved = params.get("includeRemoved").map(_.toBoolean).getOrElse(false) + val users = getAllUsers(includeRemoved) + val members = users.collect { case account if(account.isGroupAccount) => + account.userName -> getGroupMembers(account.userName).map(_.userName) + }.toMap + + html.userlist(users, members, includeRemoved) + }) + + get("/admin/users/_newuser")(adminOnly { + html.user(None) + }) + + post("/admin/users/_newuser", newUserForm)(adminOnly { form => + createAccount(form.userName, sha1(form.password), form.fullName, form.mailAddress, form.isAdmin, form.url) + updateImage(form.userName, form.fileId, false) + redirect("/admin/users") + }) + + get("/admin/users/:userName/_edituser")(adminOnly { + val userName = params("userName") + html.user(getAccountByUserName(userName, true)) + }) + + post("/admin/users/:name/_edituser", editUserForm)(adminOnly { form => + val userName = params("userName") + getAccountByUserName(userName, true).map { account => + + if(form.isRemoved){ + // Remove repositories + // getRepositoryNamesOfUser(userName).foreach { repositoryName => + // deleteRepository(userName, repositoryName) + // FileUtils.deleteDirectory(getRepositoryDir(userName, repositoryName)) + // FileUtils.deleteDirectory(getWikiRepositoryDir(userName, repositoryName)) + // FileUtils.deleteDirectory(getTemporaryDir(userName, repositoryName)) + // } + // Remove from GROUP_MEMBER, COLLABORATOR and REPOSITORY + removeUserRelatedData(userName) + } + + updateAccount(account.copy( + password = form.password.map(sha1).getOrElse(account.password), + fullName = form.fullName, + mailAddress = form.mailAddress, + isAdmin = form.isAdmin, + url = form.url, + isRemoved = form.isRemoved)) + + updateImage(userName, form.fileId, form.clearImage) + redirect("/admin/users") + + } getOrElse NotFound + }) + + get("/admin/users/_newgroup")(adminOnly { + html.usergroup(None, Nil) + }) + + post("/admin/users/_newgroup", newGroupForm)(adminOnly { form => + createGroup(form.groupName, form.url) + updateGroupMembers(form.groupName, form.members.split(",").map { + _.split(":") match { + case Array(userName, isManager) => (userName, isManager.toBoolean) + } + }.toList) + updateImage(form.groupName, form.fileId, false) + redirect("/admin/users") + }) + + get("/admin/users/:groupName/_editgroup")(adminOnly { + defining(params("groupName")){ groupName => + html.usergroup(getAccountByUserName(groupName, true), getGroupMembers(groupName)) + } + }) + + post("/admin/users/:groupName/_editgroup", editGroupForm)(adminOnly { form => + defining(params("groupName"), form.members.split(",").map { + _.split(":") match { + case Array(userName, isManager) => (userName, isManager.toBoolean) + } + }.toList){ case (groupName, members) => + getAccountByUserName(groupName, true).map { account => + updateGroup(groupName, form.url, form.isRemoved) + + if(form.isRemoved){ + // Remove from GROUP_MEMBER + updateGroupMembers(form.groupName, Nil) + // Remove repositories + getRepositoryNamesOfUser(form.groupName).foreach { repositoryName => + deleteRepository(groupName, repositoryName) + FileUtils.deleteDirectory(getRepositoryDir(groupName, repositoryName)) + FileUtils.deleteDirectory(getWikiRepositoryDir(groupName, repositoryName)) + FileUtils.deleteDirectory(getTemporaryDir(groupName, repositoryName)) + } + } else { + // Update GROUP_MEMBER + updateGroupMembers(form.groupName, members) + // Update COLLABORATOR for group repositories + getRepositoryNamesOfUser(form.groupName).foreach { repositoryName => + removeCollaborators(form.groupName, repositoryName) + members.foreach { case (userName, isManager) => + addCollaborator(form.groupName, repositoryName, userName) + } + } + } + + updateImage(form.groupName, form.fileId, form.clearImage) + redirect("/admin/users") + + } getOrElse NotFound + } + }) + + private def members: Constraint = new Constraint(){ + override def validate(name: String, value: String, messages: Messages): Option[String] = { + if(value.split(",").exists { + _.split(":") match { case Array(userName, isManager) => isManager.toBoolean } + }) None else Some("Must select one manager at least.") + } + } + + protected def disableByNotYourself(paramName: String): Constraint = new Constraint() { + override def validate(name: String, value: String, messages: Messages): Option[String] = { + params.get(paramName).flatMap { userName => + if(userName == context.loginAccount.get.userName && params.get("removed") == Some("true")) + Some("You can't disable your account yourself") + else + None + } + } + } + } diff --git a/src/main/scala/gitbucket/core/controller/UserManagementController.scala b/src/main/scala/gitbucket/core/controller/UserManagementController.scala deleted file mode 100644 index 417faf9..0000000 --- a/src/main/scala/gitbucket/core/controller/UserManagementController.scala +++ /dev/null @@ -1,204 +0,0 @@ -package gitbucket.core.controller - -import gitbucket.core.service.{RepositoryService, AccountService} -import gitbucket.core.admin.users.html -import gitbucket.core.util._ -import gitbucket.core.util.ControlUtil._ -import gitbucket.core.util.StringUtil._ -import gitbucket.core.util.Implicits._ -import gitbucket.core.util.Directory._ -import io.github.gitbucket.scalatra.forms._ -import org.scalatra.i18n.Messages -import org.apache.commons.io.FileUtils - -class UserManagementController extends UserManagementControllerBase - with AccountService with RepositoryService with AdminAuthenticator - -trait UserManagementControllerBase extends AccountManagementControllerBase { - self: AccountService with RepositoryService with AdminAuthenticator => - - case class NewUserForm(userName: String, password: String, fullName: String, - mailAddress: String, isAdmin: Boolean, - url: Option[String], fileId: Option[String]) - - case class EditUserForm(userName: String, password: Option[String], fullName: String, - mailAddress: String, isAdmin: Boolean, url: Option[String], - fileId: Option[String], clearImage: Boolean, isRemoved: Boolean) - - case class NewGroupForm(groupName: String, url: Option[String], fileId: Option[String], - members: String) - - case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String], - members: String, clearImage: Boolean, isRemoved: Boolean) - - val newUserForm = mapping( - "userName" -> trim(label("Username" ,text(required, maxlength(100), identifier, uniqueUserName))), - "password" -> trim(label("Password" ,text(required, maxlength(20)))), - "fullName" -> trim(label("Full Name" ,text(required, maxlength(100)))), - "mailAddress" -> trim(label("Mail Address" ,text(required, maxlength(100), uniqueMailAddress()))), - "isAdmin" -> trim(label("User Type" ,boolean())), - "url" -> trim(label("URL" ,optional(text(maxlength(200))))), - "fileId" -> trim(label("File ID" ,optional(text()))) - )(NewUserForm.apply) - - val editUserForm = mapping( - "userName" -> trim(label("Username" ,text(required, maxlength(100), identifier))), - "password" -> trim(label("Password" ,optional(text(maxlength(20))))), - "fullName" -> trim(label("Full Name" ,text(required, maxlength(100)))), - "mailAddress" -> trim(label("Mail Address" ,text(required, maxlength(100), uniqueMailAddress("userName")))), - "isAdmin" -> trim(label("User Type" ,boolean())), - "url" -> trim(label("URL" ,optional(text(maxlength(200))))), - "fileId" -> trim(label("File ID" ,optional(text()))), - "clearImage" -> trim(label("Clear image" ,boolean())), - "removed" -> trim(label("Disable" ,boolean(disableByNotYourself("userName")))) - )(EditUserForm.apply) - - val newGroupForm = mapping( - "groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName))), - "url" -> trim(label("URL" ,optional(text(maxlength(200))))), - "fileId" -> trim(label("File ID" ,optional(text()))), - "members" -> trim(label("Members" ,text(required, members))) - )(NewGroupForm.apply) - - val editGroupForm = mapping( - "groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier))), - "url" -> trim(label("URL" ,optional(text(maxlength(200))))), - "fileId" -> trim(label("File ID" ,optional(text()))), - "members" -> trim(label("Members" ,text(required, members))), - "clearImage" -> trim(label("Clear image" ,boolean())), - "removed" -> trim(label("Disable" ,boolean())) - )(EditGroupForm.apply) - - get("/admin/users")(adminOnly { - val includeRemoved = params.get("includeRemoved").map(_.toBoolean).getOrElse(false) - val users = getAllUsers(includeRemoved) - val members = users.collect { case account if(account.isGroupAccount) => - account.userName -> getGroupMembers(account.userName).map(_.userName) - }.toMap - - html.list(users, members, includeRemoved) - }) - - get("/admin/users/_newuser")(adminOnly { - html.user(None) - }) - - post("/admin/users/_newuser", newUserForm)(adminOnly { form => - createAccount(form.userName, sha1(form.password), form.fullName, form.mailAddress, form.isAdmin, form.url) - updateImage(form.userName, form.fileId, false) - redirect("/admin/users") - }) - - get("/admin/users/:userName/_edituser")(adminOnly { - val userName = params("userName") - html.user(getAccountByUserName(userName, true)) - }) - - post("/admin/users/:name/_edituser", editUserForm)(adminOnly { form => - val userName = params("userName") - getAccountByUserName(userName, true).map { account => - - if(form.isRemoved){ - // Remove repositories -// getRepositoryNamesOfUser(userName).foreach { repositoryName => -// deleteRepository(userName, repositoryName) -// FileUtils.deleteDirectory(getRepositoryDir(userName, repositoryName)) -// FileUtils.deleteDirectory(getWikiRepositoryDir(userName, repositoryName)) -// FileUtils.deleteDirectory(getTemporaryDir(userName, repositoryName)) -// } - // Remove from GROUP_MEMBER, COLLABORATOR and REPOSITORY - removeUserRelatedData(userName) - } - - updateAccount(account.copy( - password = form.password.map(sha1).getOrElse(account.password), - fullName = form.fullName, - mailAddress = form.mailAddress, - isAdmin = form.isAdmin, - url = form.url, - isRemoved = form.isRemoved)) - - updateImage(userName, form.fileId, form.clearImage) - redirect("/admin/users") - - } getOrElse NotFound - }) - - get("/admin/users/_newgroup")(adminOnly { - html.group(None, Nil) - }) - - post("/admin/users/_newgroup", newGroupForm)(adminOnly { form => - createGroup(form.groupName, form.url) - updateGroupMembers(form.groupName, form.members.split(",").map { - _.split(":") match { - case Array(userName, isManager) => (userName, isManager.toBoolean) - } - }.toList) - updateImage(form.groupName, form.fileId, false) - redirect("/admin/users") - }) - - get("/admin/users/:groupName/_editgroup")(adminOnly { - defining(params("groupName")){ groupName => - html.group(getAccountByUserName(groupName, true), getGroupMembers(groupName)) - } - }) - - post("/admin/users/:groupName/_editgroup", editGroupForm)(adminOnly { form => - defining(params("groupName"), form.members.split(",").map { - _.split(":") match { - case Array(userName, isManager) => (userName, isManager.toBoolean) - } - }.toList){ case (groupName, members) => - getAccountByUserName(groupName, true).map { account => - updateGroup(groupName, form.url, form.isRemoved) - - if(form.isRemoved){ - // Remove from GROUP_MEMBER - updateGroupMembers(form.groupName, Nil) - // Remove repositories - getRepositoryNamesOfUser(form.groupName).foreach { repositoryName => - deleteRepository(groupName, repositoryName) - FileUtils.deleteDirectory(getRepositoryDir(groupName, repositoryName)) - FileUtils.deleteDirectory(getWikiRepositoryDir(groupName, repositoryName)) - FileUtils.deleteDirectory(getTemporaryDir(groupName, repositoryName)) - } - } else { - // Update GROUP_MEMBER - updateGroupMembers(form.groupName, members) - // Update COLLABORATOR for group repositories - getRepositoryNamesOfUser(form.groupName).foreach { repositoryName => - removeCollaborators(form.groupName, repositoryName) - members.foreach { case (userName, isManager) => - addCollaborator(form.groupName, repositoryName, userName) - } - } - } - - updateImage(form.groupName, form.fileId, form.clearImage) - redirect("/admin/users") - - } getOrElse NotFound - } - }) - - private def members: Constraint = new Constraint(){ - override def validate(name: String, value: String, messages: Messages): Option[String] = { - if(value.split(",").exists { - _.split(":") match { case Array(userName, isManager) => isManager.toBoolean } - }) None else Some("Must select one manager at least.") - } - } - - protected def disableByNotYourself(paramName: String): Constraint = new Constraint() { - override def validate(name: String, value: String, messages: Messages): Option[String] = { - params.get(paramName).flatMap { userName => - if(userName == context.loginAccount.get.userName && params.get("removed") == Some("true")) - Some("You can't disable your account yourself") - else - None - } - } - } -} diff --git a/src/main/scala/gitbucket/core/model/BasicTemplate.scala b/src/main/scala/gitbucket/core/model/BasicTemplate.scala index 97fcb75..53388fc 100644 --- a/src/main/scala/gitbucket/core/model/BasicTemplate.scala +++ b/src/main/scala/gitbucket/core/model/BasicTemplate.scala @@ -35,7 +35,7 @@ byRepository(userName, repositoryName) && (this.labelId === labelId) def byLabel(owner: String, repository: String, labelName: String) = - byRepository(userName, repositoryName) && (this.labelName === labelName.bind) + byRepository(owner, repository) && (this.labelName === labelName.bind) } trait MilestoneTemplate extends BasicTemplate { self: Table[_] => diff --git a/src/main/scala/gitbucket/core/model/WebHook.scala b/src/main/scala/gitbucket/core/model/WebHook.scala index 3889c00..9be55a8 100644 --- a/src/main/scala/gitbucket/core/model/WebHook.scala +++ b/src/main/scala/gitbucket/core/model/WebHook.scala @@ -7,7 +7,8 @@ class WebHooks(tag: Tag) extends Table[WebHook](tag, "WEB_HOOK") with BasicTemplate { val url = column[String]("URL") - def * = (userName, repositoryName, url) <> ((WebHook.apply _).tupled, WebHook.unapply) + val token = column[Option[String]]("TOKEN", O.Nullable) + def * = (userName, repositoryName, url, token) <> ((WebHook.apply _).tupled, WebHook.unapply) def byPrimaryKey(owner: String, repository: String, url: String) = byRepository(owner, repository) && (this.url === url.bind) } @@ -16,7 +17,8 @@ case class WebHook( userName: String, repositoryName: String, - url: String + url: String, + token: Option[String] ) object WebHook { diff --git a/src/main/scala/gitbucket/core/service/AccesTokenService.scala b/src/main/scala/gitbucket/core/service/AccesTokenService.scala deleted file mode 100644 index 0a8109d..0000000 --- a/src/main/scala/gitbucket/core/service/AccesTokenService.scala +++ /dev/null @@ -1,55 +0,0 @@ -package gitbucket.core.service - -import gitbucket.core.model.Profile._ -import profile.simple._ - -import gitbucket.core.model.{Account, AccessToken} -import gitbucket.core.util.StringUtil - -import scala.util.Random - - -trait AccessTokenService { - - def makeAccessTokenString: String = { - val bytes = new Array[Byte](20) - Random.nextBytes(bytes) - bytes.map("%02x".format(_)).mkString - } - - def tokenToHash(token: String): String = StringUtil.sha1(token) - - /** - * @retuen (TokenId, Token) - */ - def generateAccessToken(userName: String, note: String)(implicit s: Session): (Int, String) = { - var token: String = null - var hash: String = null - do{ - token = makeAccessTokenString - hash = tokenToHash(token) - }while(AccessTokens.filter(_.tokenHash === hash.bind).exists.run) - val newToken = AccessToken( - userName = userName, - note = note, - tokenHash = hash) - val tokenId = (AccessTokens returning AccessTokens.map(_.accessTokenId)) += newToken - (tokenId, token) - } - - def getAccountByAccessToken(token: String)(implicit s: Session): Option[Account] = - Accounts - .innerJoin(AccessTokens) - .filter{ case (ac, t) => (ac.userName === t.userName) && (t.tokenHash === tokenToHash(token).bind) && (ac.removed === false.bind) } - .map{ case (ac, t) => ac } - .firstOption - - def getAccessTokens(userName: String)(implicit s: Session): List[AccessToken] = - AccessTokens.filter(_.userName === userName.bind).sortBy(_.accessTokenId.desc).list - - def deleteAccessToken(userName: String, accessTokenId: Int)(implicit s: Session): Unit = - AccessTokens filter (t => t.userName === userName.bind && t.accessTokenId === accessTokenId) delete - -} - -object AccessTokenService extends AccessTokenService diff --git a/src/main/scala/gitbucket/core/service/AccessTokenService.scala b/src/main/scala/gitbucket/core/service/AccessTokenService.scala new file mode 100644 index 0000000..0a8109d --- /dev/null +++ b/src/main/scala/gitbucket/core/service/AccessTokenService.scala @@ -0,0 +1,55 @@ +package gitbucket.core.service + +import gitbucket.core.model.Profile._ +import profile.simple._ + +import gitbucket.core.model.{Account, AccessToken} +import gitbucket.core.util.StringUtil + +import scala.util.Random + + +trait AccessTokenService { + + def makeAccessTokenString: String = { + val bytes = new Array[Byte](20) + Random.nextBytes(bytes) + bytes.map("%02x".format(_)).mkString + } + + def tokenToHash(token: String): String = StringUtil.sha1(token) + + /** + * @retuen (TokenId, Token) + */ + def generateAccessToken(userName: String, note: String)(implicit s: Session): (Int, String) = { + var token: String = null + var hash: String = null + do{ + token = makeAccessTokenString + hash = tokenToHash(token) + }while(AccessTokens.filter(_.tokenHash === hash.bind).exists.run) + val newToken = AccessToken( + userName = userName, + note = note, + tokenHash = hash) + val tokenId = (AccessTokens returning AccessTokens.map(_.accessTokenId)) += newToken + (tokenId, token) + } + + def getAccountByAccessToken(token: String)(implicit s: Session): Option[Account] = + Accounts + .innerJoin(AccessTokens) + .filter{ case (ac, t) => (ac.userName === t.userName) && (t.tokenHash === tokenToHash(token).bind) && (ac.removed === false.bind) } + .map{ case (ac, t) => ac } + .firstOption + + def getAccessTokens(userName: String)(implicit s: Session): List[AccessToken] = + AccessTokens.filter(_.userName === userName.bind).sortBy(_.accessTokenId.desc).list + + def deleteAccessToken(userName: String, accessTokenId: Int)(implicit s: Session): Unit = + AccessTokens filter (t => t.userName === userName.bind && t.accessTokenId === accessTokenId) delete + +} + +object AccessTokenService extends AccessTokenService diff --git a/src/main/scala/gitbucket/core/service/IssuesService.scala b/src/main/scala/gitbucket/core/service/IssuesService.scala index f1a6fde..82c8c58 100644 --- a/src/main/scala/gitbucket/core/service/IssuesService.scala +++ b/src/main/scala/gitbucket/core/service/IssuesService.scala @@ -399,7 +399,7 @@ object IssuesService { import javax.servlet.http.HttpServletRequest - val IssueLimit = 30 + val IssueLimit = 25 case class IssueSearchCondition( labels: Set[String] = Set.empty, diff --git a/src/main/scala/gitbucket/core/service/RepositoryService.scala b/src/main/scala/gitbucket/core/service/RepositoryService.scala index 8e74642..408c9b6 100644 --- a/src/main/scala/gitbucket/core/service/RepositoryService.scala +++ b/src/main/scala/gitbucket/core/service/RepositoryService.scala @@ -1,5 +1,6 @@ package gitbucket.core.service +import gitbucket.core.controller.Context import gitbucket.core.model.{Collaborator, Repository, Account} import gitbucket.core.model.Profile._ import gitbucket.core.util.JGitUtil @@ -194,10 +195,9 @@ * * @param userName the user name of the repository owner * @param repositoryName the repository name - * @param baseUrl the base url of this application * @return the repository information */ - def getRepository(userName: String, repositoryName: String, baseUrl: String)(implicit s: Session): Option[RepositoryInfo] = { + def getRepository(userName: String, repositoryName: String)(implicit s: Session): Option[RepositoryInfo] = { (Repositories filter { t => t.byRepository(userName, repositoryName) } firstOption) map { repository => // for getting issue count and pull request count val issues = Issues.filter { t => @@ -205,7 +205,7 @@ }.map(_.pullRequest).list new RepositoryInfo( - JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl), + JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName), repository, issues.count(_ == false), issues.count(_ == true), @@ -234,7 +234,7 @@ }.list } - def getUserRepositories(userName: String, baseUrl: String, withoutPhysicalInfo: Boolean = false) + def getUserRepositories(userName: String, withoutPhysicalInfo: Boolean = false) (implicit s: Session): List[RepositoryInfo] = { Repositories.filter { t1 => (t1.userName === userName.bind) || @@ -242,9 +242,9 @@ }.sortBy(_.lastActivityDate desc).list.map{ repository => new RepositoryInfo( if(withoutPhysicalInfo){ - new JGitUtil.RepositoryInfo(repository.userName, repository.repositoryName, baseUrl) + new JGitUtil.RepositoryInfo(repository.userName, repository.repositoryName) } else { - JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl) + JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName) }, repository, getForkedCount( @@ -260,13 +260,12 @@ * If repositoryUserName is given then filters results by repository owner. * * @param loginAccount the logged in account - * @param baseUrl the base url of this application * @param repositoryUserName the repository owner (if None then returns all repositories which are visible for logged in user) * @param withoutPhysicalInfo if true then the result does not include physical repository information such as commit count, * branches and tags * @return the repository information which is sorted in descending order of lastActivityDate. */ - def getVisibleRepositories(loginAccount: Option[Account], baseUrl: String, repositoryUserName: Option[String] = None, + def getVisibleRepositories(loginAccount: Option[Account], repositoryUserName: Option[String] = None, withoutPhysicalInfo: Boolean = false) (implicit s: Session): List[RepositoryInfo] = { (loginAccount match { @@ -284,9 +283,9 @@ }.sortBy(_.lastActivityDate desc).list.map{ repository => new RepositoryInfo( if(withoutPhysicalInfo){ - new JGitUtil.RepositoryInfo(repository.userName, repository.repositoryName, baseUrl) + new JGitUtil.RepositoryInfo(repository.userName, repository.repositoryName) } else { - JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl) + JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName) }, repository, getForkedCount( @@ -389,32 +388,39 @@ object RepositoryService { - case class RepositoryInfo(owner: String, name: String, httpUrl: String, repository: Repository, + case class RepositoryInfo(owner: String, name: String, repository: Repository, issueCount: Int, pullCount: Int, commitCount: Int, forkedCount: Int, - branchList: Seq[String], tags: Seq[JGitUtil.TagInfo], managers: Seq[String]){ - - lazy val host = """^https?://(.+?)(:\d+)?/""".r.findFirstMatchIn(httpUrl).get.group(1) - - def sshUrl(port: Int, userName: String) = s"ssh://${userName}@${host}:${port}/${owner}/${name}.git" - - def sshOpenRepoUrl(platform: String, port: Int, userName: String) = openRepoUrl(platform, sshUrl(port, userName)) - - def httpOpenRepoUrl(platform: String) = openRepoUrl(platform, httpUrl) - - def openRepoUrl(platform: String, openUrl: String) = s"github-${platform}://openRepo/${openUrl}" + branchList: Seq[String], tags: Seq[JGitUtil.TagInfo], managers: Seq[String]) { /** * Creates instance with issue count and pull request count. */ def this(repo: JGitUtil.RepositoryInfo, model: Repository, issueCount: Int, pullCount: Int, forkedCount: Int, managers: Seq[String]) = - this(repo.owner, repo.name, repo.url, model, issueCount, pullCount, repo.commitCount, forkedCount, repo.branchList, repo.tags, managers) + this( + repo.owner, repo.name, model, + issueCount, pullCount, repo.commitCount, forkedCount, + repo.branchList, repo.tags, managers) /** * Creates instance without issue count and pull request count. */ - def this(repo: JGitUtil.RepositoryInfo, model: Repository, forkedCount: Int, managers: Seq[String]) = - this(repo.owner, repo.name, repo.url, model, 0, 0, repo.commitCount, forkedCount, repo.branchList, repo.tags, managers) + def this(repo: JGitUtil.RepositoryInfo, model: Repository, forkedCount: Int, managers: Seq[String]) = + this( + repo.owner, repo.name, model, + 0, 0, repo.commitCount, forkedCount, + repo.branchList, repo.tags, managers) + + def httpUrl(implicit context: Context): String = RepositoryService.httpUrl(owner, name) + def sshUrl(implicit context: Context): Option[String] = RepositoryService.sshUrl(owner, name) } - case class RepositoryTreeNode(owner: String, name: String, children: List[RepositoryTreeNode]) + def httpUrl(owner: String, name: String)(implicit context: Context): String = s"${context.baseUrl}/git/${owner}/${name}.git" + def sshUrl(owner: String, name: String)(implicit context: Context): Option[String] = + if(context.settings.ssh){ + context.loginAccount.flatMap { loginAccount => + context.settings.sshAddress.map { x => s"ssh://${loginAccount.userName}@${x.host}:${x.port}/${owner}/${name}.git" } + } + } else None + def openRepoUrl(openUrl: String)(implicit context: Context): String = s"github-${context.platform}://openRepo/${openUrl}" + } diff --git a/src/main/scala/gitbucket/core/service/SystemSettingsService.scala b/src/main/scala/gitbucket/core/service/SystemSettingsService.scala index 3346bec..b1f0f5c 100644 --- a/src/main/scala/gitbucket/core/service/SystemSettingsService.scala +++ b/src/main/scala/gitbucket/core/service/SystemSettingsService.scala @@ -1,6 +1,7 @@ package gitbucket.core.service import gitbucket.core.util.{Directory, ControlUtil} +import gitbucket.core.util.Implicits._ import Directory._ import ControlUtil._ import SystemSettingsService._ @@ -21,6 +22,7 @@ props.setProperty(Notification, settings.notification.toString) settings.activityLogLimit.foreach(x => props.setProperty(ActivityLogLimit, x.toString)) props.setProperty(Ssh, settings.ssh.toString) + settings.sshHost.foreach(x => props.setProperty(SshHost, x.trim)) settings.sshPort.foreach(x => props.setProperty(SshPort, x.toString)) props.setProperty(UseSMTP, settings.useSMTP.toString) if(settings.useSMTP) { @@ -75,6 +77,7 @@ getValue(props, Notification, false), getOptionValue[Int](props, ActivityLogLimit, None), getValue(props, Ssh, false), + getOptionValue[String](props, SshHost, None).map(_.trim), getOptionValue(props, SshPort, Some(DefaultSshPort)), getValue(props, UseSMTP, getValue(props, Notification, false)), // handle migration scenario from only notification to useSMTP if(getValue(props, UseSMTP, getValue(props, Notification, false))){ @@ -126,16 +129,19 @@ notification: Boolean, activityLogLimit: Option[Int], ssh: Boolean, + sshHost: Option[String], sshPort: Option[Int], useSMTP: Boolean, smtp: Option[Smtp], ldapAuthentication: Boolean, ldap: Option[Ldap]){ - def baseUrl(request: HttpServletRequest): String = baseUrl.getOrElse { - defining(request.getRequestURL.toString){ url => - url.substring(0, url.length - (request.getRequestURI.length - request.getContextPath.length)) + def baseUrl(request: HttpServletRequest): String = baseUrl.fold(request.baseUrl)(_.stripSuffix("/")) + + def sshAddress:Option[SshAddress] = + for { + host <- sshHost if ssh } - }.stripSuffix("/") + yield SshAddress(host, sshPort.getOrElse(DefaultSshPort)) } case class Ldap( @@ -161,6 +167,10 @@ fromAddress: Option[String], fromName: Option[String]) + case class SshAddress( + host:String, + port:Int) + val DefaultSshPort = 29418 val DefaultSmtpPort = 25 val DefaultLdapPort = 389 @@ -174,6 +184,7 @@ private val Notification = "notification" private val ActivityLogLimit = "activity_log_limit" private val Ssh = "ssh" + private val SshHost = "ssh.host" private val SshPort = "ssh.port" private val UseSMTP = "useSMTP" private val SmtpHost = "smtp.host" @@ -216,7 +227,4 @@ else value } -// // TODO temporary flag -// val enablePluginSystem = Option(System.getProperty("enable.plugin")).getOrElse("false").toBoolean - } diff --git a/src/main/scala/gitbucket/core/service/WebHookService.scala b/src/main/scala/gitbucket/core/service/WebHookService.scala index 059ca5d..4af5ec3 100644 --- a/src/main/scala/gitbucket/core/service/WebHookService.scala +++ b/src/main/scala/gitbucket/core/service/WebHookService.scala @@ -1,8 +1,13 @@ package gitbucket.core.service +import java.io.ByteArrayInputStream + +import fr.brouillard.oss.security.xhub.XHub +import fr.brouillard.oss.security.xhub.XHub.{XHubDigest, XHubConverter} import gitbucket.core.api._ import gitbucket.core.model.{WebHook, Account, Issue, PullRequest, IssueComment, WebHookEvent, CommitComment} import gitbucket.core.model.Profile._ +import org.apache.http.client.utils.URLEncodedUtils import profile.simple._ import gitbucket.core.util.JGitUtil.CommitInfo import gitbucket.core.util.RepositoryName @@ -33,8 +38,11 @@ /** get All WebHook informations of repository event */ def getWebHooksByEvent(owner: String, repository: String, event: WebHook.Event)(implicit s: Session): List[WebHook] = - WebHookEvents.filter(t => t.byRepository(owner, repository) && t.event === event.bind) - .list.map(t => WebHook(t.userName, t.repositoryName, t.url)) + WebHooks.filter(_.byRepository(owner, repository)) + .innerJoin(WebHookEvents).on { (wh, whe) => whe.byWebHook(wh) } + .filter{ case (wh, whe) => whe.event === event.bind} + .map{ case (wh, whe) => wh } + .list.distinct /** get All WebHook information from repository to url */ def getWebHook(owner: String, repository: String, url: String)(implicit s: Session): Option[(WebHook, Set[WebHook.Event])] = @@ -44,14 +52,15 @@ .map{ case (w,t) => w -> t.event } .list.groupBy(_._1).mapValues(_.map(_._2).toSet).headOption - def addWebHook(owner: String, repository: String, url :String, events: Set[WebHook.Event])(implicit s: Session): Unit = { - WebHooks insert WebHook(owner, repository, url) + def addWebHook(owner: String, repository: String, url :String, events: Set[WebHook.Event], token: Option[String])(implicit s: Session): Unit = { + WebHooks insert WebHook(owner, repository, url, token) events.toSet.map{ event: WebHook.Event => WebHookEvents insert WebHookEvent(owner, repository, url, event) } } - def updateWebHook(owner: String, repository: String, url :String, events: Set[WebHook.Event])(implicit s: Session): Unit = { + def updateWebHook(owner: String, repository: String, url :String, events: Set[WebHook.Event], token: Option[String])(implicit s: Session): Unit = { + WebHooks.filter(_.byPrimaryKey(owner, repository, url)).map(w => w.token).update(token) WebHookEvents.filter(_.byWebHook(owner, repository, url)).delete events.toSet.map{ event: WebHook.Event => WebHookEvents insert WebHookEvent(owner, repository, url, event) @@ -69,17 +78,17 @@ } } - def callWebHook(event: WebHook.Event, webHookURLs: List[WebHook], payload: WebHookPayload) + def callWebHook(event: WebHook.Event, webHooks: List[WebHook], payload: WebHookPayload) (implicit c: JsonFormat.Context): List[(WebHook, String, Future[HttpRequest], Future[HttpResponse])] = { import org.apache.http.impl.client.HttpClientBuilder import ExecutionContext.Implicits.global import org.apache.http.protocol.HttpContext import org.apache.http.client.methods.HttpPost - if(webHookURLs.nonEmpty){ + if(webHooks.nonEmpty){ val json = JsonFormat(payload) - webHookURLs.map { webHookUrl => + webHooks.map { webHook => val reqPromise = Promise[HttpRequest] val f = Future { val itcp = new org.apache.http.HttpRequestInterceptor{ @@ -89,19 +98,26 @@ } try{ val httpClient = HttpClientBuilder.create.addInterceptorLast(itcp).build - logger.debug(s"start web hook invocation for ${webHookUrl.url}") - val httpPost = new HttpPost(webHookUrl.url) + logger.debug(s"start web hook invocation for ${webHook.url}") + val httpPost = new HttpPost(webHook.url) httpPost.addHeader("Content-Type", "application/x-www-form-urlencoded") httpPost.addHeader("X-Github-Event", event.name) httpPost.addHeader("X-Github-Delivery", java.util.UUID.randomUUID().toString) val params: java.util.List[NameValuePair] = new java.util.ArrayList() params.add(new BasicNameValuePair("payload", json)) - httpPost.setEntity(new UrlEncodedFormEntity(params, "UTF-8")) + def postContent = new UrlEncodedFormEntity(params, "UTF-8") + httpPost.setEntity(postContent) + + if (!webHook.token.isEmpty) { + // TODO find a better way and see how to extract content from postContent + val contentAsBytes = URLEncodedUtils.format(params, "UTF-8").getBytes("UTF-8") + httpPost.addHeader("X-Hub-Signature", XHub.generateHeaderXHubToken(XHubConverter.HEXA_LOWERCASE, XHubDigest.SHA1, webHook.token.orNull, contentAsBytes)) + } val res = httpClient.execute(httpPost) httpPost.releaseConnection() - logger.debug(s"end web hook invocation for ${webHookUrl}") + logger.debug(s"end web hook invocation for ${webHook}") res }catch{ case e:Throwable => { @@ -113,12 +129,12 @@ } } f.onSuccess { - case s => logger.debug(s"Success: web hook request to ${webHookUrl.url}") + case s => logger.debug(s"Success: web hook request to ${webHook.url}") } f.onFailure { - case t => logger.error(s"Failed: web hook request to ${webHookUrl.url}", t) + case t => logger.error(s"Failed: web hook request to ${webHook.url}", t) } - (webHookUrl, json, reqPromise.future, f) + (webHook, json, reqPromise.future, f) } } else { Nil @@ -161,7 +177,7 @@ baseOwner <- users.get(repository.owner) headOwner <- users.get(pullRequest.requestUserName) issueUser <- users.get(issue.openedUserName) - headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName, baseUrl) + headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName) } yield { WebHookPullRequestPayload( action = action, @@ -200,7 +216,7 @@ import WebHookService._ for{ ((issue, issueUser, pullRequest, baseOwner, headOwner), webHooks) <- getPullRequestsByRequestForWebhook(requestRepository.owner, requestRepository.name, requestBranch) - baseRepo <- getRepository(pullRequest.userName, pullRequest.repositoryName, baseUrl) + baseRepo <- getRepository(pullRequest.userName, pullRequest.repositoryName) } yield { val payload = WebHookPullRequestPayload( action = action, @@ -229,7 +245,7 @@ baseOwner <- users.get(repository.owner) headOwner <- users.get(pullRequest.requestUserName) issueUser <- users.get(issue.openedUserName) - headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName, baseUrl) + headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName) } yield { WebHookPullRequestReviewCommentPayload( action = action, diff --git a/src/main/scala/gitbucket/core/service/WikiService.scala b/src/main/scala/gitbucket/core/service/WikiService.scala index 29d8e56..1bff5dc 100644 --- a/src/main/scala/gitbucket/core/service/WikiService.scala +++ b/src/main/scala/gitbucket/core/service/WikiService.scala @@ -1,7 +1,9 @@ package gitbucket.core.service import java.util.Date +import gitbucket.core.controller.Context import gitbucket.core.model.Account +import gitbucket.core.service.RepositoryService.RepositoryInfo import gitbucket.core.util._ import gitbucket.core.util.ControlUtil._ import org.eclipse.jgit.api.Git @@ -13,7 +15,6 @@ import org.eclipse.jgit.patch._ import org.eclipse.jgit.api.errors.PatchFormatException import scala.collection.JavaConverters._ -import RepositoryService.RepositoryInfo object WikiService { @@ -38,10 +39,13 @@ */ case class WikiPageHistoryInfo(name: String, committer: String, message: String, date: Date) - def httpUrl(repository: RepositoryInfo) = repository.httpUrl.replaceFirst("\\.git\\Z", ".wiki.git") - def sshUrl(repository: RepositoryInfo, settings: SystemSettingsService.SystemSettings, userName: String) = - repository.sshUrl(settings.sshPort.getOrElse(SystemSettingsService.DefaultSshPort), userName).replaceFirst("\\.git\\Z", ".wiki.git") + def wikiHttpUrl(repositoryInfo: RepositoryInfo)(implicit context: Context): String + = RepositoryService.httpUrl(repositoryInfo.owner, repositoryInfo.name + ".wiki") + + def wikiSshUrl(repositoryInfo: RepositoryInfo)(implicit context: Context): Option[String] + = RepositoryService.sshUrl(repositoryInfo.owner, repositoryInfo.name + ".wiki") + } trait WikiService { diff --git a/src/main/scala/gitbucket/core/servlet/BasicAuthenticationFilter.scala b/src/main/scala/gitbucket/core/servlet/BasicAuthenticationFilter.scala index 421a461..696f5f1 100644 --- a/src/main/scala/gitbucket/core/servlet/BasicAuthenticationFilter.scala +++ b/src/main/scala/gitbucket/core/servlet/BasicAuthenticationFilter.scala @@ -74,7 +74,7 @@ request.paths match { case Array(_, repositoryOwner, repositoryName, _*) => - getRepository(repositoryOwner, repositoryName.replaceFirst("\\.wiki\\.git$|\\.git$", ""), "") match { + getRepository(repositoryOwner, repositoryName.replaceFirst("\\.wiki\\.git$|\\.git$", "")) match { case Some(repository) => { if(!isUpdating && !repository.repository.isPrivate && settings.allowAnonymousAccess){ chain.doFilter(request, response) diff --git a/src/main/scala/gitbucket/core/servlet/GitRepositoryServlet.scala b/src/main/scala/gitbucket/core/servlet/GitRepositoryServlet.scala index 1f8347a..0583f61 100644 --- a/src/main/scala/gitbucket/core/servlet/GitRepositoryServlet.scala +++ b/src/main/scala/gitbucket/core/servlet/GitRepositoryServlet.scala @@ -160,7 +160,7 @@ countIssue(IssueSearchCondition(state = "open"), false, owner -> repository) + countIssue(IssueSearchCondition(state = "closed"), false, owner -> repository) - val repositoryInfo = getRepository(owner, repository, baseUrl).get + val repositoryInfo = getRepository(owner, repository).get // Extract new commit and apply issue comment val defaultBranch = repositoryInfo.repository.defaultBranch diff --git a/src/main/scala/gitbucket/core/ssh/GitCommand.scala b/src/main/scala/gitbucket/core/ssh/GitCommand.scala index 8e19255..eb12001 100644 --- a/src/main/scala/gitbucket/core/ssh/GitCommand.scala +++ b/src/main/scala/gitbucket/core/ssh/GitCommand.scala @@ -87,11 +87,11 @@ } -class DefaultGitUploadPack(owner: String, repoName: String, baseUrl: String) extends DefaultGitCommand(owner, repoName) +class DefaultGitUploadPack(owner: String, repoName: String) extends DefaultGitCommand(owner, repoName) with RepositoryService with AccountService { override protected def runTask(user: String)(implicit session: Session): Unit = { - getRepository(owner, repoName.replaceFirst("\\.wiki\\Z", ""), baseUrl).foreach { repositoryInfo => + getRepository(owner, repoName.replaceFirst("\\.wiki\\Z", "")).foreach { repositoryInfo => if(!repositoryInfo.repository.isPrivate || isWritableUser(user, repositoryInfo)){ using(Git.open(getRepositoryDir(owner, repoName))) { git => val repository = git.getRepository @@ -107,7 +107,7 @@ with RepositoryService with AccountService { override protected def runTask(user: String)(implicit session: Session): Unit = { - getRepository(owner, repoName.replaceFirst("\\.wiki\\Z", ""), baseUrl).foreach { repositoryInfo => + getRepository(owner, repoName.replaceFirst("\\.wiki\\Z", "")).foreach { repositoryInfo => if(isWritableUser(user, repositoryInfo)){ using(Git.open(getRepositoryDir(owner, repoName))) { git => val repository = git.getRepository @@ -124,7 +124,7 @@ } } -class PluginGitUploadPack(repoName: String, baseUrl: String, routing: GitRepositoryRouting) extends GitCommand +class PluginGitUploadPack(repoName: String, routing: GitRepositoryRouting) extends GitCommand with SystemSettingsService { override protected def runTask(user: String)(implicit session: Session): Unit = { @@ -139,7 +139,7 @@ } } -class PluginGitReceivePack(repoName: String, baseUrl: String, routing: GitRepositoryRouting) extends GitCommand +class PluginGitReceivePack(repoName: String, routing: GitRepositoryRouting) extends GitCommand with SystemSettingsService { override protected def runTask(user: String)(implicit session: Session): Unit = { @@ -163,9 +163,9 @@ logger.debug(s"command: $command") command match { - case SimpleCommandRegex ("upload" , repoName) if(pluginRepository(repoName)) => new PluginGitUploadPack (repoName, baseUrl, routing(repoName)) - case SimpleCommandRegex ("receive", repoName) if(pluginRepository(repoName)) => new PluginGitReceivePack(repoName, baseUrl, routing(repoName)) - case DefaultCommandRegex("upload" , owner, repoName) => new DefaultGitUploadPack (owner, repoName, baseUrl) + case SimpleCommandRegex ("upload" , repoName) if(pluginRepository(repoName)) => new PluginGitUploadPack (repoName, routing(repoName)) + case SimpleCommandRegex ("receive", repoName) if(pluginRepository(repoName)) => new PluginGitReceivePack(repoName, routing(repoName)) + case DefaultCommandRegex("upload" , owner, repoName) => new DefaultGitUploadPack (owner, repoName) case DefaultCommandRegex("receive", owner, repoName) => new DefaultGitReceivePack(owner, repoName, baseUrl) case _ => new UnknownCommand(command) } diff --git a/src/main/scala/gitbucket/core/ssh/NoShell.scala b/src/main/scala/gitbucket/core/ssh/NoShell.scala index bd30ccf..6f2f42d 100644 --- a/src/main/scala/gitbucket/core/ssh/NoShell.scala +++ b/src/main/scala/gitbucket/core/ssh/NoShell.scala @@ -1,12 +1,13 @@ package gitbucket.core.ssh import gitbucket.core.service.SystemSettingsService +import gitbucket.core.service.SystemSettingsService.SshAddress import org.apache.sshd.common.Factory import org.apache.sshd.server.{Environment, ExitCallback, Command} import java.io.{OutputStream, InputStream} import org.eclipse.jgit.lib.Constants -class NoShell extends Factory[Command] with SystemSettingsService { +class NoShell(sshAddress:SshAddress) extends Factory[Command] { override def create(): Command = new Command() { private var in: InputStream = null private var out: OutputStream = null @@ -15,7 +16,6 @@ override def start(env: Environment): Unit = { val user = env.getEnv.get("USER") - val port = loadSystemSettings().sshPort.getOrElse(SystemSettingsService.DefaultSshPort) val message = """ | Welcome to @@ -31,8 +31,8 @@ | | Please use: | - | git clone ssh://%s@GITBUCKET_HOST:%d/OWNER/REPOSITORY_NAME.git - """.stripMargin.format(user, port).replace("\n", "\r\n") + "\r\n" + | git clone ssh://%s@%s:%d/OWNER/REPOSITORY_NAME.git + """.stripMargin.format(user, sshAddress.host, sshAddress.port).replace("\n", "\r\n") + "\r\n" err.write(Constants.encode(message)) err.flush() in.close() diff --git a/src/main/scala/gitbucket/core/ssh/SshServerListener.scala b/src/main/scala/gitbucket/core/ssh/SshServerListener.scala index f79ae93..8795e40 100644 --- a/src/main/scala/gitbucket/core/ssh/SshServerListener.scala +++ b/src/main/scala/gitbucket/core/ssh/SshServerListener.scala @@ -5,7 +5,8 @@ import javax.servlet.{ServletContextEvent, ServletContextListener} import gitbucket.core.service.SystemSettingsService -import gitbucket.core.util.Directory +import gitbucket.core.service.SystemSettingsService.SshAddress +import gitbucket.core.util.{Directory} import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider import org.slf4j.LoggerFactory @@ -14,20 +15,20 @@ private val server = org.apache.sshd.server.SshServer.setUpDefaultServer() private val active = new AtomicBoolean(false) - private def configure(port: Int, baseUrl: String) = { - server.setPort(port) + private def configure(sshAddress: SshAddress, baseUrl: String) = { + server.setPort(sshAddress.port) val provider = new SimpleGeneratorHostKeyProvider(new File(s"${Directory.GitBucketHome}/gitbucket.ser")) provider.setAlgorithm("RSA") provider.setOverwriteAllowed(false) server.setKeyPairProvider(provider) server.setPublickeyAuthenticator(new PublicKeyAuthenticator) server.setCommandFactory(new GitCommandFactory(baseUrl)) - server.setShellFactory(new NoShell) + server.setShellFactory(new NoShell(sshAddress)) } - def start(port: Int, baseUrl: String) = { + def start(sshAddress: SshAddress, baseUrl: String) = { if(active.compareAndSet(false, true)){ - configure(port, baseUrl) + configure(sshAddress, baseUrl) server.start() logger.info(s"Start SSH Server Listen on ${server.getPort}") } @@ -55,20 +56,18 @@ override def contextInitialized(sce: ServletContextEvent): Unit = { val settings = loadSystemSettings() - if(settings.ssh){ - settings.baseUrl match { - case None => - logger.error("Could not start SshServer because the baseUrl is not configured.") - case Some(baseUrl) => - SshServer.start(settings.sshPort.getOrElse(SystemSettingsService.DefaultSshPort), baseUrl) - } + if (settings.sshAddress.isDefined && settings.baseUrl.isEmpty) { + logger.error("Could not start SshServer because the baseUrl is not configured.") } + for { + sshAddress <- settings.sshAddress + baseUrl <- settings.baseUrl + } + SshServer.start(sshAddress, baseUrl) } override def contextDestroyed(sce: ServletContextEvent): Unit = { - if(loadSystemSettings().ssh){ - SshServer.stop() - } + SshServer.stop() } } diff --git a/src/main/scala/gitbucket/core/util/Authenticator.scala b/src/main/scala/gitbucket/core/util/Authenticator.scala index 49de665..0035494 100644 --- a/src/main/scala/gitbucket/core/util/Authenticator.scala +++ b/src/main/scala/gitbucket/core/util/Authenticator.scala @@ -36,7 +36,7 @@ private def authenticate(action: (RepositoryInfo) => Any) = { { defining(request.paths){ paths => - getRepository(paths(0), paths(1), baseUrl).map { repository => + getRepository(paths(0), paths(1)).map { repository => context.loginAccount match { case Some(x) if(x.isAdmin) => action(repository) case Some(x) if(repository.owner == x.userName) => action(repository) @@ -95,7 +95,7 @@ private def authenticate(action: (RepositoryInfo) => Any) = { { defining(request.paths){ paths => - getRepository(paths(0), paths(1), baseUrl).map { repository => + getRepository(paths(0), paths(1)).map { repository => context.loginAccount match { case Some(x) if(x.isAdmin) => action(repository) case Some(x) if(paths(0) == x.userName) => action(repository) @@ -118,7 +118,7 @@ private def authenticate(action: (RepositoryInfo) => Any) = { { defining(request.paths){ paths => - getRepository(paths(0), paths(1), baseUrl).map { repository => + getRepository(paths(0), paths(1)).map { repository => if(!repository.repository.isPrivate){ action(repository) } else { @@ -145,7 +145,7 @@ private def authenticate(action: (RepositoryInfo) => Any) = { { defining(request.paths){ paths => - getRepository(paths(0), paths(1), baseUrl).map { repository => + getRepository(paths(0), paths(1)).map { repository => context.loginAccount match { case Some(x) if(x.isAdmin) => action(repository) case Some(x) if(!repository.repository.isPrivate) => action(repository) diff --git a/src/main/scala/gitbucket/core/util/Implicits.scala b/src/main/scala/gitbucket/core/util/Implicits.scala index 131bd98..a7209de 100644 --- a/src/main/scala/gitbucket/core/util/Implicits.scala +++ b/src/main/scala/gitbucket/core/util/Implicits.scala @@ -75,6 +75,11 @@ def gitRepositoryPath: String = request.getRequestURI.replaceFirst("^/git/", "/") + def baseUrl:String = { + val url = request.getRequestURL.toString + val len = url.length - (request.getRequestURI.length - request.getContextPath.length) + url.substring(0, len).stripSuffix("/") + } } implicit class RichSession(session: HttpSession){ diff --git a/src/main/scala/gitbucket/core/util/JGitUtil.scala b/src/main/scala/gitbucket/core/util/JGitUtil.scala index cf23257..2a1182f 100644 --- a/src/main/scala/gitbucket/core/util/JGitUtil.scala +++ b/src/main/scala/gitbucket/core/util/JGitUtil.scala @@ -32,14 +32,13 @@ * * @param owner the user name of the repository owner * @param name the repository name - * @param url the repository URL * @param commitCount the commit count. If the repository has over 1000 commits then this property is 1001. * @param branchList the list of branch names * @param tags the list of tags */ - case class RepositoryInfo(owner: String, name: String, url: String, commitCount: Int, branchList: List[String], tags: List[TagInfo]){ - def this(owner: String, name: String, baseUrl: String) = { - this(owner, name, s"${baseUrl}/git/${owner}/${name}.git", 0, Nil, Nil) + case class RepositoryInfo(owner: String, name: String, commitCount: Int, branchList: List[String], tags: List[TagInfo]){ + def this(owner: String, name: String) = { + this(owner, name, 0, Nil, Nil) } } @@ -174,14 +173,14 @@ /** * Returns the repository information. It contains branch names and tag names. */ - def getRepositoryInfo(owner: String, repository: String, baseUrl: String): RepositoryInfo = { + def getRepositoryInfo(owner: String, repository: String): RepositoryInfo = { using(Git.open(getRepositoryDir(owner, repository))){ git => try { // get commit count val commitCount = git.log.all.call.iterator.asScala.map(_ => 1).take(10001).sum RepositoryInfo( - owner, repository, s"${baseUrl}/git/${owner}/${repository}.git", + owner, repository, // commit count commitCount, // branches @@ -197,7 +196,7 @@ } catch { // not initialized case e: NoHeadException => RepositoryInfo( - owner, repository, s"${baseUrl}/git/${owner}/${repository}.git", 0, Nil, Nil) + owner, repository, 0, Nil, Nil) } } diff --git a/src/main/scala/gitbucket/core/view/helpers.scala b/src/main/scala/gitbucket/core/view/helpers.scala index b61c0d6..570e9b1 100644 --- a/src/main/scala/gitbucket/core/view/helpers.scala +++ b/src/main/scala/gitbucket/core/view/helpers.scala @@ -9,7 +9,7 @@ import gitbucket.core.service.{RepositoryService, RequestCache} import gitbucket.core.util.{FileUtil, JGitUtil, StringUtil} -import play.twirl.api.Html +import play.twirl.api.{Html, HtmlFormat} /** * Provides helper methods for Twirl templates. @@ -225,6 +225,13 @@ def avatarLink(userName: String, size: Int, mailAddress: String = "", tooltip: Boolean = false)(implicit context: Context): Html = userWithContent(userName, mailAddress)(avatar(userName, size, tooltip, mailAddress)) + /** + * Generates the avatar link to the account page. + * If user does not exist or disabled, this method returns avatar image without link. + */ + def avatarLink(commit: JGitUtil.CommitInfo, size: Int)(implicit context: Context): Html = + userWithContent(commit.authorName, commit.authorEmailAddress)(avatar(commit, size)) + private def userWithContent(userName: String, mailAddress: String = "", styleClass: String = "")(content: Html)(implicit context: Context): Html = (if(mailAddress.isEmpty){ getAccountByUserName(userName) @@ -306,6 +313,19 @@ private[this] val detectAndRenderLinksRegex = """(?i)\b((?:https?://|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,13}/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))""".r def detectAndRenderLinks(text: String): Html = { - Html(detectAndRenderLinksRegex.replaceAllIn(text, m => s"""${m.group(0)}""")) + val matches = detectAndRenderLinksRegex.findAllMatchIn(text).toSeq + + val (x, pos) = matches.foldLeft((collection.immutable.Seq.empty[Html], 0)){ case ((x, pos), m) => + val url = m.group(0) + val href = url.replace("\"", """) + (x ++ (Seq( + if(pos < m.start) Some(HtmlFormat.escape(text.substring(pos, m.start))) else None, + Some(Html(s"""${url}""")) + ).flatten), m.end) + } + // append rest fragment + val out = if (pos < text.length) x :+ HtmlFormat.escape(text.substring(pos)) else x + + HtmlFormat.fill(out) } } diff --git a/src/main/twirl/gitbucket/core/account/application.scala.html b/src/main/twirl/gitbucket/core/account/application.scala.html index f36e840..d2d8f6e 100644 --- a/src/main/twirl/gitbucket/core/account/application.scala.html +++ b/src/main/twirl/gitbucket/core/account/application.scala.html @@ -4,7 +4,7 @@ @import context._ @import gitbucket.core.view.helpers._ @html.main("Applications"){ -