diff --git a/src/main/resources/update/gitbucket-core_4.35.xml b/src/main/resources/update/gitbucket-core_4.35.xml new file mode 100644 index 0000000..687419a --- /dev/null +++ b/src/main/resources/update/gitbucket-core_4.35.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/main/scala/gitbucket/core/GitBucketCoreModule.scala b/src/main/scala/gitbucket/core/GitBucketCoreModule.scala index dd00503..0e2a15b 100644 --- a/src/main/scala/gitbucket/core/GitBucketCoreModule.scala +++ b/src/main/scala/gitbucket/core/GitBucketCoreModule.scala @@ -113,5 +113,6 @@ } }, new LiquibaseMigration("update/gitbucket-core_4.34.xml") - ) + ), + new Version("4.35.0", new LiquibaseMigration("update/gitbucket-core_4.35.xml")), ) diff --git a/src/main/scala/gitbucket/core/api/ApiWebhook.scala b/src/main/scala/gitbucket/core/api/ApiWebhook.scala new file mode 100644 index 0000000..30a5f48 --- /dev/null +++ b/src/main/scala/gitbucket/core/api/ApiWebhook.scala @@ -0,0 +1,46 @@ +package gitbucket.core.api + +import gitbucket.core.model.Profile.{RepositoryWebHookEvents, RepositoryWebHooks} +import gitbucket.core.model.{RepositoryWebHook, WebHook} +import gitbucket.core.util.RepositoryName + +/** + * https://docs.github.com/en/rest/reference/repos#webhooks + */ +case class ApiWebhookConfig( + content_type: String, +// insecure_ssl: String, + url: String +) + +case class ApiWebhook( + `type`: String, + id: Int, + name: String, + active: Boolean, + events: List[String], + config: ApiWebhookConfig, +// updated_at: Option[Date], +// created_at: Option[Date], + url: ApiPath, +// test_url: ApiPath, +// ping_url: ApiPath, +// last_response: ... +) + +object ApiWebhook { + def apply( + _type: String, + hook: RepositoryWebHook, + hookEvents: Set[WebHook.Event] + ): ApiWebhook = + ApiWebhook( + `type` = _type, + id = hook.hookId, + name = "web", // dummy + active = true, // dummy + events = hookEvents.toList.map(_.name), + config = ApiWebhookConfig(hook.ctype.code, hook.url), + url = ApiPath(s"/api/v3/${hook.userName}/${hook.repositoryName}/hooks/${hook.hookId}") + ) +} diff --git a/src/main/scala/gitbucket/core/api/CreateARepositoryWebhook.scala b/src/main/scala/gitbucket/core/api/CreateARepositoryWebhook.scala new file mode 100644 index 0000000..572c7ba --- /dev/null +++ b/src/main/scala/gitbucket/core/api/CreateARepositoryWebhook.scala @@ -0,0 +1,35 @@ +package gitbucket.core.api + +case class CreateARepositoryWebhookConfig( + url: String, + content_type: String = "form", + insecure_ssl: String = "0", + secret: Option[String] +) + +/** + * https://docs.github.com/en/rest/reference/repos#create-a-repository-webhook + */ +case class CreateARepositoryWebhook( + name: String = "web", + config: CreateARepositoryWebhookConfig, + events: List[String] = List("push"), + active: Boolean = true +) { + def isValid: Boolean = { + config.content_type == "form" || config.content_type == "json" + } +} + +case class UpdateARepositoryWebhook( + name: String = "web", + config: CreateARepositoryWebhookConfig, + events: List[String] = List("push"), + add_events: List[String] = List(), + remove_events: List[String] = List(), + active: Boolean = true +) { + def isValid: Boolean = { + config.content_type == "form" || config.content_type == "json" + } +} diff --git a/src/main/scala/gitbucket/core/controller/AccountController.scala b/src/main/scala/gitbucket/core/controller/AccountController.scala index f4978d4..b0aae0d 100644 --- a/src/main/scala/gitbucket/core/controller/AccountController.scala +++ b/src/main/scala/gitbucket/core/controller/AccountController.scala @@ -522,7 +522,8 @@ val url = params("url") val token = Some(params("token")) val ctype = WebHookContentType.valueOf(params("ctype")) - val dummyWebHookInfo = RepositoryWebHook(userName, "dummy", url, ctype, token) + val dummyWebHookInfo = + RepositoryWebHook(userName = userName, repositoryName = "dummy", url = url, ctype = ctype, token = token) val dummyPayload = { val ownerAccount = getAccountByUserName(userName).get WebHookPushPayload.createDummyPayload(ownerAccount) diff --git a/src/main/scala/gitbucket/core/controller/ApiController.scala b/src/main/scala/gitbucket/core/controller/ApiController.scala index 07898e1..a5af352 100644 --- a/src/main/scala/gitbucket/core/controller/ApiController.scala +++ b/src/main/scala/gitbucket/core/controller/ApiController.scala @@ -22,6 +22,7 @@ with ApiRepositoryContentsControllerBase with ApiRepositoryControllerBase with ApiRepositoryStatusControllerBase + with ApiRepositoryWebhookControllerBase with ApiUserControllerBase with RepositoryService with AccountService diff --git a/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala b/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala index 7ef721e..b47aab2 100644 --- a/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala +++ b/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala @@ -228,7 +228,13 @@ * Display the web hook edit page. */ get("/:owner/:repository/settings/hooks/new")(ownerOnly { repository => - val webhook = RepositoryWebHook(repository.owner, repository.name, "", WebHookContentType.FORM, None) + val webhook = RepositoryWebHook( + userName = repository.owner, + repositoryName = repository.name, + url = "", + ctype = WebHookContentType.FORM, + token = None + ) html.edithook(webhook, Set(WebHook.Push), repository, true) }) @@ -271,7 +277,13 @@ val url = params("url") val token = Some(params("token")) val ctype = WebHookContentType.valueOf(params("ctype")) - val dummyWebHookInfo = RepositoryWebHook(repository.owner, repository.name, url, ctype, token) + val dummyWebHookInfo = RepositoryWebHook( + userName = repository.owner, + repositoryName = repository.name, + url = url, + ctype = ctype, + token = token + ) val dummyPayload = { val ownerAccount = getAccountByUserName(repository.owner).get val commits = diff --git a/src/main/scala/gitbucket/core/controller/api/ApiRepositoryWebhookControllerBase.scala b/src/main/scala/gitbucket/core/controller/api/ApiRepositoryWebhookControllerBase.scala new file mode 100644 index 0000000..797d19a --- /dev/null +++ b/src/main/scala/gitbucket/core/controller/api/ApiRepositoryWebhookControllerBase.scala @@ -0,0 +1,120 @@ +package gitbucket.core.controller.api +import gitbucket.core.api._ +import gitbucket.core.controller.ControllerBase +import gitbucket.core.model.{WebHook, WebHookContentType} +import gitbucket.core.service.{RepositoryService, WebHookService} +import gitbucket.core.util._ +import gitbucket.core.util.Implicits._ +import org.scalatra.NoContent + +trait ApiRepositoryWebhookControllerBase extends ControllerBase { + self: RepositoryService with WebHookService with ReferrerAuthenticator with WritableUsersAuthenticator => + + /* + * i. List repository webhooks + * https://docs.github.com/en/rest/reference/repos#list-repository-webhooks + */ + get("/api/v3/repos/:owner/:repository/hooks")(referrersOnly { repository => + val apiWebhooks = for { + (hook, events) <- getWebHooks(repository.owner, repository.name) + } yield { + ApiWebhook("Repository", hook, events) + } + JsonFormat(apiWebhooks) + }) + + /* + * ii. Create a repository webhook + * https://docs.github.com/en/rest/reference/repos#create-a-repository-webhook + */ + post("/api/v3/repos/:owner/:repository/hooks")(writableUsersOnly { repository => + (for { + data <- extractFromJsonBody[CreateARepositoryWebhook] if data.isValid + ctype = if (data.config.content_type == "form") WebHookContentType.FORM else WebHookContentType.JSON + events = data.events.map(p => WebHook.Event.valueOf(p)).toSet + } yield { + addWebHook( + repository.owner, + repository.name, + data.config.url, + events, + ctype, + data.config.secret + ) + getWebHook(repository.owner, repository.name, data.config.url) match { + case Some(createdHook) => JsonFormat(ApiWebhook("Repository", createdHook._1, createdHook._2)) + case _ => + } + }) getOrElse NotFound() + }) + + /* + * iii. Get a repository webhook + * https://docs.github.com/en/rest/reference/repos#get-a-repository-webhook + */ + get("/api/v3/repos/:owner/:repository/hooks/:id")(referrersOnly { repository => + val hookId = params("id").toInt + getWebHookById(hookId) match { + case Some(hook) => JsonFormat(ApiWebhook("Repository", hook._1, hook._2)) + case _ => NotFound() + } + }) + + /* + * iv. Update a repository webhook + * https://docs.github.com/en/rest/reference/repos#update-a-repository-webhook + */ + patch("/api/v3/repos/:owner/:repository/hooks/:id")(writableUsersOnly { repository => + val hookId = params("id").toInt + (for { + data <- extractFromJsonBody[UpdateARepositoryWebhook] if data.isValid + ctype = data.config.content_type match { + case "json" => WebHookContentType.JSON + case _ => WebHookContentType.FORM + } + } yield { + val events = (data.events ++ data.add_events) + .filterNot(p => data.remove_events.contains(p)) + .map(p => WebHook.Event.valueOf(p)) + .toSet + updateWebHookByApi( + hookId, + repository.owner, + repository.name, + data.config.url, + events, + ctype, + data.config.secret + ) + getWebHookById(hookId) match { + case Some(updatedHook) => JsonFormat(ApiWebhook("Repository", updatedHook._1, updatedHook._2)) + case _ => + } + }) getOrElse NotFound() + }) + + /* + * v. Delete a repository webhook + * https://docs.github.com/en/rest/reference/repos#delete-a-repository-webhook + */ + delete("/api/v3/repos/:owner/:repository/hooks/:id")(writableUsersOnly { repository => + val hookId = params("id").toInt + getWebHookById(hookId) match { + case Some(_) => + deleteWebHookById(params("id").toInt) + NoContent() + case _ => NotFound() + } + }) + + /* + * vi. Ping a repository webhook + * https://docs.github.com/en/rest/reference/repos#ping-a-repository-webhook + */ + + /* + * vi. Test the push repository webhook + * https://docs.github.com/en/rest/reference/repos#test-the-push-repository-webhook + */ + +} diff --git a/src/main/scala/gitbucket/core/model/RepositoryWebHook.scala b/src/main/scala/gitbucket/core/model/RepositoryWebHook.scala index 15ea535..cbe38f8 100644 --- a/src/main/scala/gitbucket/core/model/RepositoryWebHook.scala +++ b/src/main/scala/gitbucket/core/model/RepositoryWebHook.scala @@ -9,20 +9,25 @@ lazy val RepositoryWebHooks = TableQuery[RepositoryWebHooks] class RepositoryWebHooks(tag: Tag) extends Table[RepositoryWebHook](tag, "WEB_HOOK") with BasicTemplate { + val hookId = column[Int]("HOOK_ID", O AutoInc) val url = column[String]("URL") val token = column[Option[String]]("TOKEN") val ctype = column[WebHookContentType]("CTYPE") def * = - (userName, repositoryName, url, ctype, token) <> ((RepositoryWebHook.apply _).tupled, RepositoryWebHook.unapply) + (userName, repositoryName, hookId, url, ctype, token) <> ((RepositoryWebHook.apply _).tupled, RepositoryWebHook.unapply) - def byPrimaryKey(owner: String, repository: String, url: String) = + def byRepositoryUrl(owner: String, repository: String, url: String) = byRepository(owner, repository) && (this.url === url.bind) + + def byId(id: Int) = + (this.hookId === id.bind) } } case class RepositoryWebHook( userName: String, repositoryName: String, + hookId: Int = 0, url: String, ctype: WebHookContentType, token: Option[String] diff --git a/src/main/scala/gitbucket/core/service/WebHookService.scala b/src/main/scala/gitbucket/core/service/WebHookService.scala index 9a55873..7d12cc3 100644 --- a/src/main/scala/gitbucket/core/service/WebHookService.scala +++ b/src/main/scala/gitbucket/core/service/WebHookService.scala @@ -83,7 +83,7 @@ implicit s: Session ): Option[(RepositoryWebHook, Set[WebHook.Event])] = RepositoryWebHooks - .filter(_.byPrimaryKey(owner, repository, url)) + .filter(_.byRepositoryUrl(owner, repository, url)) .join(RepositoryWebHookEvents) .on { (w, t) => t.byRepositoryWebHook(w) @@ -95,6 +95,24 @@ .mapValues(_.map(_._2).toSet) .headOption + /** get All WebHook informations of repository */ + def getWebHookById(id: Int)( + implicit s: Session + ): Option[(RepositoryWebHook, Set[WebHook.Event])] = + RepositoryWebHooks + .filter(_.byId(id)) + .join(RepositoryWebHookEvents) + .on { (w, t) => + t.byRepositoryWebHook(w) + } + .map { case (w, t) => w -> t.event } + .list + .groupBy(_._1) + .view + .mapValues(_.map(_._2).toSet) + .toList + .headOption + def addWebHook( owner: String, repository: String, @@ -103,7 +121,13 @@ ctype: WebHookContentType, token: Option[String] )(implicit s: Session): Unit = { - RepositoryWebHooks insert RepositoryWebHook(owner, repository, url, ctype, token) + RepositoryWebHooks insert RepositoryWebHook( + userName = owner, + repositoryName = repository, + url = url, + ctype = ctype, + token = token + ) events.map { event: WebHook.Event => RepositoryWebHookEvents insert RepositoryWebHookEvent(owner, repository, url, event) } @@ -118,7 +142,7 @@ token: Option[String] )(implicit s: Session): Unit = { RepositoryWebHooks - .filter(_.byPrimaryKey(owner, repository, url)) + .filter(_.byRepositoryUrl(owner, repository, url)) .map(w => (w.ctype, w.token)) .update((ctype, token)) RepositoryWebHookEvents.filter(_.byRepositoryWebHook(owner, repository, url)).delete @@ -127,8 +151,30 @@ } } + def updateWebHookByApi( + id: Int, + owner: String, + repository: String, + url: String, + events: Set[WebHook.Event], + ctype: WebHookContentType, + token: Option[String] + )(implicit s: Session): Unit = { + RepositoryWebHooks + .filter(_.byId(id)) + .map(w => (w.url, w.ctype, w.token)) + .update((url, ctype, token)) + RepositoryWebHookEvents.filter(_.byRepositoryWebHook(owner, repository, url)).delete + events.map { event: WebHook.Event => + RepositoryWebHookEvents insert RepositoryWebHookEvent(owner, repository, url, event) + } + } + def deleteWebHook(owner: String, repository: String, url: String)(implicit s: Session): Unit = - RepositoryWebHooks.filter(_.byPrimaryKey(owner, repository, url)).delete + RepositoryWebHooks.filter(_.byRepositoryUrl(owner, repository, url)).delete + + def deleteWebHookById(id: Int)(implicit s: Session): Unit = + RepositoryWebHooks.filter(_.byId(id)).delete /** get All AccountWebHook informations of user */ def getAccountWebHooks(owner: String)(implicit s: Session): List[(AccountWebHook, Set[WebHook.Event])] = diff --git a/src/test/scala/gitbucket/core/service/WebHookServiceSpec.scala b/src/test/scala/gitbucket/core/service/WebHookServiceSpec.scala index ff73547..2a1d528 100644 --- a/src/test/scala/gitbucket/core/service/WebHookServiceSpec.scala +++ b/src/test/scala/gitbucket/core/service/WebHookServiceSpec.scala @@ -55,17 +55,23 @@ service.addWebHook("user1", "repo1", "http://example.com", Set(WebHook.PullRequest), formType, Some("key")) assert( service.getWebHooks("user1", "repo1") == List( - (RepositoryWebHook("user1", "repo1", "http://example.com", formType, Some("key")), Set(WebHook.PullRequest)) + ( + RepositoryWebHook("user1", "repo1", 1, "http://example.com", formType, Some("key")), + Set(WebHook.PullRequest) + ) ) ) assert( service.getWebHook("user1", "repo1", "http://example.com") == Some( - (RepositoryWebHook("user1", "repo1", "http://example.com", formType, Some("key")), Set(WebHook.PullRequest)) + ( + RepositoryWebHook("user1", "repo1", 1, "http://example.com", formType, Some("key")), + Set(WebHook.PullRequest) + ) ) ) assert( service.getWebHooksByEvent("user1", "repo1", WebHook.PullRequest) == List( - (RepositoryWebHook("user1", "repo1", "http://example.com", formType, Some("key"))) + (RepositoryWebHook("user1", "repo1", 1, "http://example.com", formType, Some("key"))) ) ) assert(service.getWebHooksByEvent("user1", "repo1", WebHook.Push) == Nil) @@ -83,7 +89,7 @@ assert( service.getWebHook("user1", "repo1", "http://example.com") == Some( ( - RepositoryWebHook("user1", "repo1", "http://example.com", jsonType, Some("key")), + RepositoryWebHook("user1", "repo1", 1, "http://example.com", jsonType, Some("key")), Set(WebHook.Push, WebHook.Issues) ) ) @@ -91,7 +97,7 @@ assert(service.getWebHooksByEvent("user1", "repo1", WebHook.PullRequest) == Nil) assert( service.getWebHooksByEvent("user1", "repo1", WebHook.Push) == List( - (RepositoryWebHook("user1", "repo1", "http://example.com", jsonType, Some("key"))) + (RepositoryWebHook("user1", "repo1", 1, "http://example.com", jsonType, Some("key"))) ) ) service.deleteWebHook("user1", "repo1", "http://example.com") @@ -115,9 +121,11 @@ ) assert( service.getWebHooks("user1", "repo1") == List( - RepositoryWebHook("user1", "repo1", "http://example.com/1", ctype, Some("key")) -> Set(WebHook.PullRequest), - RepositoryWebHook("user1", "repo1", "http://example.com/2", ctype, Some("key")) -> Set(WebHook.Push), - RepositoryWebHook("user1", "repo1", "http://example.com/3", ctype, Some("key")) -> Set( + RepositoryWebHook("user1", "repo1", 1, "http://example.com/1", ctype, Some("key")) -> Set( + WebHook.PullRequest + ), + RepositoryWebHook("user1", "repo1", 2, "http://example.com/2", ctype, Some("key")) -> Set(WebHook.Push), + RepositoryWebHook("user1", "repo1", 3, "http://example.com/3", ctype, Some("key")) -> Set( WebHook.PullRequest, WebHook.Push ) @@ -125,8 +133,8 @@ ) assert( service.getWebHooksByEvent("user1", "repo1", WebHook.PullRequest) == List( - RepositoryWebHook("user1", "repo1", "http://example.com/1", ctype, Some("key")), - RepositoryWebHook("user1", "repo1", "http://example.com/3", ctype, Some("key")) + RepositoryWebHook("user1", "repo1", 1, "http://example.com/1", ctype, Some("key")), + RepositoryWebHook("user1", "repo1", 3, "http://example.com/3", ctype, Some("key")) ) ) }