diff --git a/src/main/resources/update/gitbucket-core_4.11.xml b/src/main/resources/update/gitbucket-core_4.11.xml index 1c41d88..6026a95 100644 --- a/src/main/resources/update/gitbucket-core_4.11.xml +++ b/src/main/resources/update/gitbucket-core_4.11.xml @@ -11,4 +11,22 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/main/scala/gitbucket/core/api/ApiRepository.scala b/src/main/scala/gitbucket/core/api/ApiRepository.scala index 9c7377c..e1d88d9 100644 --- a/src/main/scala/gitbucket/core/api/ApiRepository.scala +++ b/src/main/scala/gitbucket/core/api/ApiRepository.scala @@ -53,4 +53,14 @@ def forPushPayload(repositoryInfo: RepositoryInfo, owner: ApiUser): ApiRepository = ApiRepository(repositoryInfo.repository, owner, forkedCount=repositoryInfo.forkedCount, urlIsHtmlUrl=true) + def forDummyPayload(owner: ApiUser): ApiRepository = + ApiRepository( + name="dummy", + full_name=s"${owner.login}/dummy", + description="", + watchers=0, + forks=0, + `private`=false, + default_branch="master", + owner=owner)(true) } diff --git a/src/main/scala/gitbucket/core/controller/AccountController.scala b/src/main/scala/gitbucket/core/controller/AccountController.scala index 19c553a..f050be4 100644 --- a/src/main/scala/gitbucket/core/controller/AccountController.scala +++ b/src/main/scala/gitbucket/core/controller/AccountController.scala @@ -2,9 +2,10 @@ import gitbucket.core.account.html import gitbucket.core.helper -import gitbucket.core.model.{GroupMember, Role} +import gitbucket.core.model.{GroupMember, Role, WebHook, WebHookContentType, AccountWebHook, RepositoryWebHook, RepositoryWebHookEvent} import gitbucket.core.plugin.PluginRegistry import gitbucket.core.service._ +import gitbucket.core.service.WebHookService._ import gitbucket.core.ssh.SshUtil import gitbucket.core.util.SyntaxSugars._ import gitbucket.core.util.Directory._ @@ -16,7 +17,6 @@ import org.scalatra.i18n.Messages import org.scalatra.BadRequest - class AccountController extends AccountControllerBase with AccountService with RepositoryService with ActivityService with WikiService with LabelsService with SshKeyService with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator @@ -109,6 +109,47 @@ "account" -> trim(label("Group/User name", text(required, validAccountName))) )(AccountForm.apply) + // for account web hook url addition. + case class AccountWebHookForm(url: String, events: Set[WebHook.Event], ctype: WebHookContentType, token: Option[String]) + + def accountWebHookForm(update:Boolean) = mapping( + "url" -> trim(label("url", text(required, accountWebHook(update)))), + "events" -> accountWebhookEvents, + "ctype" -> label("ctype", text()), + "token" -> optional(trim(label("token", text(maxlength(100))))) + )( + (url, events, ctype, token) => AccountWebHookForm(url, events, WebHookContentType.valueOf(ctype), token) + ) + /** + * Provides duplication check for web hook url. duplicated from RepositorySettingsController.scala + */ + private def accountWebHook(needExists: Boolean): Constraint = new Constraint(){ + override def validate(name: String, value: String, messages: Messages): Option[String] = + if(getAccountWebHook(params("userName"), value).isDefined != needExists){ + Some(if(needExists){ + "URL had not been registered yet." + } else { + "URL had been registered already." + }) + } else { + None + } + } + + private def accountWebhookEvents = new ValueType[Set[WebHook.Event]]{ + def convert(name: String, params: Map[String, String], messages: Messages): Set[WebHook.Event] = { + WebHook.Event.values.flatMap { t => + params.get(name + "." + t.name).map(_ => t) + }.toSet + } + def validate(name: String, params: Map[String, String], messages: Messages): Seq[(String, String)] = if(convert(name,params,messages).isEmpty){ + Seq(name -> messages("error.required").format(name)) + } else { + Nil + } + } + + /** * Displays user information. */ @@ -129,6 +170,13 @@ context.loginAccount.exists(x => members.exists { member => member.userName == x.userName && member.isManager })) } + // Webhooks + case "webhooks" => + gitbucket.core.account.html.webhook(account, + if(account.isGroupAccount) Nil else getGroupsByUserName(userName), + getAccountWebHooks(account.userName) + ) + // Repositories case _ => { val members = getGroupMembers(account.userName) @@ -259,6 +307,106 @@ redirect(s"/${userName}/_application") }) + /** + * Display the account web hook edit page. + */ + get("/:userName/_hooks/new")(oneselfOnly { + val userName = params("userName") + getAccountByUserName(userName).map { account => + val webhook = AccountWebHook(userName, "", WebHookContentType.FORM, None) + html.edithooks(webhook, Set(WebHook.Push), account, if (account.isGroupAccount) Nil else getGroupsByUserName(userName), flash.get("info"), true) + } + }) + + /** + * Add the account web hook URL. + */ + post("/:userName/_hooks/new", accountWebHookForm(false))(oneselfOnly { form => + val userName = params("userName") + addAccountWebHook(userName, form.url, form.events, form.ctype, form.token) + flash += "info" -> s"Webhook ${form.url} created" + redirect(s"/${userName}?tab=webhooks") + }) + + /** + * Delete the account web hook URL. + */ + get("/:userName/_hooks/delete")(oneselfOnly { + val userName = params("userName") + deleteAccountWebHook(userName, params("url")) + flash += "info" -> s"Webhook ${params("url")} deleted" + redirect(s"/${userName}?tab=webhooks") + }) + + /** + * Display the account web hook edit page. + */ + get("/:userName/_hooks/edit")(oneselfOnly { + val userName = params("userName") + getAccountByUserName(userName).map { account => + getAccountWebHook(userName, params("url")).map { case (webhook, events) => + html.edithooks(webhook, events, account, if (account.isGroupAccount) Nil else getGroupsByUserName(userName), flash.get("info"), false) + } getOrElse NotFound() + } + }) + + /** + * Update account web hook settings. + */ + post("/:userName/_hooks/edit", accountWebHookForm(true))(oneselfOnly { form => + val userName = params("userName") + updateAccountWebHook(userName, form.url, form.events, form.ctype, form.token) + flash += "info" -> s"webhook ${form.url} updated" + redirect(s"/${userName}?tab=webhooks") + }) + + /** + * Send the test request to registered account web hook URLs. + */ + ajaxPost("/:userName/_hooks/test")(oneselfOnly { + import scala.collection.JavaConverters._ + import scala.concurrent.duration._ + import scala.concurrent._ + import scala.util.control.NonFatal + import org.apache.http.util.EntityUtils + import scala.concurrent.ExecutionContext.Implicits.global + + def _headers(h: Array[org.apache.http.Header]): Array[Array[String]] = h.map { h => Array(h.getName, h.getValue) } + + val userName = params("userName") + val url = params("url") + val token = Some(params("token")) + val ctype = WebHookContentType.valueOf(params("ctype")) + val dummyWebHookInfo = RepositoryWebHook(userName, "dummy", url, ctype, token) + val dummyPayload = { + val ownerAccount = getAccountByUserName(userName).get + WebHookPushPayload.createDummyPayload(ownerAccount) + } + + val (webHook, json, reqFuture, resFuture) = callWebHook(WebHook.Push, List(dummyWebHookInfo), dummyPayload).head + + val toErrorMap: PartialFunction[Throwable, Map[String,String]] = { + case e: java.net.UnknownHostException => Map("error"-> ("Unknown host " + e.getMessage)) + case e: java.lang.IllegalArgumentException => Map("error"-> ("invalid url")) + case e: org.apache.http.client.ClientProtocolException => Map("error"-> ("invalid url")) + case NonFatal(e) => Map("error"-> (e.getClass + " "+ e.getMessage)) + } + + contentType = formats("json") + org.json4s.jackson.Serialization.write(Map( + "url" -> url, + "request" -> Await.result(reqFuture.map(req => Map( + "headers" -> _headers(req.getAllHeaders), + "payload" -> json + )).recover(toErrorMap), 20 seconds), + "responce" -> Await.result(resFuture.map(res => Map( + "status" -> res.getStatusLine(), + "body" -> EntityUtils.toString(res.getEntity()), + "headers" -> _headers(res.getAllHeaders()) + )).recover(toErrorMap), 20 seconds) + )) + }) + get("/register"){ if(context.settings.allowAccountRegistration){ if(context.loginAccount.isDefined){ diff --git a/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala b/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala index 0ca0305..2d911bb 100644 --- a/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala +++ b/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala @@ -1,7 +1,7 @@ package gitbucket.core.controller import gitbucket.core.settings.html -import gitbucket.core.model.WebHook +import gitbucket.core.model.{WebHook, RepositoryWebHook} import gitbucket.core.service._ import gitbucket.core.service.WebHookService._ import gitbucket.core.util._ @@ -215,7 +215,7 @@ * Display the web hook edit page. */ get("/:owner/:repository/settings/hooks/new")(ownerOnly { repository => - val webhook = WebHook(repository.owner, repository.name, "", WebHookContentType.FORM, None) + val webhook = RepositoryWebHook(repository.owner, repository.name, "", WebHookContentType.FORM, None) html.edithooks(webhook, Set(WebHook.Push), repository, flash.get("info"), true) }) @@ -254,7 +254,7 @@ val url = params("url") val token = Some(params("token")) val ctype = WebHookContentType.valueOf(params("ctype")) - val dummyWebHookInfo = WebHook(repository.owner, repository.name, url, ctype, token) + val dummyWebHookInfo = RepositoryWebHook(repository.owner, repository.name, url, ctype, token) val dummyPayload = { val ownerAccount = getAccountByUserName(repository.owner).get val commits = if(JGitUtil.isEmpty(git)) List.empty else git.log diff --git a/src/main/scala/gitbucket/core/model/AccountWebHook.scala b/src/main/scala/gitbucket/core/model/AccountWebHook.scala new file mode 100644 index 0000000..df28993 --- /dev/null +++ b/src/main/scala/gitbucket/core/model/AccountWebHook.scala @@ -0,0 +1,25 @@ +package gitbucket.core.model + +trait AccountWebHookComponent extends TemplateComponent { self: Profile => + import profile.api._ + + private implicit val whContentTypeColumnType = MappedColumnType.base[WebHookContentType, String](whct => whct.code , code => WebHookContentType.valueOf(code)) + + lazy val AccountWebHooks = TableQuery[AccountWebHooks] + + class AccountWebHooks(tag: Tag) extends Table[AccountWebHook](tag, "ACCOUNT_WEB_HOOK") with BasicTemplate { + val url = column[String]("URL") + val token = column[Option[String]]("TOKEN") + val ctype = column[WebHookContentType]("CTYPE") + def * = (userName, url, ctype, token) <> ((AccountWebHook.apply _).tupled, AccountWebHook.unapply) + + def byPrimaryKey(userName: String, url: String) = (this.userName === userName.bind) && (this.url === url.bind) + } +} + +case class AccountWebHook( + userName: String, + url: String, + ctype: WebHookContentType, + token: Option[String] +) extends WebHook diff --git a/src/main/scala/gitbucket/core/model/AccountWebHookEvent.scala b/src/main/scala/gitbucket/core/model/AccountWebHookEvent.scala new file mode 100644 index 0000000..36ffa3c --- /dev/null +++ b/src/main/scala/gitbucket/core/model/AccountWebHookEvent.scala @@ -0,0 +1,34 @@ +package gitbucket.core.model + +trait AccountWebHookEventComponent extends TemplateComponent { + self: Profile => + + import profile.api._ + import gitbucket.core.model.Profile.AccountWebHooks + + lazy val AccountWebHookEvents = TableQuery[AccountWebHookEvents] + + class AccountWebHookEvents(tag: Tag) extends Table[AccountWebHookEvent](tag, "ACCOUNT_WEB_HOOK_EVENT") with BasicTemplate { + val url = column[String]("URL") + val event = column[WebHook.Event]("EVENT") + + def * = (userName, url, event) <> ((AccountWebHookEvent.apply _).tupled, AccountWebHookEvent.unapply) + + def byAccountWebHook(userName: String, url: String) = (this.userName === userName.bind) && (this.url === url.bind) + + def byAccountWebHook(owner: Rep[String], url: Rep[String]) = + (this.userName === userName) && (this.url === url) + + def byAccountWebHook(webhook: AccountWebHooks) = + (this.userName === webhook.userName) && (this.url === webhook.url) + + def byPrimaryKey(userName: String, url: String, event: WebHook.Event) = + (this.userName === userName.bind) && (this.url === url.bind) && (this.event === event.bind) + } +} + +case class AccountWebHookEvent( + userName: String, + url: String, + event: WebHook.Event + ) diff --git a/src/main/scala/gitbucket/core/model/BasicTemplate.scala b/src/main/scala/gitbucket/core/model/BasicTemplate.scala index c3b8e1b..4f93d3a 100644 --- a/src/main/scala/gitbucket/core/model/BasicTemplate.scala +++ b/src/main/scala/gitbucket/core/model/BasicTemplate.scala @@ -7,6 +7,10 @@ val userName = column[String]("USER_NAME") val repositoryName = column[String]("REPOSITORY_NAME") + def byAccount(userName: String) = (this.userName === userName.bind) + + def byAccount(userName: Rep[String]) = (this.userName === userName) + def byRepository(owner: String, repository: String) = (userName === owner.bind) && (repositoryName === repository.bind) diff --git a/src/main/scala/gitbucket/core/model/Profile.scala b/src/main/scala/gitbucket/core/model/Profile.scala index 332e7ea..928d4e6 100644 --- a/src/main/scala/gitbucket/core/model/Profile.scala +++ b/src/main/scala/gitbucket/core/model/Profile.scala @@ -16,6 +16,11 @@ ) /** + * WebHookBase.Event Column Types + */ + implicit val eventColumnType = MappedColumnType.base[WebHook.Event, String](_.name, WebHook.Event.valueOf(_)) + + /** * Extends Column to add conditional condition */ implicit class RichColumn(c1: Rep[Boolean]){ @@ -51,8 +56,10 @@ with PullRequestComponent with RepositoryComponent with SshKeyComponent - with WebHookComponent - with WebHookEventComponent + with RepositoryWebHookComponent + with RepositoryWebHookEventComponent + with AccountWebHookComponent + with AccountWebHookEventComponent with ProtectedBranchComponent with DeployKeyComponent diff --git a/src/main/scala/gitbucket/core/model/RepositoryWebHook.scala b/src/main/scala/gitbucket/core/model/RepositoryWebHook.scala new file mode 100644 index 0000000..967d067 --- /dev/null +++ b/src/main/scala/gitbucket/core/model/RepositoryWebHook.scala @@ -0,0 +1,27 @@ +package gitbucket.core.model + +trait RepositoryWebHookComponent extends TemplateComponent { self: Profile => + import profile.api._ + + implicit val whContentTypeColumnType = MappedColumnType.base[WebHookContentType, String](whct => whct.code , code => WebHookContentType.valueOf(code)) + + lazy val RepositoryWebHooks = TableQuery[RepositoryWebHooks] + + class RepositoryWebHooks(tag: Tag) extends Table[RepositoryWebHook](tag, "WEB_HOOK") with BasicTemplate { + 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) + + def byPrimaryKey(owner: String, repository: String, url: String) = byRepository(owner, repository) && (this.url === url.bind) + } +} + + +case class RepositoryWebHook( + userName: String, + repositoryName: String, + url: String, + ctype: WebHookContentType, + token: Option[String] +) extends WebHook diff --git a/src/main/scala/gitbucket/core/model/RepositoryWebHookEvent.scala b/src/main/scala/gitbucket/core/model/RepositoryWebHookEvent.scala new file mode 100644 index 0000000..83cbea5 --- /dev/null +++ b/src/main/scala/gitbucket/core/model/RepositoryWebHookEvent.scala @@ -0,0 +1,28 @@ +package gitbucket.core.model + +trait RepositoryWebHookEventComponent extends TemplateComponent { self: Profile => + import profile.api._ + import gitbucket.core.model.Profile.RepositoryWebHooks + + lazy val RepositoryWebHookEvents = TableQuery[RepositoryWebHookEvents] + + class RepositoryWebHookEvents(tag: Tag) extends Table[RepositoryWebHookEvent](tag, "WEB_HOOK_EVENT") with BasicTemplate { + val url = column[String]("URL") + val event = column[WebHook.Event]("EVENT") + def * = (userName, repositoryName, url, event) <> ((RepositoryWebHookEvent.apply _).tupled, RepositoryWebHookEvent.unapply) + + def byRepositoryWebHook(owner: String, repository: String, url: String) = byRepository(owner, repository) && (this.url === url.bind) + def byRepositoryWebHook(owner: Rep[String], repository: Rep[String], url: Rep[String]) = + byRepository(userName, repositoryName) && (this.url === url) + def byRepositoryWebHook(webhook: RepositoryWebHooks) = + byRepository(webhook.userName, webhook.repositoryName) && (this.url === webhook.url) + def byPrimaryKey(owner: String, repository: String, url: String, event: WebHook.Event) = byRepositoryWebHook(owner, repository, url) && (this.event === event.bind) + } +} + +case class RepositoryWebHookEvent( + userName: String, + repositoryName: String, + url: String, + event: WebHook.Event +) diff --git a/src/main/scala/gitbucket/core/model/WebHook.scala b/src/main/scala/gitbucket/core/model/WebHook.scala index 48de21b..3643dfb 100644 --- a/src/main/scala/gitbucket/core/model/WebHook.scala +++ b/src/main/scala/gitbucket/core/model/WebHook.scala @@ -1,22 +1,5 @@ package gitbucket.core.model -trait WebHookComponent extends TemplateComponent { self: Profile => - import profile.api._ - - implicit val whContentTypeColumnType = MappedColumnType.base[WebHookContentType, String](whct => whct.code , code => WebHookContentType.valueOf(code)) - - lazy val WebHooks = TableQuery[WebHooks] - - class WebHooks(tag: Tag) extends Table[WebHook](tag, "WEB_HOOK") with BasicTemplate { - val url = column[String]("URL") - val token = column[Option[String]]("TOKEN") - val ctype = column[WebHookContentType]("CTYPE") - def * = (userName, repositoryName, url, ctype, token) <> ((WebHook.apply _).tupled, WebHook.unapply) - - def byPrimaryKey(owner: String, repository: String, url: String) = byRepository(owner, repository) && (this.url === url.bind) - } -} - abstract sealed case class WebHookContentType(code: String, ctype: String) object WebHookContentType { @@ -33,13 +16,11 @@ def valueOpt(code: String): Option[WebHookContentType] = map.get(code) } -case class WebHook( - userName: String, - repositoryName: String, - url: String, - ctype: WebHookContentType, - token: Option[String] -) +trait WebHook{ + val url: String + val ctype: WebHookContentType + val token: Option[String] +} object WebHook { abstract sealed class Event(val name: String) @@ -86,6 +67,7 @@ TeamAdd, Watch ) + private val map: Map[String,Event] = values.map(e => e.name -> e).toMap def valueOf(name: String): Event = map(name) def valueOpt(name: String): Option[Event] = map.get(name) diff --git a/src/main/scala/gitbucket/core/model/WebHookEvent.scala b/src/main/scala/gitbucket/core/model/WebHookEvent.scala deleted file mode 100644 index d9f5a55..0000000 --- a/src/main/scala/gitbucket/core/model/WebHookEvent.scala +++ /dev/null @@ -1,30 +0,0 @@ -package gitbucket.core.model - -trait WebHookEventComponent extends TemplateComponent { self: Profile => - import profile.api._ - import gitbucket.core.model.Profile.WebHooks - - lazy val WebHookEvents = TableQuery[WebHookEvents] - - implicit val typedType = MappedColumnType.base[WebHook.Event, String](_.name, WebHook.Event.valueOf(_)) - - class WebHookEvents(tag: Tag) extends Table[WebHookEvent](tag, "WEB_HOOK_EVENT") with BasicTemplate { - val url = column[String]("URL") - val event = column[WebHook.Event]("EVENT") - def * = (userName, repositoryName, url, event) <> ((WebHookEvent.apply _).tupled, WebHookEvent.unapply) - - def byWebHook(owner: String, repository: String, url: String) = byRepository(owner, repository) && (this.url === url.bind) - def byWebHook(owner: Rep[String], repository: Rep[String], url: Rep[String]) = - byRepository(userName, repositoryName) && (this.url === url) - def byWebHook(webhook: WebHooks) = - byRepository(webhook.userName, webhook.repositoryName) && (this.url === webhook.url) - def byPrimaryKey(owner: String, repository: String, url: String, event: WebHook.Event) = byWebHook(owner, repository, url) && (this.event === event.bind) - } -} - -case class WebHookEvent( - userName: String, - repositoryName: String, - url: String, - event: WebHook.Event -) diff --git a/src/main/scala/gitbucket/core/service/RepositoryService.scala b/src/main/scala/gitbucket/core/service/RepositoryService.scala index 9b5a1bf..cdb2cde 100644 --- a/src/main/scala/gitbucket/core/service/RepositoryService.scala +++ b/src/main/scala/gitbucket/core/service/RepositoryService.scala @@ -59,8 +59,8 @@ (Repositories filter { t => t.byRepository(oldUserName, oldRepositoryName) } firstOption).map { repository => Repositories insert repository.copy(userName = newUserName, repositoryName = newRepositoryName) - val webHooks = WebHooks .filter(_.byRepository(oldUserName, oldRepositoryName)).list - val webHookEvents = WebHookEvents .filter(_.byRepository(oldUserName, oldRepositoryName)).list + val webHooks = RepositoryWebHooks .filter(_.byRepository(oldUserName, oldRepositoryName)).list + val webHookEvents = RepositoryWebHookEvents .filter(_.byRepository(oldUserName, oldRepositoryName)).list val milestones = Milestones .filter(_.byRepository(oldUserName, oldRepositoryName)).list val issueId = IssueId .filter(_.byRepository(oldUserName, oldRepositoryName)).list val issues = Issues .filter(_.byRepository(oldUserName, oldRepositoryName)).list @@ -91,8 +91,8 @@ deleteRepository(oldUserName, oldRepositoryName) - WebHooks .insertAll(webHooks .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) - WebHookEvents.insertAll(webHookEvents .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) + RepositoryWebHooks .insertAll(webHooks .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) + RepositoryWebHookEvents.insertAll(webHookEvents .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) Milestones .insertAll(milestones .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) IssueId .insertAll(issueId .map(_.copy(_1 = newUserName, _2 = newRepositoryName)) :_*) @@ -166,8 +166,8 @@ Issues .filter(_.byRepository(userName, repositoryName)).delete IssueId .filter(_.byRepository(userName, repositoryName)).delete Milestones .filter(_.byRepository(userName, repositoryName)).delete - WebHooks .filter(_.byRepository(userName, repositoryName)).delete - WebHookEvents .filter(_.byRepository(userName, repositoryName)).delete + RepositoryWebHooks .filter(_.byRepository(userName, repositoryName)).delete + RepositoryWebHookEvents .filter(_.byRepository(userName, repositoryName)).delete Repositories .filter(_.byRepository(userName, repositoryName)).delete // Update ORIGIN_USER_NAME and ORIGIN_REPOSITORY_NAME diff --git a/src/main/scala/gitbucket/core/service/WebHookService.scala b/src/main/scala/gitbucket/core/service/WebHookService.scala index 2f060de..f58fc55 100644 --- a/src/main/scala/gitbucket/core/service/WebHookService.scala +++ b/src/main/scala/gitbucket/core/service/WebHookService.scala @@ -3,7 +3,7 @@ import fr.brouillard.oss.security.xhub.XHub import fr.brouillard.oss.security.xhub.XHub.{XHubConverter, XHubDigest} import gitbucket.core.api._ -import gitbucket.core.model.{Account, CommitComment, Issue, IssueComment, PullRequest, WebHook, WebHookEvent} +import gitbucket.core.model.{Account, CommitComment, Issue, IssueComment, PullRequest, WebHook, RepositoryWebHook, RepositoryWebHookEvent, AccountWebHook, AccountWebHookEvent} import gitbucket.core.model.Profile._ import gitbucket.core.model.Profile.profile.blockingApi._ import org.apache.http.client.utils.URLEncodedUtils @@ -32,45 +32,86 @@ private val logger = LoggerFactory.getLogger(classOf[WebHookService]) /** get All WebHook informations of repository */ - def getWebHooks(owner: String, repository: String)(implicit s: Session): List[(WebHook, Set[WebHook.Event])] = - WebHooks.filter(_.byRepository(owner, repository)) - .join(WebHookEvents).on { (w, t) => t.byWebHook(w) } + def getWebHooks(owner: String, repository: String)(implicit s: Session): List[(RepositoryWebHook, Set[WebHook.Event])] = + RepositoryWebHooks.filter(_.byRepository(owner, repository)) + .join(RepositoryWebHookEvents).on { (w, t) => t.byRepositoryWebHook(w) } .map { case (w, t) => w -> t.event } .list.groupBy(_._1).mapValues(_.map(_._2).toSet).toList.sortBy(_._1.url) /** get All WebHook informations of repository event */ - def getWebHooksByEvent(owner: String, repository: String, event: WebHook.Event)(implicit s: Session): List[WebHook] = - WebHooks.filter(_.byRepository(owner, repository)) - .join(WebHookEvents).on { (wh, whe) => whe.byWebHook(wh) } + def getWebHooksByEvent(owner: String, repository: String, event: WebHook.Event)(implicit s: Session): List[RepositoryWebHook] = + RepositoryWebHooks.filter(_.byRepository(owner, repository)) + .join(RepositoryWebHookEvents).on { (wh, whe) => whe.byRepositoryWebHook(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])] = - WebHooks + def getWebHook(owner: String, repository: String, url: String)(implicit s: Session): Option[(RepositoryWebHook, Set[WebHook.Event])] = + RepositoryWebHooks .filter(_.byPrimaryKey(owner, repository, url)) - .join(WebHookEvents).on { (w, t) => t.byWebHook(w) } + .join(RepositoryWebHookEvents).on { (w, t) => t.byRepositoryWebHook(w) } .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], ctype: WebHookContentType, token: Option[String])(implicit s: Session): Unit = { - WebHooks insert WebHook(owner, repository, url, ctype, token) + RepositoryWebHooks insert RepositoryWebHook(owner, repository, url, ctype, token) events.map { event: WebHook.Event => - WebHookEvents insert WebHookEvent(owner, repository, url, event) + RepositoryWebHookEvents insert RepositoryWebHookEvent(owner, repository, url, event) } } def updateWebHook(owner: String, repository: String, url :String, events: Set[WebHook.Event], ctype: WebHookContentType, token: Option[String])(implicit s: Session): Unit = { - WebHooks.filter(_.byPrimaryKey(owner, repository, url)).map(w => (w.ctype, w.token)).update((ctype, token)) - WebHookEvents.filter(_.byWebHook(owner, repository, url)).delete + RepositoryWebHooks.filter(_.byPrimaryKey(owner, repository, url)).map(w => (w.ctype, w.token)).update((ctype, token)) + RepositoryWebHookEvents.filter(_.byRepositoryWebHook(owner, repository, url)).delete events.map { event: WebHook.Event => - WebHookEvents insert WebHookEvent(owner, repository, url, event) + RepositoryWebHookEvents insert RepositoryWebHookEvent(owner, repository, url, event) } } def deleteWebHook(owner: String, repository: String, url :String)(implicit s: Session): Unit = - WebHooks.filter(_.byPrimaryKey(owner, repository, url)).delete + RepositoryWebHooks.filter(_.byPrimaryKey(owner, repository, url)).delete + + /** get All AccountWebHook informations of user */ + def getAccountWebHooks(owner: String)(implicit s: Session): List[(AccountWebHook, Set[WebHook.Event])] = + AccountWebHooks.filter(_.byAccount(owner)) + .join(AccountWebHookEvents).on { (w, t) => t.byAccountWebHook(w) } + .map { case (w, t) => w -> t.event } + .list.groupBy(_._1).mapValues(_.map(_._2).toSet).toList.sortBy(_._1.url) + + /** get All AccountWebHook informations of repository event */ + def getAccountWebHooksByEvent(owner: String, event: WebHook.Event)(implicit s: Session): List[AccountWebHook] = + AccountWebHooks.filter(_.byAccount(owner)) + .join(AccountWebHookEvents).on { (wh, whe) => whe.byAccountWebHook(wh) } + .filter { case (wh, whe) => whe.event === event.bind} + .map{ case (wh, whe) => wh } + .list.distinct + + /** get All AccountWebHook information from repository to url */ + def getAccountWebHook(owner: String, url: String)(implicit s: Session): Option[(AccountWebHook, Set[WebHook.Event])] = + AccountWebHooks + .filter(_.byPrimaryKey(owner, url)) + .join(AccountWebHookEvents).on { (w, t) => t.byAccountWebHook(w) } + .map { case (w, t) => w -> t.event } + .list.groupBy(_._1).mapValues(_.map(_._2).toSet).headOption + + def addAccountWebHook(owner: String, url :String, events: Set[WebHook.Event], ctype: WebHookContentType, token: Option[String])(implicit s: Session): Unit = { + AccountWebHooks insert AccountWebHook(owner, url, ctype, token) + events.map { event: WebHook.Event => + AccountWebHookEvents insert AccountWebHookEvent(owner, url, event) + } + } + + def updateAccountWebHook(owner: String, url :String, events: Set[WebHook.Event], ctype: WebHookContentType, token: Option[String])(implicit s: Session): Unit = { + AccountWebHooks.filter(_.byPrimaryKey(owner, url)).map(w => (w.ctype, w.token)).update((ctype, token)) + AccountWebHookEvents.filter(_.byAccountWebHook(owner, url)).delete + events.map { event: WebHook.Event => + AccountWebHookEvents insert AccountWebHookEvent(owner, url, event) + } + } + + def deleteAccountWebHook(owner: String, url :String)(implicit s: Session): Unit = + AccountWebHooks.filter(_.byPrimaryKey(owner, url)).delete def callWebHookOf(owner: String, repository: String, event: WebHook.Event)(makePayload: => Option[WebHookPayload]) (implicit s: Session, c: JsonFormat.Context): Unit = { @@ -78,6 +119,10 @@ if(webHooks.nonEmpty){ makePayload.map(callWebHook(event, webHooks, _)) } + val accountWebHooks = getAccountWebHooksByEvent(owner, event) + if(accountWebHooks.nonEmpty){ + makePayload.map(callWebHook(event, accountWebHooks, _)) + } } def callWebHook(event: WebHook.Event, webHooks: List[WebHook], payload: WebHookPayload) @@ -207,7 +252,7 @@ /** @return Map[(issue, issueUser, pullRequest, baseOwner, headOwner), webHooks] */ def getPullRequestsByRequestForWebhook(userName:String, repositoryName:String, branch:String) - (implicit s: Session): Map[(Issue, Account, PullRequest, Account, Account), List[WebHook]] = + (implicit s: Session): Map[(Issue, Account, PullRequest, Account, Account), List[RepositoryWebHook]] = (for{ is <- Issues if is.closed === false.bind pr <- PullRequests if pr.byPrimaryKey(is.userName, is.repositoryName, is.issueId) @@ -217,8 +262,8 @@ bu <- Accounts if bu.userName === pr.userName ru <- Accounts if ru.userName === pr.requestUserName iu <- Accounts if iu.userName === is.openedUserName - wh <- WebHooks if wh.byRepository(is.userName , is.repositoryName) - wht <- WebHookEvents if wht.event === WebHook.PullRequest.asInstanceOf[WebHook.Event].bind && wht.byWebHook(wh) + wh <- RepositoryWebHooks if wh.byRepository(is.userName , is.repositoryName) + wht <- RepositoryWebHookEvents if wht.event === WebHook.PullRequest.asInstanceOf[WebHook.Event].bind && wht.byRepositoryWebHook(wh) } yield { ((is, iu, pr, bu, ru), wh) }).list.groupBy(_._1).mapValues(_.map(_._2)) @@ -344,6 +389,17 @@ repositoryInfo, owner= ApiUser(repositoryOwner)) ) + + def createDummyPayload(sender: Account): WebHookPushPayload = + WebHookPushPayload( + pusher = ApiPusher(sender), + sender = ApiUser(sender), + ref = "refs/heads/master", + before = "adc83b19e793491b1c6ea0fd8b46cd9f32e592fc", + after = "adc83b19e793491b1c6ea0fd8b46cd9f32e592fc", + commits = List.empty, + repository = ApiRepository.forDummyPayload(ApiUser(sender)) + ) } // https://developer.github.com/v3/activity/events/types/#issuesevent diff --git a/src/main/twirl/gitbucket/core/account/edithooks.scala.html b/src/main/twirl/gitbucket/core/account/edithooks.scala.html new file mode 100644 index 0000000..4b33246 --- /dev/null +++ b/src/main/twirl/gitbucket/core/account/edithooks.scala.html @@ -0,0 +1,191 @@ +@(webHook: gitbucket.core.model.AccountWebHook, +events: Set[gitbucket.core.model.WebHook.Event], +account: gitbucket.core.model.Account, +groupNames: List[String], +info: Option[Any], +create: Boolean)(implicit context: gitbucket.core.controller.Context) +@import gitbucket.core.view.helpers +@import gitbucket.core.model.WebHook._ +@import gitbucket.core.model.WebHookContentType +@check(name: String, event: Event) = { +name="@(name).@event.name" value="on" @if(events(event)){checked} +} +@gitbucket.core.account.html.main(account, groupNames, "webhooks"){ +
+
Webhook / Manage webhook
+
+
+
+ +
+ +
+ @if(create){ + + } else { + + + } + +
+
+ +
+ +
+
+ +
+ +
+
+ +
+ +
+ + + + + + +
+ @if(!create){ + + + Delete webhook + + } else { + + } +
+
+
+
+ + +} diff --git a/src/main/twirl/gitbucket/core/account/main.scala.html b/src/main/twirl/gitbucket/core/account/main.scala.html index 4cdd5e5..747db11 100644 --- a/src/main/twirl/gitbucket/core/account/main.scala.html +++ b/src/main/twirl/gitbucket/core/account/main.scala.html @@ -43,6 +43,7 @@ } else { Public activity } + Webhooks @gitbucket.core.plugin.PluginRegistry().getProfileTabs.map { tab => @tab(account, context).map { link => @link.label diff --git a/src/main/twirl/gitbucket/core/account/webhook.scala.html b/src/main/twirl/gitbucket/core/account/webhook.scala.html new file mode 100644 index 0000000..2d08c57 --- /dev/null +++ b/src/main/twirl/gitbucket/core/account/webhook.scala.html @@ -0,0 +1,39 @@ +@(account: gitbucket.core.model.Account, +groupNames: List[String], +webHooks: List[(gitbucket.core.model.AccountWebHook, Set[gitbucket.core.model.WebHook.Event])])(implicit context: gitbucket.core.controller.Context) +@import gitbucket.core.view.helpers +@gitbucket.core.account.html.main(account, groupNames, "webhooks"){ +
+
+ Webhooks +
+
+

+ Webhooks allow external services to be notified when certain events happen within your repository. + When the specified events happen, we’ll send a POST request to each of the URLs you provide. + Learn more in GitBucket Wiki Webhook Page. +

+ Add webhook + + + @webHooks.map { case (webHook, events) => + + } +
+ + @webHook.url + + (@events.map(_.name).mkString(", ")) + + +
+
+
+}