Merge pull request #1488 from kounoike/pr-add-account-webhook

Add account(group/user)'s webhooks feature.
This commit is contained in:
Naoki Takezoe
2017-06-19 02:13:55 +09:00
committed by GitHub
19 changed files with 640 additions and 100 deletions

View File

@@ -11,4 +11,22 @@
<addPrimaryKey constraintName="IDX_DEPLOY_KEY_PK" tableName="DEPLOY_KEY" columnNames="USER_NAME, REPOSITORY_NAME, DEPLOY_KEY_ID"/>
<addForeignKeyConstraint constraintName="IDX_DEPLOY_KEY_FK0" baseTableName="DEPLOY_KEY" baseColumnNames="USER_NAME, REPOSITORY_NAME" referencedTableName="REPOSITORY" referencedColumnNames="USER_NAME, REPOSITORY_NAME"/>
<createTable tableName="ACCOUNT_WEB_HOOK">
<column name="USER_NAME" type="varchar(100)" nullable="false"/>
<column name="URL" type="varchar(200)" nullable="false"/>
<column name="TOKEN" type="varchar(100)" nullable="true"/>
<column name="CTYPE" type="varchar(10)" nullable="true"/>
</createTable>
<addPrimaryKey constraintName="IDX_ACCOUNT_WEB_HOOK_PK" tableName="ACCOUNT_WEB_HOOK" columnNames="USER_NAME, URL"/>
<addForeignKeyConstraint constraintName="IDX_ACCOUNT_WEB_HOOK_FK0" baseTableName="ACCOUNT_WEB_HOOK" baseColumnNames="USER_NAME" referencedTableName="ACCOUNT" referencedColumnNames="USER_NAME"/>
<createTable tableName="ACCOUNT_WEB_HOOK_EVENT">
<column name="USER_NAME" type="varchar(100)" nullable="false"/>
<column name="URL" type="varchar(200)" nullable="false"/>
<column name="EVENT" type="varchar(30)" nullable="false"/>
</createTable>
</changeSet>

View File

@@ -53,4 +53,14 @@ object ApiRepository{
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)
}

View File

@@ -2,9 +2,10 @@ package gitbucket.core.controller
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.apache.commons.io.FileUtils
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 @@ trait AccountControllerBase extends AccountManagementControllerBase {
"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 @@ trait AccountControllerBase extends AccountManagementControllerBase {
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)
@@ -273,6 +321,106 @@ trait AccountControllerBase extends AccountManagementControllerBase {
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){

View File

@@ -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._
@@ -221,7 +221,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
* 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)
})
@@ -260,7 +260,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
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

View File

@@ -1,6 +1,6 @@
package gitbucket.core.controller
import gitbucket.core.model.{WebHook, WebHookEvent}
import gitbucket.core.model.WebHook
import gitbucket.core.service.RepositoryService.RepositoryInfo
import gitbucket.core.service.WebHookService.WebHookGollumPayload
import gitbucket.core.wiki.html

View File

@@ -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

View File

@@ -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
)

View File

@@ -7,6 +7,10 @@ protected[model] trait TemplateComponent { self: Profile =>
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)

View File

@@ -15,6 +15,11 @@ trait Profile {
t => new java.util.Date(t.getTime)
)
/**
* WebHookBase.Event Column Types
*/
implicit val eventColumnType = MappedColumnType.base[WebHook.Event, String](_.name, WebHook.Event.valueOf(_))
/**
* Extends Column to add conditional condition
*/
@@ -52,8 +57,10 @@ trait CoreProfile extends ProfileProvider with Profile
with PullRequestComponent
with RepositoryComponent
with SshKeyComponent
with WebHookComponent
with WebHookEventComponent
with RepositoryWebHookComponent
with RepositoryWebHookEventComponent
with AccountWebHookComponent
with AccountWebHookEventComponent
with ProtectedBranchComponent
with DeployKeyComponent

View File

@@ -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

View File

@@ -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
)

View File

@@ -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 @@ object WebHookContentType {
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 @@ object WebHook {
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)

View File

@@ -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
)

