mirror of
https://github.com/gitbucket/gitbucket.git
synced 2025-11-08 14:35:52 +01:00
Merge branch 'purge-old-plugin-system'
This commit is contained in:
@@ -44,7 +44,6 @@ object MyBuild extends Build {
|
||||
"org.apache.sshd" % "apache-sshd" % "0.11.0",
|
||||
"com.typesafe.slick" %% "slick" % "2.1.0",
|
||||
"com.novell.ldap" % "jldap" % "2009-10-07",
|
||||
"org.quartz-scheduler" % "quartz" % "2.2.1",
|
||||
"com.h2database" % "h2" % "1.4.180",
|
||||
"ch.qos.logback" % "logback-classic" % "1.0.13" % "runtime",
|
||||
"org.eclipse.jetty" % "jetty-webapp" % "8.1.8.v20121106" % "container;provided",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import _root_.servlet.{PluginActionInvokeFilter, BasicAuthenticationFilter, TransactionFilter}
|
||||
import _root_.servlet.{BasicAuthenticationFilter, TransactionFilter}
|
||||
import app._
|
||||
//import jp.sf.amateras.scalatra.forms.ValidationJavaScriptProvider
|
||||
import org.scalatra._
|
||||
@@ -10,7 +10,6 @@ class ScalatraBootstrap extends LifeCycle {
|
||||
// Register TransactionFilter and BasicAuthenticationFilter at first
|
||||
context.addFilter("transactionFilter", new TransactionFilter)
|
||||
context.getFilterRegistration("transactionFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/*")
|
||||
context.addFilter("pluginActionInvokeFilter", new PluginActionInvokeFilter)
|
||||
context.getFilterRegistration("pluginActionInvokeFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/*")
|
||||
context.addFilter("basicAuthenticationFilter", new BasicAuthenticationFilter)
|
||||
context.getFilterRegistration("basicAuthenticationFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/git/*")
|
||||
|
||||
@@ -3,15 +3,8 @@ package app
|
||||
import service.{AccountService, SystemSettingsService}
|
||||
import SystemSettingsService._
|
||||
import util.AdminAuthenticator
|
||||
import util.Directory._
|
||||
import util.ControlUtil._
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
import ssh.SshServer
|
||||
import org.apache.commons.io.FileUtils
|
||||
import java.io.FileInputStream
|
||||
import plugin.{Plugin, PluginSystem}
|
||||
import org.scalatra.Ok
|
||||
import util.Implicits._
|
||||
|
||||
class SystemSettingsController extends SystemSettingsControllerBase
|
||||
with AccountService with AdminAuthenticator
|
||||
@@ -85,118 +78,4 @@ trait SystemSettingsControllerBase extends ControllerBase {
|
||||
redirect("/admin/system")
|
||||
})
|
||||
|
||||
get("/admin/plugins")(adminOnly {
|
||||
if(enablePluginSystem){
|
||||
val installedPlugins = plugin.PluginSystem.plugins
|
||||
val updatablePlugins = getAvailablePlugins(installedPlugins).filter(_.status == "updatable")
|
||||
admin.plugins.html.installed(installedPlugins, updatablePlugins)
|
||||
} else NotFound
|
||||
})
|
||||
|
||||
post("/admin/plugins/_update", pluginForm)(adminOnly { form =>
|
||||
if(enablePluginSystem){
|
||||
deletePlugins(form.pluginIds)
|
||||
installPlugins(form.pluginIds)
|
||||
redirect("/admin/plugins")
|
||||
} else NotFound
|
||||
})
|
||||
|
||||
post("/admin/plugins/_delete", pluginForm)(adminOnly { form =>
|
||||
if(enablePluginSystem){
|
||||
deletePlugins(form.pluginIds)
|
||||
redirect("/admin/plugins")
|
||||
} else NotFound
|
||||
})
|
||||
|
||||
get("/admin/plugins/available")(adminOnly {
|
||||
if(enablePluginSystem){
|
||||
val installedPlugins = plugin.PluginSystem.plugins
|
||||
val availablePlugins = getAvailablePlugins(installedPlugins).filter(_.status == "available")
|
||||
admin.plugins.html.available(availablePlugins)
|
||||
} else NotFound
|
||||
})
|
||||
|
||||
post("/admin/plugins/_install", pluginForm)(adminOnly { form =>
|
||||
if(enablePluginSystem){
|
||||
installPlugins(form.pluginIds)
|
||||
redirect("/admin/plugins")
|
||||
} else NotFound
|
||||
})
|
||||
|
||||
get("/admin/plugins/console")(adminOnly {
|
||||
if(enablePluginSystem){
|
||||
admin.plugins.html.console()
|
||||
} else NotFound
|
||||
})
|
||||
|
||||
post("/admin/plugins/console")(adminOnly {
|
||||
if(enablePluginSystem){
|
||||
val script = request.getParameter("script")
|
||||
val result = plugin.ScalaPlugin.eval(script)
|
||||
Ok()
|
||||
} else NotFound
|
||||
})
|
||||
|
||||
// TODO Move these methods to PluginSystem or Service?
|
||||
private def deletePlugins(pluginIds: List[String]): Unit = {
|
||||
pluginIds.foreach { pluginId =>
|
||||
plugin.PluginSystem.uninstall(pluginId)
|
||||
val dir = new java.io.File(PluginHome, pluginId)
|
||||
if(dir.exists && dir.isDirectory){
|
||||
FileUtils.deleteQuietly(dir)
|
||||
PluginSystem.uninstall(pluginId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def installPlugins(pluginIds: List[String]): Unit = {
|
||||
val dir = getPluginCacheDir()
|
||||
val installedPlugins = plugin.PluginSystem.plugins
|
||||
getAvailablePlugins(installedPlugins).filter(x => pluginIds.contains(x.id)).foreach { plugin =>
|
||||
val pluginDir = new java.io.File(PluginHome, plugin.id)
|
||||
if(pluginDir.exists){
|
||||
FileUtils.deleteDirectory(pluginDir)
|
||||
}
|
||||
FileUtils.copyDirectory(new java.io.File(dir, plugin.repository + "/" + plugin.id), pluginDir)
|
||||
PluginSystem.installPlugin(plugin.id)
|
||||
}
|
||||
}
|
||||
|
||||
private def getAvailablePlugins(installedPlugins: List[Plugin]): List[SystemSettingsControllerBase.AvailablePlugin] = {
|
||||
val repositoryRoot = getPluginCacheDir()
|
||||
|
||||
if(repositoryRoot.exists && repositoryRoot.isDirectory){
|
||||
PluginSystem.repositories.flatMap { repo =>
|
||||
val repoDir = new java.io.File(repositoryRoot, repo.id)
|
||||
if(repoDir.exists && repoDir.isDirectory){
|
||||
repoDir.listFiles.filter(d => d.isDirectory && !d.getName.startsWith(".")).map { plugin =>
|
||||
val propertyFile = new java.io.File(plugin, "plugin.properties")
|
||||
val properties = new java.util.Properties()
|
||||
if(propertyFile.exists && propertyFile.isFile){
|
||||
using(new FileInputStream(propertyFile)){ in =>
|
||||
properties.load(in)
|
||||
}
|
||||
}
|
||||
SystemSettingsControllerBase.AvailablePlugin(
|
||||
repository = repo.id,
|
||||
id = properties.getProperty("id"),
|
||||
version = properties.getProperty("version"),
|
||||
author = properties.getProperty("author"),
|
||||
url = properties.getProperty("url"),
|
||||
description = properties.getProperty("description"),
|
||||
status = installedPlugins.find(_.id == properties.getProperty("id")) match {
|
||||
case Some(x) if(PluginSystem.isUpdatable(x.version, properties.getProperty("version")))=> "updatable"
|
||||
case Some(x) => "installed"
|
||||
case None => "available"
|
||||
})
|
||||
}
|
||||
} else Nil
|
||||
}
|
||||
} else Nil
|
||||
}
|
||||
}
|
||||
|
||||
object SystemSettingsControllerBase {
|
||||
case class AvailablePlugin(repository: String, id: String, version: String,
|
||||
author: String, url: String, description: String, status: String)
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
package plugin
|
||||
|
||||
import plugin.PluginSystem._
|
||||
import java.sql.Connection
|
||||
|
||||
trait Plugin {
|
||||
val id: String
|
||||
val version: String
|
||||
val author: String
|
||||
val url: String
|
||||
val description: String
|
||||
|
||||
def repositoryMenus : List[RepositoryMenu]
|
||||
def globalMenus : List[GlobalMenu]
|
||||
def repositoryActions : List[RepositoryAction]
|
||||
def globalActions : List[Action]
|
||||
def javaScripts : List[JavaScript]
|
||||
}
|
||||
|
||||
object PluginConnectionHolder {
|
||||
val threadLocal = new ThreadLocal[Connection]
|
||||
}
|
||||
@@ -1,194 +0,0 @@
|
||||
package plugin
|
||||
|
||||
import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import util.Directory._
|
||||
import util.ControlUtil._
|
||||
import org.apache.commons.io.{IOUtils, FileUtils}
|
||||
import Security._
|
||||
import service.PluginService
|
||||
import model.Profile._
|
||||
import profile.simple._
|
||||
import java.io.FileInputStream
|
||||
import java.sql.Connection
|
||||
import app.Context
|
||||
import service.RepositoryService.RepositoryInfo
|
||||
|
||||
/**
|
||||
* Provides extension points to plug-ins.
|
||||
*/
|
||||
object PluginSystem extends PluginService {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(PluginSystem.getClass)
|
||||
|
||||
private val initialized = new AtomicBoolean(false)
|
||||
private val pluginsMap = scala.collection.mutable.Map[String, Plugin]()
|
||||
private val repositoriesList = scala.collection.mutable.ListBuffer[PluginRepository]()
|
||||
|
||||
def install(plugin: Plugin): Unit = {
|
||||
pluginsMap.put(plugin.id, plugin)
|
||||
}
|
||||
|
||||
def plugins: List[Plugin] = pluginsMap.values.toList
|
||||
|
||||
def uninstall(id: String)(implicit session: Session): Unit = {
|
||||
pluginsMap.remove(id)
|
||||
|
||||
// Delete from PLUGIN table
|
||||
deletePlugin(id)
|
||||
|
||||
// Drop tables
|
||||
val pluginDir = new java.io.File(PluginHome)
|
||||
val sqlFile = new java.io.File(pluginDir, s"${id}/sql/drop.sql")
|
||||
if(sqlFile.exists){
|
||||
val sql = IOUtils.toString(new FileInputStream(sqlFile), "UTF-8")
|
||||
using(session.conn.createStatement()){ stmt =>
|
||||
stmt.executeUpdate(sql)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def repositories: List[PluginRepository] = repositoriesList.toList
|
||||
|
||||
/**
|
||||
* Initializes the plugin system. Load scripts from GITBUCKET_HOME/plugins.
|
||||
*/
|
||||
def init()(implicit session: Session): Unit = {
|
||||
if(initialized.compareAndSet(false, true)){
|
||||
// Load installed plugins
|
||||
val pluginDir = new java.io.File(PluginHome)
|
||||
if(pluginDir.exists && pluginDir.isDirectory){
|
||||
pluginDir.listFiles.filter(f => f.isDirectory && !f.getName.startsWith(".")).foreach { dir =>
|
||||
installPlugin(dir.getName)
|
||||
}
|
||||
}
|
||||
// Add default plugin repositories
|
||||
repositoriesList += PluginRepository("central", "https://github.com/takezoe/gitbucket_plugins.git")
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Method name seems to not so good.
|
||||
def installPlugin(id: String)(implicit session: Session): Unit = {
|
||||
val pluginHome = new java.io.File(PluginHome)
|
||||
val pluginDir = new java.io.File(pluginHome, id)
|
||||
|
||||
val scalaFile = new java.io.File(pluginDir, "plugin.scala")
|
||||
if(scalaFile.exists && scalaFile.isFile){
|
||||
val properties = new java.util.Properties()
|
||||
using(new java.io.FileInputStream(new java.io.File(pluginDir, "plugin.properties"))){ in =>
|
||||
properties.load(in)
|
||||
}
|
||||
|
||||
val pluginId = properties.getProperty("id")
|
||||
val version = properties.getProperty("version")
|
||||
val author = properties.getProperty("author")
|
||||
val url = properties.getProperty("url")
|
||||
val description = properties.getProperty("description")
|
||||
|
||||
val source = s"""
|
||||
|val id = "${pluginId}"
|
||||
|val version = "${version}"
|
||||
|val author = "${author}"
|
||||
|val url = "${url}"
|
||||
|val description = "${description}"
|
||||
""".stripMargin + FileUtils.readFileToString(scalaFile, "UTF-8")
|
||||
|
||||
try {
|
||||
// Compile and eval Scala source code
|
||||
ScalaPlugin.eval(pluginDir.listFiles.filter(_.getName.endsWith(".scala.html")).map { file =>
|
||||
ScalaPlugin.compileTemplate(
|
||||
id.replace("-", ""),
|
||||
file.getName.stripSuffix(".scala.html"),
|
||||
IOUtils.toString(new FileInputStream(file)))
|
||||
}.mkString("\n") + source)
|
||||
|
||||
// Migrate database
|
||||
val plugin = getPlugin(pluginId)
|
||||
if(plugin.isEmpty){
|
||||
registerPlugin(model.Plugin(pluginId, version))
|
||||
migrate(session.conn, pluginId, "0.0")
|
||||
} else {
|
||||
updatePlugin(model.Plugin(pluginId, version))
|
||||
migrate(session.conn, pluginId, plugin.get.version)
|
||||
}
|
||||
} catch {
|
||||
case e: Throwable => logger.warn(s"Error in plugin loading for ${scalaFile.getAbsolutePath}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Should PluginSystem provide a way to migrate resources other than H2?
|
||||
private def migrate(conn: Connection, pluginId: String, current: String): Unit = {
|
||||
val pluginDir = new java.io.File(PluginHome)
|
||||
|
||||
// TODO Is ot possible to use this migration system in GitBucket migration?
|
||||
val dim = current.split("\\.")
|
||||
val currentVersion = Version(dim(0).toInt, dim(1).toInt)
|
||||
|
||||
val sqlDir = new java.io.File(pluginDir, s"${pluginId}/sql")
|
||||
if(sqlDir.exists && sqlDir.isDirectory){
|
||||
sqlDir.listFiles.filter(_.getName.endsWith(".sql")).map { file =>
|
||||
val array = file.getName.replaceFirst("\\.sql", "").split("_")
|
||||
Version(array(0).toInt, array(1).toInt)
|
||||
}
|
||||
.sorted.reverse.takeWhile(_ > currentVersion)
|
||||
.reverse.foreach { version =>
|
||||
val sqlFile = new java.io.File(pluginDir, s"${pluginId}/sql/${version.major}_${version.minor}.sql")
|
||||
val sql = IOUtils.toString(new FileInputStream(sqlFile), "UTF-8")
|
||||
using(conn.createStatement()){ stmt =>
|
||||
stmt.executeUpdate(sql)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case class Version(major: Int, minor: Int) extends Ordered[Version] {
|
||||
|
||||
override def compare(that: Version): Int = {
|
||||
if(major != that.major){
|
||||
major.compare(that.major)
|
||||
} else{
|
||||
minor.compare(that.minor)
|
||||
}
|
||||
}
|
||||
|
||||
def displayString: String = major + "." + minor
|
||||
}
|
||||
|
||||
def repositoryMenus : List[RepositoryMenu] = pluginsMap.values.flatMap(_.repositoryMenus).toList
|
||||
def globalMenus : List[GlobalMenu] = pluginsMap.values.flatMap(_.globalMenus).toList
|
||||
def repositoryActions : List[RepositoryAction] = pluginsMap.values.flatMap(_.repositoryActions).toList
|
||||
def globalActions : List[Action] = pluginsMap.values.flatMap(_.globalActions).toList
|
||||
def javaScripts : List[JavaScript] = pluginsMap.values.flatMap(_.javaScripts).toList
|
||||
|
||||
// Case classes to hold plug-ins information internally in GitBucket
|
||||
case class PluginRepository(id: String, url: String)
|
||||
case class GlobalMenu(label: String, url: String, icon: String, condition: Context => Boolean)
|
||||
case class RepositoryMenu(label: String, name: String, url: String, icon: String, condition: Context => Boolean)
|
||||
case class Action(method: String, path: String, security: Security, function: (HttpServletRequest, HttpServletResponse, Context) => Any)
|
||||
case class RepositoryAction(method: String, path: String, security: Security, function: (HttpServletRequest, HttpServletResponse, Context, RepositoryInfo) => Any)
|
||||
case class Button(label: String, href: String)
|
||||
case class JavaScript(filter: String => Boolean, script: String)
|
||||
|
||||
/**
|
||||
* Checks whether the plugin is updatable.
|
||||
*/
|
||||
def isUpdatable(oldVersion: String, newVersion: String): Boolean = {
|
||||
if(oldVersion == newVersion){
|
||||
false
|
||||
} else {
|
||||
val dim1 = oldVersion.split("\\.").map(_.toInt)
|
||||
val dim2 = newVersion.split("\\.").map(_.toInt)
|
||||
dim1.zip(dim2).foreach { case (a, b) =>
|
||||
if(a < b){
|
||||
return true
|
||||
} else if(a > b){
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
package plugin
|
||||
|
||||
import util.Directory._
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.quartz.{Scheduler, JobExecutionContext, Job}
|
||||
import org.quartz.JobBuilder._
|
||||
import org.quartz.TriggerBuilder._
|
||||
import org.quartz.SimpleScheduleBuilder._
|
||||
|
||||
class PluginUpdateJob extends Job {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(classOf[PluginUpdateJob])
|
||||
private var failedCount = 0
|
||||
|
||||
/**
|
||||
* Clone or pull all plugin repositories
|
||||
*
|
||||
* TODO Support plugin repository access through the proxy server
|
||||
*/
|
||||
override def execute(context: JobExecutionContext): Unit = {
|
||||
try {
|
||||
if(failedCount > 3){
|
||||
logger.error("Skip plugin information updating because failed count is over limit")
|
||||
} else {
|
||||
logger.info("Start plugin information updating")
|
||||
PluginSystem.repositories.foreach { repository =>
|
||||
logger.info(s"Updating ${repository.id}: ${repository.url}...")
|
||||
val dir = getPluginCacheDir()
|
||||
val repo = new java.io.File(dir, repository.id)
|
||||
if(repo.exists){
|
||||
// pull if the repository is already cloned
|
||||
Git.open(repo).pull().call()
|
||||
} else {
|
||||
// clone if the repository is not exist
|
||||
Git.cloneRepository().setURI(repository.url).setDirectory(repo).call()
|
||||
}
|
||||
}
|
||||
logger.info("End plugin information updating")
|
||||
}
|
||||
} catch {
|
||||
case e: Exception => {
|
||||
failedCount = failedCount + 1
|
||||
logger.error("Failed to update plugin information", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object PluginUpdateJob {
|
||||
|
||||
def schedule(scheduler: Scheduler): Unit = {
|
||||
val job = newJob(classOf[PluginUpdateJob])
|
||||
.withIdentity("pluginUpdateJob")
|
||||
.build()
|
||||
|
||||
val trigger = newTrigger()
|
||||
.withIdentity("pluginUpdateTrigger")
|
||||
.startNow()
|
||||
.withSchedule(simpleSchedule().withIntervalInHours(24).repeatForever())
|
||||
.build()
|
||||
|
||||
scheduler.scheduleJob(job, trigger)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
package plugin
|
||||
|
||||
import scala.collection.mutable.ListBuffer
|
||||
import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
|
||||
import app.Context
|
||||
import plugin.PluginSystem._
|
||||
import plugin.PluginSystem.RepositoryMenu
|
||||
import plugin.Security._
|
||||
import service.RepositoryService.RepositoryInfo
|
||||
import scala.reflect.runtime.currentMirror
|
||||
import scala.tools.reflect.ToolBox
|
||||
import play.twirl.compiler.TwirlCompiler
|
||||
import scala.io.Codec
|
||||
|
||||
// TODO This is a sample implementation for Scala based plug-ins.
|
||||
class ScalaPlugin(val id: String, val version: String,
|
||||
val author: String, val url: String, val description: String) extends Plugin {
|
||||
|
||||
private val repositoryMenuList = ListBuffer[RepositoryMenu]()
|
||||
private val globalMenuList = ListBuffer[GlobalMenu]()
|
||||
private val repositoryActionList = ListBuffer[RepositoryAction]()
|
||||
private val globalActionList = ListBuffer[Action]()
|
||||
private val javaScriptList = ListBuffer[JavaScript]()
|
||||
|
||||
def repositoryMenus : List[RepositoryMenu] = repositoryMenuList.toList
|
||||
def globalMenus : List[GlobalMenu] = globalMenuList.toList
|
||||
def repositoryActions : List[RepositoryAction] = repositoryActionList.toList
|
||||
def globalActions : List[Action] = globalActionList.toList
|
||||
def javaScripts : List[JavaScript] = javaScriptList.toList
|
||||
|
||||
def addRepositoryMenu(label: String, name: String, url: String, icon: String)(condition: (Context) => Boolean): Unit = {
|
||||
repositoryMenuList += RepositoryMenu(label, name, url, icon, condition)
|
||||
}
|
||||
|
||||
def addGlobalMenu(label: String, url: String, icon: String)(condition: (Context) => Boolean): Unit = {
|
||||
globalMenuList += GlobalMenu(label, url, icon, condition)
|
||||
}
|
||||
|
||||
def addGlobalAction(method: String, path: String, security: Security = All())(function: (HttpServletRequest, HttpServletResponse, Context) => Any): Unit = {
|
||||
globalActionList += Action(method, path, security, function)
|
||||
}
|
||||
|
||||
def addRepositoryAction(method: String, path: String, security: Security = All())(function: (HttpServletRequest, HttpServletResponse, Context, RepositoryInfo) => Any): Unit = {
|
||||
repositoryActionList += RepositoryAction(method, path, security, function)
|
||||
}
|
||||
|
||||
def addJavaScript(filter: String => Boolean, script: String): Unit = {
|
||||
javaScriptList += JavaScript(filter, script)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object ScalaPlugin {
|
||||
|
||||
def define(id: String, version: String, author: String, url: String, description: String)
|
||||
= new ScalaPlugin(id, version, author, url, description)
|
||||
|
||||
def eval(source: String): Any = {
|
||||
val toolbox = currentMirror.mkToolBox()
|
||||
val tree = toolbox.parse(source)
|
||||
toolbox.eval(tree)
|
||||
}
|
||||
|
||||
def compileTemplate(packageName: String, name: String, source: String): String = {
|
||||
val result = TwirlCompiler.parseAndGenerateCodeNewParser(
|
||||
Array(packageName, name),
|
||||
source.getBytes("UTF-8"),
|
||||
Codec(scala.util.Properties.sourceEncoding),
|
||||
"",
|
||||
"play.twirl.api.HtmlFormat.Appendable",
|
||||
"play.twirl.api.HtmlFormat",
|
||||
"",
|
||||
false)
|
||||
|
||||
result.replaceFirst("package .*", "")
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package plugin
|
||||
|
||||
/**
|
||||
* Defines enum case classes to specify permission for actions which is provided by plugin.
|
||||
*/
|
||||
object Security {
|
||||
|
||||
sealed trait Security
|
||||
|
||||
/**
|
||||
* All users and guests
|
||||
*/
|
||||
case class All() extends Security
|
||||
|
||||
/**
|
||||
* Only signed-in users
|
||||
*/
|
||||
case class Login() extends Security
|
||||
|
||||
/**
|
||||
* Only repository owner and collaborators
|
||||
*/
|
||||
case class Member() extends Security
|
||||
|
||||
/**
|
||||
* Only repository owner and managers of group repository
|
||||
*/
|
||||
case class Owner() extends Security
|
||||
|
||||
/**
|
||||
* Only administrators
|
||||
*/
|
||||
case class Admin() extends Security
|
||||
|
||||
}
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
import java.sql.PreparedStatement
|
||||
import play.twirl.api.Html
|
||||
import util.ControlUtil._
|
||||
import scala.collection.mutable.ListBuffer
|
||||
|
||||
package object plugin {
|
||||
|
||||
case class Redirect(path: String)
|
||||
case class Fragment(html: Html)
|
||||
case class RawData(contentType: String, content: Array[Byte])
|
||||
|
||||
object db {
|
||||
// TODO labelled place holder support
|
||||
def select(sql: String, params: Any*): Seq[Map[String, String]] = {
|
||||
defining(PluginConnectionHolder.threadLocal.get){ conn =>
|
||||
using(conn.prepareStatement(sql)){ stmt =>
|
||||
setParams(stmt, params: _*)
|
||||
using(stmt.executeQuery()){ rs =>
|
||||
val list = new ListBuffer[Map[String, String]]()
|
||||
while(rs.next){
|
||||
defining(rs.getMetaData){ meta =>
|
||||
val map = Range(1, meta.getColumnCount + 1).map { i =>
|
||||
val name = meta.getColumnName(i)
|
||||
(name, rs.getString(name))
|
||||
}.toMap
|
||||
list += map
|
||||
}
|
||||
}
|
||||
list
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO labelled place holder support
|
||||
def update(sql: String, params: Any*): Int = {
|
||||
defining(PluginConnectionHolder.threadLocal.get){ conn =>
|
||||
using(conn.prepareStatement(sql)){ stmt =>
|
||||
setParams(stmt, params: _*)
|
||||
stmt.executeUpdate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def setParams(stmt: PreparedStatement, params: Any*): Unit = {
|
||||
params.zipWithIndex.foreach { case (p, i) =>
|
||||
p match {
|
||||
case x: String => stmt.setString(i + 1, x)
|
||||
case x: Int => stmt.setInt(i + 1, x)
|
||||
case x: Boolean => stmt.setBoolean(i + 1, x)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -195,7 +195,7 @@ object SystemSettingsService {
|
||||
else value
|
||||
}
|
||||
|
||||
// TODO temporary flag
|
||||
val enablePluginSystem = Option(System.getProperty("enable.plugin")).getOrElse("false").toBoolean
|
||||
// // TODO temporary flag
|
||||
// val enablePluginSystem = Option(System.getProperty("enable.plugin")).getOrElse("false").toBoolean
|
||||
|
||||
}
|
||||
|
||||
@@ -11,8 +11,6 @@ import util.ControlUtil._
|
||||
import util.JDBCUtil._
|
||||
import org.eclipse.jgit.api.Git
|
||||
import util.Directory
|
||||
import plugin.PluginUpdateJob
|
||||
import service.SystemSettingsService
|
||||
|
||||
object AutoUpdate {
|
||||
|
||||
@@ -197,11 +195,10 @@ object AutoUpdate {
|
||||
* Update database schema automatically in the context initializing.
|
||||
*/
|
||||
class AutoUpdateListener extends ServletContextListener {
|
||||
import org.quartz.impl.StdSchedulerFactory
|
||||
import AutoUpdate._
|
||||
|
||||
private val logger = LoggerFactory.getLogger(classOf[AutoUpdateListener])
|
||||
private val scheduler = StdSchedulerFactory.getDefaultScheduler
|
||||
// private val scheduler = StdSchedulerFactory.getDefaultScheduler
|
||||
|
||||
override def contextInitialized(event: ServletContextEvent): Unit = {
|
||||
val dataDir = event.getServletContext.getInitParameter("gitbucket.home")
|
||||
@@ -236,31 +233,9 @@ class AutoUpdateListener extends ServletContextListener {
|
||||
}
|
||||
logger.debug("End schema update")
|
||||
}
|
||||
|
||||
if(SystemSettingsService.enablePluginSystem){
|
||||
getDatabase(context).withSession { implicit session =>
|
||||
logger.debug("Starting plugin system...")
|
||||
try {
|
||||
plugin.PluginSystem.init()
|
||||
|
||||
scheduler.start()
|
||||
PluginUpdateJob.schedule(scheduler)
|
||||
logger.debug("PluginUpdateJob is started.")
|
||||
|
||||
logger.debug("Plugin system is initialized.")
|
||||
} catch {
|
||||
case ex: Throwable => {
|
||||
logger.error("Failed to initialize plugin system", ex)
|
||||
ex.printStackTrace()
|
||||
throw ex
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def contextDestroyed(sce: ServletContextEvent): Unit = {
|
||||
scheduler.shutdown()
|
||||
}
|
||||
|
||||
private def getConnection(servletContext: ServletContext): Connection =
|
||||
|
||||
@@ -1,192 +0,0 @@
|
||||
package servlet
|
||||
|
||||
import javax.servlet._
|
||||
import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
|
||||
import org.apache.commons.io.IOUtils
|
||||
import play.twirl.api.Html
|
||||
import service.{AccountService, RepositoryService, SystemSettingsService}
|
||||
import model.{Account, Session}
|
||||
import util.{JGitUtil, Keys}
|
||||
import plugin.{RawData, Fragment, PluginConnectionHolder, Redirect}
|
||||
import service.RepositoryService.RepositoryInfo
|
||||
import plugin.Security._
|
||||
|
||||
class PluginActionInvokeFilter extends Filter with SystemSettingsService with RepositoryService with AccountService {
|
||||
|
||||
def init(config: FilterConfig) = {}
|
||||
|
||||
def destroy(): Unit = {}
|
||||
|
||||
def doFilter(req: ServletRequest, res: ServletResponse, chain: FilterChain): Unit = {
|
||||
(req, res) match {
|
||||
case (request: HttpServletRequest, response: HttpServletResponse) => {
|
||||
Database(req.getServletContext) withTransaction { implicit session =>
|
||||
val path = request.getRequestURI.substring(request.getServletContext.getContextPath.length)
|
||||
if(!processGlobalAction(path, request, response) && !processRepositoryAction(path, request, response)){
|
||||
chain.doFilter(req, res)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def processGlobalAction(path: String, request: HttpServletRequest, response: HttpServletResponse)
|
||||
(implicit session: Session): Boolean = {
|
||||
plugin.PluginSystem.globalActions.find(x =>
|
||||
x.method.toLowerCase == request.getMethod.toLowerCase && path.matches(x.path)
|
||||
).map { action =>
|
||||
val loginAccount = request.getSession.getAttribute(Keys.Session.LoginAccount).asInstanceOf[Account]
|
||||
val systemSettings = loadSystemSettings()
|
||||
implicit val context = app.Context(systemSettings, Option(loginAccount), request)
|
||||
|
||||
if(authenticate(action.security, context)){
|
||||
val result = try {
|
||||
PluginConnectionHolder.threadLocal.set(session.conn)
|
||||
action.function(request, response, context)
|
||||
} finally {
|
||||
PluginConnectionHolder.threadLocal.remove()
|
||||
}
|
||||
processActionResult(result, request, response, context)
|
||||
} else {
|
||||
// TODO NotFound or Error?
|
||||
}
|
||||
true
|
||||
} getOrElse false
|
||||
}
|
||||
|
||||
private def processRepositoryAction(path: String, request: HttpServletRequest, response: HttpServletResponse)
|
||||
(implicit session: Session): Boolean = {
|
||||
val elements = path.split("/")
|
||||
if(elements.length > 3){
|
||||
val owner = elements(1)
|
||||
val name = elements(2)
|
||||
val remain = elements.drop(3).mkString("/", "/", "")
|
||||
|
||||
val loginAccount = request.getSession.getAttribute(Keys.Session.LoginAccount).asInstanceOf[Account]
|
||||
val systemSettings = loadSystemSettings()
|
||||
implicit val context = app.Context(systemSettings, Option(loginAccount), request)
|
||||
|
||||
getRepository(owner, name, systemSettings.baseUrl(request)).flatMap { repository =>
|
||||
plugin.PluginSystem.repositoryActions.find(x => remain.matches(x.path)).map { action =>
|
||||
if(authenticate(action.security, context, repository)){
|
||||
val result = try {
|
||||
PluginConnectionHolder.threadLocal.set(session.conn)
|
||||
action.function(request, response, context, repository)
|
||||
} finally {
|
||||
PluginConnectionHolder.threadLocal.remove()
|
||||
}
|
||||
processActionResult(result, request, response, context)
|
||||
} else {
|
||||
// TODO NotFound or Error?
|
||||
}
|
||||
true
|
||||
}
|
||||
} getOrElse false
|
||||
} else false
|
||||
}
|
||||
|
||||
private def processActionResult(result: Any, request: HttpServletRequest, response: HttpServletResponse,
|
||||
context: app.Context): Unit = {
|
||||
result match {
|
||||
case null|None => renderError(request, response, context, 404)
|
||||
case x: String => renderGlobalHtml(request, response, context, x)
|
||||
case Some(x: String) => renderGlobalHtml(request, response, context, x)
|
||||
case x: Html => renderGlobalHtml(request, response, context, x.toString)
|
||||
case Some(x: Html) => renderGlobalHtml(request, response, context, x.toString)
|
||||
case x: Fragment => renderFragmentHtml(request, response, context, x.html.toString)
|
||||
case Some(x: Fragment) => renderFragmentHtml(request, response, context, x.html.toString)
|
||||
case x: RawData => renderRawData(request, response, context, x)
|
||||
case Some(x: RawData) => renderRawData(request, response, context, x)
|
||||
case x: Redirect => response.sendRedirect(x.path)
|
||||
case Some(x: Redirect) => response.sendRedirect(x.path)
|
||||
case x: AnyRef => renderJson(request, response, x)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Authentication for global action
|
||||
*/
|
||||
private def authenticate(security: Security, context: app.Context)(implicit session: Session): Boolean = {
|
||||
// Global Action
|
||||
security match {
|
||||
case All() => true
|
||||
case Login() => context.loginAccount.isDefined
|
||||
case Admin() => context.loginAccount.exists(_.isAdmin)
|
||||
case _ => false // TODO throw Exception?
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate for repository action
|
||||
*/
|
||||
private def authenticate(security: Security, context: app.Context, repository: RepositoryInfo)(implicit session: Session): Boolean = {
|
||||
if(repository.repository.isPrivate){
|
||||
// Private Repository
|
||||
security match {
|
||||
case Admin() => context.loginAccount.exists(_.isAdmin)
|
||||
case Owner() => context.loginAccount.exists { account =>
|
||||
account.userName == repository.owner ||
|
||||
getGroupMembers(repository.owner).exists(m => m.userName == account.userName && m.isManager)
|
||||
}
|
||||
case _ => context.loginAccount.exists { account =>
|
||||
account.isAdmin || account.userName == repository.owner ||
|
||||
getCollaborators(repository.owner, repository.name).contains(account.userName)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Public Repository
|
||||
security match {
|
||||
case All() => true
|
||||
case Login() => context.loginAccount.isDefined
|
||||
case Owner() => context.loginAccount.exists { account =>
|
||||
account.userName == repository.owner ||
|
||||
getGroupMembers(repository.owner).exists(m => m.userName == account.userName && m.isManager)
|
||||
}
|
||||
case Member() => context.loginAccount.exists { account =>
|
||||
account.userName == repository.owner ||
|
||||
getCollaborators(repository.owner, repository.name).contains(account.userName)
|
||||
}
|
||||
case Admin() => context.loginAccount.exists(_.isAdmin)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def renderError(request: HttpServletRequest, response: HttpServletResponse, context: app.Context, error: Int): Unit = {
|
||||
response.sendError(error)
|
||||
}
|
||||
|
||||
private def renderGlobalHtml(request: HttpServletRequest, response: HttpServletResponse, context: app.Context, body: String): Unit = {
|
||||
response.setContentType("text/html; charset=UTF-8")
|
||||
val html = _root_.html.main("GitBucket", None)(Html(body))(context)
|
||||
IOUtils.write(html.toString.getBytes("UTF-8"), response.getOutputStream)
|
||||
}
|
||||
|
||||
private def renderRepositoryHtml(request: HttpServletRequest, response: HttpServletResponse, context: app.Context, repository: RepositoryInfo, body: String): Unit = {
|
||||
response.setContentType("text/html; charset=UTF-8")
|
||||
val html = _root_.html.main("GitBucket", None)(_root_.html.menu("", repository)(Html(body))(context))(context) // TODO specify active side menu
|
||||
IOUtils.write(html.toString.getBytes("UTF-8"), response.getOutputStream)
|
||||
}
|
||||
|
||||
private def renderFragmentHtml(request: HttpServletRequest, response: HttpServletResponse, context: app.Context, body: String): Unit = {
|
||||
response.setContentType("text/html; charset=UTF-8")
|
||||
IOUtils.write(body.getBytes("UTF-8"), response.getOutputStream)
|
||||
}
|
||||
|
||||
private def renderRawData(request: HttpServletRequest, response: HttpServletResponse, context: app.Context, rawData: RawData): Unit = {
|
||||
response.setContentType(rawData.contentType)
|
||||
IOUtils.write(rawData.content, response.getOutputStream)
|
||||
}
|
||||
|
||||
private def renderJson(request: HttpServletRequest, response: HttpServletResponse, obj: AnyRef): Unit = {
|
||||
import org.json4s._
|
||||
import org.json4s.jackson.Serialization
|
||||
import org.json4s.jackson.Serialization.write
|
||||
implicit val formats = Serialization.formats(NoTypeHints)
|
||||
|
||||
val json = write(obj)
|
||||
|
||||
response.setContentType("application/json; charset=UTF-8")
|
||||
IOUtils.write(json.getBytes("UTF-8"), response.getOutputStream)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -11,11 +11,6 @@
|
||||
<li@if(active=="system"){ class="active"}>
|
||||
<a href="@path/admin/system">System Settings</a>
|
||||
</li>
|
||||
@if(service.SystemSettingsService.enablePluginSystem){
|
||||
<li@if(active=="plugins"){ class="active"}>
|
||||
<a href="@path/admin/plugins">Plugins</a>
|
||||
</li>
|
||||
}
|
||||
<li>
|
||||
<a href="@path/console/login.jsp">H2 Console</a>
|
||||
</li>
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
@(plugins: List[app.SystemSettingsControllerBase.AvailablePlugin])(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@html.main("Plugins"){
|
||||
@admin.html.menu("plugins"){
|
||||
@tab("available")
|
||||
<form action="@path/admin/plugins/_install" method="POST" validate="true">
|
||||
<table class="table table-bordered">
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Version</th>
|
||||
<th>Provider</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
@plugins.zipWithIndex.map { case (plugin, i) =>
|
||||
<tr>
|
||||
<td>
|
||||
<input type="checkbox" name="pluginId[@i]" value="@plugin.id"/>
|
||||
@plugin.id
|
||||
</td>
|
||||
<td>@plugin.version</td>
|
||||
<td><a href="@plugin.url">@plugin.author</a></td>
|
||||
<td>@plugin.description</td>
|
||||
</tr>
|
||||
}
|
||||
</table>
|
||||
<input type="submit" id="install-plugins" class="btn btn-success" value="Install selected plugins"/>
|
||||
</form>
|
||||
}
|
||||
}
|
||||
<script>
|
||||
$(function(){
|
||||
$('#install-plugins').click(function(){
|
||||
return confirm('Selected plugin will be installed. Are you sure?');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -1,37 +0,0 @@
|
||||
@()(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@html.main("JavaScript Console"){
|
||||
@admin.html.menu("plugins"){
|
||||
@tab("console")
|
||||
<form method="POST">
|
||||
<div class="box">
|
||||
<div class="box-header">JavaScript Console</div>
|
||||
<div class="box-content">
|
||||
<div id="editor" style="width: 100%; height: 400px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<fieldset>
|
||||
<input type="button" id="evaluate" class="btn btn-success" value="Evaluate"/>
|
||||
</fieldset>
|
||||
</form>
|
||||
}
|
||||
}
|
||||
<script src="@assets/vendors/ace/ace.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script>
|
||||
$(function(){
|
||||
var editor = ace.edit("editor");
|
||||
editor.setTheme("ace/theme/monokai");
|
||||
editor.getSession().setMode("ace/mode/javascript");
|
||||
|
||||
$('#evaluate').click(function(){
|
||||
$.post('@path/admin/plugins/console', {
|
||||
script: editor.getValue()
|
||||
}, function(data){
|
||||
alert('Success: ' + data);
|
||||
}).fail(function(error){
|
||||
alert(error.statusText);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -1,47 +0,0 @@
|
||||
@(plugins: List[plugin.Plugin],
|
||||
updatablePlugins: List[app.SystemSettingsControllerBase.AvailablePlugin])(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@html.main("Plugins"){
|
||||
@admin.html.menu("plugins"){
|
||||
@tab("installed")
|
||||
<form method="POST" validate="true">
|
||||
<table class="table table-bordered">
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Version</th>
|
||||
<th>Provider</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
@plugins.zipWithIndex.map { case (plugin, i) =>
|
||||
<tr>
|
||||
<td>
|
||||
<input type="checkbox" name="pluginId[@i]" value="@plugin.id"/>
|
||||
@plugin.id
|
||||
</td>
|
||||
<td>
|
||||
@plugin.version
|
||||
@updatablePlugins.find(_.id == plugin.id).map { x =>
|
||||
(@x.version is available)
|
||||
}
|
||||
</td>
|
||||
<td><a href="@plugin.url">@plugin.author</a></td>
|
||||
<td>@plugin.description</td>
|
||||
</tr>
|
||||
}
|
||||
</table>
|
||||
<input type="submit" id="update-plugins" class="btn btn-success" value="Update selected plugins" formaction="@path/admin/plugins/_update"/>
|
||||
<input type="submit" id="delete-plugins" class="btn btn-danger" value="Uninstall selected plugins" formaction="@path/admin/plugins/_delete"/>
|
||||
</form>
|
||||
}
|
||||
}
|
||||
<script>
|
||||
$(function(){
|
||||
$('#update-plugins').click(function(){
|
||||
return confirm('Selected plugin will be updated. Are you sure?');
|
||||
});
|
||||
$('#delete-plugins').click(function(){
|
||||
return confirm('Selected plugin will be removed permanently. Are you sure?');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -1,9 +0,0 @@
|
||||
@(active: String)(implicit context: app.Context)
|
||||
@import context._
|
||||
<ul class="nav nav-tabs">
|
||||
<li@if(active == "installed"){ class="active"}><a href="@path/admin/plugins">Installed plugins</a></li>
|
||||
<li@if(active == "available"){ class="active"}><a href="@path/admin/plugins/available">Available plugins</a></li>
|
||||
@*
|
||||
<li@if(active == "console" ){ class="active"}><a href="@path/admin/plugins/console">JavaScript console</a></li>
|
||||
*@
|
||||
</ul>
|
||||
@@ -62,21 +62,11 @@
|
||||
<li><a href="@path/groups/new">New group</a></li>
|
||||
</ul>
|
||||
<a href="@url(loginAccount.get.userName)/_edit" class="menu" data-toggle="tooltip" data-placement="bottom" title="Account settings"><i class="icon-user"></i></a>
|
||||
@plugin.PluginSystem.globalMenus.map { menu =>
|
||||
@if(menu.condition(context)){
|
||||
<a href="@menu.url" class="menu" data-toggle="tooltip" data-placement="bottom" title="@menu.label">@if(menu.icon.nonEmpty){<img src="@menu.icon" class="plugin-global-menu"/>} else {@menu.label}</a>
|
||||
}
|
||||
}
|
||||
@if(loginAccount.get.isAdmin){
|
||||
<a href="@path/admin/users" class="menu" data-toggle="tooltip" data-placement="bottom" title="Administration"><i class="icon-wrench"></i></a>
|
||||
}
|
||||
<a href="@path/signout" class="menu-last" data-toggle="tooltip" data-placement="bottom" title="Sign out"><i class="icon-share-alt"></i></a>
|
||||
} else {
|
||||
@plugin.PluginSystem.globalMenus.map { menu =>
|
||||
@if(menu.condition(context)){
|
||||
<a href="@menu.url" class="menu" data-toggle="tooltip" data-placement="bottom" title="@menu.label">@if(menu.icon.nonEmpty){<img src="@menu.icon" class="plugin-global-menu"/>} else {@menu.label}</a>
|
||||
}
|
||||
}
|
||||
<a href="@path/signin?redirect=@urlEncode(currentPath)" class="btn btn-last" id="signin">Sign in</a>
|
||||
}
|
||||
</div><!--/.nav-collapse -->
|
||||
@@ -90,9 +80,6 @@
|
||||
$('#search').submit(function(){
|
||||
return $.trim($(this).find('input[name=query]').val()) != '';
|
||||
});
|
||||
@plugin.PluginSystem.javaScripts.filter(_.filter(context.currentPath)).map { js =>
|
||||
@Html(js.script)
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -74,11 +74,6 @@
|
||||
@sidemenu("/issues", "issues", "Issues", repository.issueCount)
|
||||
@sidemenu("/pulls" , "pulls" , "Pull Requests", repository.pullCount)
|
||||
@sidemenu("/wiki" , "wiki" , "Wiki")
|
||||
@plugin.PluginSystem.repositoryMenus.map { menu =>
|
||||
@if(menu.condition(context)){
|
||||
@sidemenuPlugin(menu.url, menu.label, menu.label, menu.icon)
|
||||
}
|
||||
}
|
||||
@if(loginAccount.isDefined && (loginAccount.get.isAdmin || repository.managers.contains(loginAccount.get.userName))){
|
||||
@sidemenu("/settings", "settings", "Settings")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user