View File

@@ -59,8 +59,8 @@ trait RepositoryService { self: AccountService =>
(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
@@ -93,8 +93,8 @@ trait RepositoryService { self: AccountService =>
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)) :_*)
Priorities .insertAll(priorities .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
IssueId .insertAll(issueId .map(_.copy(_1 = newUserName, _2 = newRepositoryName)) :_*)
@@ -170,8 +170,8 @@ trait RepositoryService { self: AccountService =>
Priorities .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
DeployKeys .filter(_.byRepository(userName, repositoryName)).delete
Repositories .filter(_.byRepository(userName, repositoryName)).delete

View File

@@ -3,7 +3,7 @@ package gitbucket.core.service
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 @@ trait WebHookService {
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) }
.filter { case (wh, whe) => whe.event === event.bind }
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 @@ trait WebHookService {
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 @@ trait WebHookPullRequestService extends WebHookService {
/** @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 @@ trait WebHookPullRequestService extends WebHookService {
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))
@@ -345,6 +390,17 @@ object WebHookService {
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

View File

@@ -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"){
<div class="panel panel-default">
<div class="panel-heading strong">Webhook / Manage webhook</div>
<div class="panel-body">
<form method="POST" validate="true">
<fieldset class="form-group">
<label class="strong">Payload URL</label>
<div>
<span class="error" id="error-url"></span>
</div>
@if(create){
<input type="text" name="url" id="url" value="@webHook.url" class="form-control" style="display: inline; width: 500px; vertical-align: middle;" required />
} else {
<input type="text" value="@webHook.url" class="form-control" style="display: inline; width: 500px; vertical-align: middle;" disabled />
<input type="hidden" value="@webHook.url" name="url" />
}
<button class="btn btn-default" id="test">Test Hook</button>
</fieldset>
<fieldset class="form-group">
<label class="strong">Content type</label>
<div></div>
<select name="ctype">
<option value="@WebHookContentType.FORM.code" @if(webHook.ctype == WebHookContentType.FORM){selected}>@WebHookContentType.FORM.ctype</option>
<option value="@WebHookContentType.JSON.code" @if(webHook.ctype == WebHookContentType.JSON){selected}>@WebHookContentType.JSON.ctype</option>
</select>
</fieldset>
<fieldset class="form-group">
<label class="strong">Security Token</label>
<div></div>
<input type="text" name="token" id="token" placeholder="leave blank for no X-Hub-Signature usage" value="@webHook.token" class="form-control" style="display: inline; width: 500px; vertical-align: middle;" />
</fieldset>
<hr />
<label class="strong">Which events would you like to trigger this webhook?</label>
<div>
<span class="error" id="error-events"></span>
</div>
<!--
<label class="checkbox"><input type="checkbox" @check("events",CommitComment) />Commit comment <small class="help-block">Commit or diff commented on. </small> </label>
<label class="checkbox"><input type="checkbox" @check("events",Create) />Create <small class="help-block">Branch, or tag created. </small> </label>
<label class="checkbox"><input type="checkbox" @check("events",Delete) />Delete <small class="help-block">Branch, or tag deleted. </small> </label>
<label class="checkbox"><input type="checkbox" @check("events",Deployment) />Deployment <small class="help-block">Repository deployed. </small> </label>
<label class="checkbox"><input type="checkbox" @check("events",DeploymentStatus) />Deployment status <small class="help-block">Deployment status updated from the API. </small> </label>
<label class="checkbox"><input type="checkbox" @check("events",Fork) />Fork <small class="help-block">Repository forked. </small> </label>
<label class="checkbox"><input type="checkbox" @check("events",Gollum) />Gollum <small class="help-block">Wiki page updated. </small> </label>
<label class="checkbox"><input type="checkbox" @check("events",Member) />Member <small class="help-block">Collaborator added to a non-organization repository. </small> </label>
<label class="checkbox"><input type="checkbox" @check("events",PageBuild) />Page build <small class="help-block">Pages site built. </small> </label>
<label class="checkbox"><input type="checkbox" @check("events",Public) />Public <small class="help-block">Repository changes from private to public. </small> </label>
<label class="checkbox"><input type="checkbox" @check("events",Release) />Release <small class="help-block">Release published in a repository. </small> </label>
<label class="checkbox"><input type="checkbox" @check("events",TeamAdd) />Team add <small class="help-block">Team added or modified on a repository. </small> </label>
<label class="checkbox"><input type="checkbox" @check("events",Watch) />Watch <small class="help-block">User stars a repository.</small></label>
<label class="checkbox"><input type="checkbox" @check("events",Status) />Status <small class="help-block">Commit status updated from the API. </small> </label>
-->
<label class="checkbox"><input type="checkbox" @check("events",IssueComment) />Issue comment <span class="help-block normal">Issue commented on. </span> </label>
<label class="checkbox"><input type="checkbox" @check("events",Issues) />Issues <span class="help-block normal">Issue opened, closed<!-- , assigned, or labeled -->. </span> </label>
<label class="checkbox"><input type="checkbox" @check("events",PullRequest) />Pull request <span class="help-block normal">Pull request opened, closed<!-- , assigned, labeled -->, or synchronized. </span> </label>
<label class="checkbox"><input type="checkbox" @check("events",PullRequestReviewComment) />Pull request review comment <span class="help-block normal">Pull request diff commented on. </span> </label>
<label class="checkbox"><input type="checkbox" @check("events",Push) />Push <span class="help-block normal">Git push to a repository. </span> </label>
<div class="text-right">
@if(!create){
<input type="submit" class="btn btn-success" value="Update webhook" formaction="@helpers.url(account.userName)/_hooks/edit" />
<a href="@helpers.url(account.userName)/_hooks/delete?url=@helpers.urlEncode(webHook.url)" class="btn btn-danger" onclick="return confirm('delete webhook for @webHook.url ?')">
Delete webhook
</a>
} else {
<input type="submit" class="btn btn-success" value="Add webhook" formaction="@helpers.url(account.userName)/_hooks/new" />
}
</div>
</form>
</div>
</div>
<div class="modal" id="test-report-modal" role="dialog" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h3>WebHook Test</h3>
</div>
<div class="modal-body" style="max-height: 300px; overflow: auto;">
<p>request to <span id="test-modal-url" style="word-break: break-all; word-wrap: break-word; white-space: pre; white-space: pre-wrap;"></span></p>
<div id="test-report" style="display:none">
<ul class="nav nav-tabs" id="test-report-tab">
<li class="active"><a href="#request">Request</a></li>
<li><a href="#response">Response <span class="badge badge-success" id="res-status"></span></a></li>
</ul>
<div class="tab-content">
<div class="tab-pane active" id="request">
<div id="req-errors" class="alert alert-error">
ERROR<span id="req-errors-body"></span>
</div>
<div id="req-success" style="display:none">
Headers
<pre id="req-headers"></pre>
Payload
<pre id="req-payload"></pre>
</div>
</div>
<div class="tab-pane" id="response">
<div id="res-errors" class="alert alert-error">
ERROR<span id="res-errors-body"></span>
</div>
<div id="res-success" style="display:none">
Headers
<pre id="res-headers"></pre>
Body
<pre id="res-body"></pre>
</div>
</div>
</div>
</div>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<script>
$(function(){
$('#test-report-tab a').click(function (e) {
e.preventDefault();
$(this).tab('show');
});
$('#test').click(function(e){
e.stopPropagation();
e.stopImmediatePropagation();
e.preventDefault();
var url = this.form.url.value;
var token = this.form.token.value;
var ctype = this.form.ctype.value;
if(!/^https?:\/\/.+/.test(url)){
alert("invalid url");
return;
}
$("#test-modal-url").text(url)
$("#test-report-modal").modal('show')
$("#test-report").hide();
var targetUrl = '@helpers.url(account.userName)/_hooks/test?url=' + encodeURIComponent(url) + '&ctype=' + ctype + '&token=';
if (token) {
targetUrl = targetUrl + encodeURIComponent(token);
}
$.ajax({
method:'POST',
url:targetUrl,
success: function(e){
//console.log(e);
$('#test-report-tab a:first').tab('show');
$("#test-report").show();
$("#req-success").toggle(e.request&&!e.request.error);
$("#req-errors").toggle(e.request&&!!e.request.error);
$("#req-errors-body").text(e.request.error);
function headers(h){
h = h["headers"];
return h ? $.map(h, function(h){
return $("<div>").append($('<b>').text(h[0] + ":"),$('<span>').text(" " + h[1]))
}):"";
}
$("#req-headers").html(headers(e.request));
$("#req-payload").text(e.request && e.request.payload ? JSON.stringify(JSON.parse(e.request.payload),undefined,4) : "");
$("#res-success").toggle(e.responce && !e.responce.error);
$("#res-errors").toggle(e.responce && !!e.responce.error);
$("#res-errors-body").text(e.responce.error);
var success = !!(e.responce && e.responce.status && /^2\d\d$/.test(e.responce.status.statusCode));
$("#res-status").text((e.responce && e.responce.status && e.responce.status.statusCode) || "ERROR");
$("#res-status").toggleClass("badge-success", success).toggleClass("badge-important", !success);
$("#res-headers").html(headers(e.responce));
$("#res-body").text(e.responce && e.responce.body ? e.responce.body : "");
},
error:function (e) {
if(e) {
console.log(e.responseText, e);
alert("request error ( http status " + e.status + " error on gitbugket or browser to gitbucket. show details on your javascript console )");
}else{
alert("unknown javascript error (please report to gitbucket team)");
}
$("#test-report-modal").modal('hide')
}
});
return false;
});
})
</script>
}

View File

@@ -43,6 +43,7 @@
} else {
<li@if(active == "activity"){ class="active"}><a href="@helpers.url(account.userName)?tab=activity">Public activity</a></li>
}
<li@if(active == "webhooks"){ class="active"}><a href="@helpers.url(account.userName)?tab=webhooks">Webhooks</a></li>
@gitbucket.core.plugin.PluginRegistry().getProfileTabs.map { tab =>
@tab(account, context).map { link =>
<li@if(active == link.id){ class="active"}><a href="@context.path/@link.path">@link.label</a></li>

View File

@@ -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"){
<div class="panel panel-default">
<div class="panel-heading strong">
Webhooks
</div>
<div class="panel-body">
<p>
Webhooks allow external services to be notified when certain events happen within your repository.
When the specified events happen, well send a POST request to each of the URLs you provide.
Learn more in <a href="https://github.com/takezoe/gitbucket/wiki/API-WebHook" target="_blank">GitBucket Wiki Webhook Page</a>.
</p>
<a href="@helpers.url(account.userName)/_hooks/new" class="btn btn-success pull-right" style="margin-bottom: 10px;">Add webhook</a>
<table class="table table-condensed" style="margin-bottom:0px;">
@webHooks.map { case (webHook, events) =>
<tr><td style="vertical-align: middle;">
<a href="@helpers.url(account.userName)/_hooks/edit?url=@helpers.urlEncode(webHook.url)" class="css-truncate" style="max-width:360px">
<span class="css-truncate-target">@webHook.url</span>
</a>
<em class="css-truncate" style="max-width: 225px;">(<span class="css-truncate-target">@events.map(_.name).mkString(", ")</span>)</em>
</td><td>
<div class="btn-group pull-right">
<a href="@helpers.url(account.userName)/_hooks/edit?url=@helpers.urlEncode(webHook.url)" class="btn btn-default">
<span class="octicon octicon-pencil"></span>
</a>
<a href="@helpers.url(account.userName)/_hooks/delete?url=@helpers.urlEncode(webHook.url)" class="btn btn-danger" onclick="return confirm('delete webhook for @webHook.url ?')">
<span class="octicon octicon-x"></span>
</a>
</div>
</td></tr>
}
</table>
</div>
</div>
}

View File

@@ -1,6 +1,6 @@
package gitbucket.core.service
import gitbucket.core.model.WebHook
import gitbucket.core.model.{WebHook, RepositoryWebHook}
import org.scalatest.FunSuite
import gitbucket.core.model.WebHookContentType
@@ -47,17 +47,17 @@ class WebHookServiceSpec extends FunSuite with ServiceSpecBase {
val formType = WebHookContentType.FORM
val jsonType = WebHookContentType.JSON
service.addWebHook("user1", "repo1", "http://example.com", Set(WebHook.PullRequest), formType, Some("key"))
assert(service.getWebHooks("user1", "repo1") == List((WebHook("user1","repo1","http://example.com", formType, Some("key")),Set(WebHook.PullRequest))))
assert(service.getWebHook("user1", "repo1", "http://example.com") == Some((WebHook("user1","repo1","http://example.com", formType, Some("key")),Set(WebHook.PullRequest))))
assert(service.getWebHooksByEvent("user1", "repo1", WebHook.PullRequest) == List((WebHook("user1","repo1","http://example.com", formType, Some("key")))))
assert(service.getWebHooks("user1", "repo1") == List((RepositoryWebHook("user1","repo1","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))))
assert(service.getWebHooksByEvent("user1", "repo1", WebHook.PullRequest) == List((RepositoryWebHook("user1","repo1","http://example.com", formType, Some("key")))))
assert(service.getWebHooksByEvent("user1", "repo1", WebHook.Push) == Nil)
assert(service.getWebHook("user1", "repo1", "http://example.com2") == None)
assert(service.getWebHook("user2", "repo1", "http://example.com") == None)
assert(service.getWebHook("user1", "repo2", "http://example.com") == None)
service.updateWebHook("user1", "repo1", "http://example.com", Set(WebHook.Push, WebHook.Issues), jsonType, Some("key"))
assert(service.getWebHook("user1", "repo1", "http://example.com") == Some((WebHook("user1","repo1","http://example.com", jsonType, Some("key")),Set(WebHook.Push, WebHook.Issues))))
assert(service.getWebHook("user1", "repo1", "http://example.com") == Some((RepositoryWebHook("user1","repo1","http://example.com", jsonType, Some("key")),Set(WebHook.Push, WebHook.Issues))))
assert(service.getWebHooksByEvent("user1", "repo1", WebHook.PullRequest) == Nil)
assert(service.getWebHooksByEvent("user1", "repo1", WebHook.Push) == List((WebHook("user1","repo1","http://example.com", jsonType, Some("key")))))
assert(service.getWebHooksByEvent("user1", "repo1", WebHook.Push) == List((RepositoryWebHook("user1","repo1","http://example.com", jsonType, Some("key")))))
service.deleteWebHook("user1", "repo1", "http://example.com")
assert(service.getWebHook("user1", "repo1", "http://example.com") == None)
} }
@@ -69,11 +69,11 @@ class WebHookServiceSpec extends FunSuite with ServiceSpecBase {
service.addWebHook("user1", "repo1", "http://example.com/2", Set(WebHook.Push), ctype, Some("key"))
service.addWebHook("user1", "repo1", "http://example.com/3", Set(WebHook.PullRequest,WebHook.Push), ctype, Some("key"))
assert(service.getWebHooks("user1", "repo1") == List(
WebHook("user1","repo1","http://example.com/1", ctype, Some("key"))->Set(WebHook.PullRequest),
WebHook("user1","repo1","http://example.com/2", ctype, Some("key"))->Set(WebHook.Push),
WebHook("user1","repo1","http://example.com/3", ctype, Some("key"))->Set(WebHook.PullRequest,WebHook.Push)))
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(WebHook.PullRequest,WebHook.Push)))
assert(service.getWebHooksByEvent("user1", "repo1", WebHook.PullRequest) == List(
WebHook("user1","repo1","http://example.com/1", ctype, Some("key")),
WebHook("user1","repo1","http://example.com/3", ctype, Some("key"))))
RepositoryWebHook("user1","repo1","http://example.com/1", ctype, Some("key")),
RepositoryWebHook("user1","repo1","http://example.com/3", ctype, Some("key"))))
} }
}
}