diff --git a/.travis.yml b/.travis.yml index 889f1e2..8eb0548 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,3 +16,23 @@ - $HOME/.coursier - $HOME/.embedmysql - $HOME/.embedpostgresql +matrix: + include: + - dist: trusty + group: edge + sudo: required + jdk: oraclejdk9 + script: + # https://github.com/sbt/sbt/pull/2951 + - git clone https://github.com/retronym/java9-rt-export + - cd java9-rt-export/ + - git checkout 1019a2873d057dd7214f4135e84283695728395d + - jdk_switcher use oraclejdk8 + - sbt package + - jdk_switcher use oraclejdk9 + - mkdir -p $HOME/.sbt/0.13/java9-rt-ext; java -jar target/java9-rt-export-*.jar $HOME/.sbt/0.13/java9-rt-ext/rt.jar + - jar tf $HOME/.sbt/0.13/java9-rt-ext/rt.jar | grep java/lang/Object + - cd .. + - echo "sbt.version=0.13.14-RC1" > project/build.properties + - wget https://raw.githubusercontent.com/paulp/sbt-extras/9ade5fa54914ca8aded44105bf4b9a60966f3ccd/sbt && chmod +x ./sbt + - ./sbt -Dscala.ext.dirs=$HOME/.sbt/0.13/java9-rt-ext test diff --git a/LICENSE b/LICENSE index b98f26a..8dada3e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,3 @@ - Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -179,7 +178,7 @@ APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" + boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a @@ -187,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2013-2016 GitBucket Team + Copyright {yyyy} {name of copyright owner} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/build.sbt b/build.sbt index d3532de..ed5078b 100644 --- a/build.sbt +++ b/build.sbt @@ -21,8 +21,8 @@ "amateras-snapshot" at "http://amateras.sourceforge.jp/mvn-snapshot/" ) libraryDependencies ++= Seq( - "org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "4.6.0.201612231935-r", - "org.eclipse.jgit" % "org.eclipse.jgit.archive" % "4.6.0.201612231935-r", + "org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "4.6.1.201703071140-r", + "org.eclipse.jgit" % "org.eclipse.jgit.archive" % "4.6.1.201703071140-r", "org.scalatra" %% "scalatra" % ScalatraVersion, "org.scalatra" %% "scalatra-json" % ScalatraVersion, "org.json4s" %% "json4s-jackson" % "3.5.0", @@ -54,12 +54,13 @@ "javax.servlet" % "javax.servlet-api" % "3.1.0" % "provided", "junit" % "junit" % "4.12" % "test", "org.scalatra" %% "scalatra-scalatest" % ScalatraVersion % "test", + "org.mockito" % "mockito-core" % "2.7.16" % "test", "com.wix" % "wix-embedded-mysql" % "2.1.4" % "test", "ru.yandex.qatools.embed" % "postgresql-embedded" % "1.14" % "test" ) // Compiler settings -scalacOptions := Seq("-deprecation", "-language:postfixOps", "-opt:_") +scalacOptions := Seq("-deprecation", "-language:postfixOps") javacOptions in compile ++= Seq("-target", "8", "-source", "8") javaOptions in Jetty += "-Dlogback.configurationFile=/logback-dev.xml" diff --git a/src/main/scala/gitbucket/core/api/ApiContents.scala b/src/main/scala/gitbucket/core/api/ApiContents.scala index 1d5753b..8b3735a 100644 --- a/src/main/scala/gitbucket/core/api/ApiContents.scala +++ b/src/main/scala/gitbucket/core/api/ApiContents.scala @@ -1,8 +1,9 @@ package gitbucket.core.api +import java.util.Base64 + import gitbucket.core.util.JGitUtil.FileInfo import gitbucket.core.util.RepositoryName -import org.apache.commons.codec.binary.Base64 case class ApiContents( `type`: String, @@ -20,7 +21,7 @@ ApiContents("dir", fileInfo.name, fileInfo.path, fileInfo.commitId, None, None)(repositoryName) } else { content.map(arr => - ApiContents("file", fileInfo.name, fileInfo.path, fileInfo.commitId, Some(Base64.encodeBase64String(arr)), Some("base64"))(repositoryName) + ApiContents("file", fileInfo.name, fileInfo.path, fileInfo.commitId, Some(Base64.getEncoder.encodeToString(arr)), Some("base64"))(repositoryName) ).getOrElse(ApiContents("file", fileInfo.name, fileInfo.path, fileInfo.commitId, None, None)(repositoryName)) } } diff --git a/src/main/scala/gitbucket/core/controller/AccountController.scala b/src/main/scala/gitbucket/core/controller/AccountController.scala index 19c553a..1394702 100644 --- a/src/main/scala/gitbucket/core/controller/AccountController.scala +++ b/src/main/scala/gitbucket/core/controller/AccountController.scala @@ -15,6 +15,7 @@ import org.apache.commons.io.FileUtils import org.scalatra.i18n.Messages import org.scalatra.BadRequest +import java.util.Date class AccountController extends AccountControllerBase @@ -149,11 +150,20 @@ get("/:userName/_avatar"){ val userName = params("userName") - getAccountByUserName(userName).flatMap(_.image).map { image => - RawData(FileUtil.getMimeType(image), new java.io.File(getUserUploadDir(userName), image)) - } getOrElse { - contentType = "image/png" - Thread.currentThread.getContextClassLoader.getResourceAsStream("noimage.png") + getAccountByUserName(userName).map{ account => + response.setDateHeader("Last-Modified", account.updatedDate.getTime) + account.image.map{ image => + RawData(FileUtil.getMimeType(image), new java.io.File(getUserUploadDir(userName), image)) + }.getOrElse{ + contentType = "image/png" + (if (account.isGroupAccount) { + TextAvatarUtil.textGroupAvatar(account.fullName) + } else { + TextAvatarUtil.textAvatar(account.fullName) + }).getOrElse(Thread.currentThread.getContextClassLoader.getResourceAsStream("noimage.png")) + } + }.getOrElse{ + NotFound() } } diff --git a/src/main/scala/gitbucket/core/model/DeployKey.scala b/src/main/scala/gitbucket/core/model/DeployKey.scala index 4f34e45..71b80a2 100644 --- a/src/main/scala/gitbucket/core/model/DeployKey.scala +++ b/src/main/scala/gitbucket/core/model/DeployKey.scala @@ -1,13 +1,11 @@ package gitbucket.core.model -trait DeployKeyComponent { self: Profile => +trait DeployKeyComponent extends TemplateComponent { self: Profile => import profile.api._ lazy val DeployKeys = TableQuery[DeployKeys] - class DeployKeys(tag: Tag) extends Table[DeployKey](tag, "DEPLOY_KEY") { - val userName = column[String]("USER_NAME") - val repositoryName = column[String]("REPOSITORY_NAME") + class DeployKeys(tag: Tag) extends Table[DeployKey](tag, "DEPLOY_KEY") with BasicTemplate { val deployKeyId = column[Int]("DEPLOY_KEY_ID", O AutoInc) val title = column[String]("TITLE") val publicKey = column[String]("PUBLIC_KEY") diff --git a/src/main/scala/gitbucket/core/plugin/PluginRegistory.scala b/src/main/scala/gitbucket/core/plugin/PluginRegistory.scala index 802ed10..41da276 100644 --- a/src/main/scala/gitbucket/core/plugin/PluginRegistory.scala +++ b/src/main/scala/gitbucket/core/plugin/PluginRegistory.scala @@ -3,6 +3,7 @@ import java.io.{File, FilenameFilter, InputStream} import java.net.URLClassLoader import java.nio.file.{Paths, StandardWatchEventKinds} +import java.util.Base64 import javax.servlet.ServletContext import gitbucket.core.controller.{Context, ControllerBase} @@ -17,7 +18,6 @@ import io.github.gitbucket.solidbase.Solidbase import io.github.gitbucket.solidbase.manager.JDBCVersionManager import io.github.gitbucket.solidbase.model.Module -import org.apache.commons.codec.binary.{Base64, StringUtils} import org.apache.commons.io.FileUtils import org.slf4j.LoggerFactory @@ -57,7 +57,7 @@ def getPlugins(): List[PluginInfo] = plugins.toList def addImage(id: String, bytes: Array[Byte]): Unit = { - val encoded = StringUtils.newStringUtf8(Base64.encodeBase64(bytes, false)) + val encoded = Base64.getEncoder.encodeToString(bytes) images += ((id, encoded)) } diff --git a/src/main/scala/gitbucket/core/service/RepositoryService.scala b/src/main/scala/gitbucket/core/service/RepositoryService.scala index 9b5a1bf..3640f1c 100644 --- a/src/main/scala/gitbucket/core/service/RepositoryService.scala +++ b/src/main/scala/gitbucket/core/service/RepositoryService.scala @@ -73,6 +73,7 @@ val collaborators = Collaborators .filter(_.byRepository(oldUserName, oldRepositoryName)).list val protectedBranches = ProtectedBranches .filter(_.byRepository(oldUserName, oldRepositoryName)).list val protectedBranchContexts = ProtectedBranchContexts.filter(_.byRepository(oldUserName, oldRepositoryName)).list + val deployKeys = DeployKeys .filter(_.byRepository(oldUserName, oldRepositoryName)).list Repositories.filter { t => (t.originUserName === oldUserName.bind) && (t.originRepositoryName === oldRepositoryName.bind) @@ -112,6 +113,7 @@ CommitStatuses .insertAll(commitStatuses.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) ProtectedBranches .insertAll(protectedBranches.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) ProtectedBranchContexts.insertAll(protectedBranchContexts.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) + DeployKeys .insertAll(deployKeys .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) // Update source repository of pull requests PullRequests.filter { t => @@ -126,11 +128,6 @@ userName = newUserName, repositoryName = newRepositoryName )) :_*) - IssueLabels.insertAll(issueLabels.map(x => x.copy( - labelId = newLabelMap(oldLabelMap(x.labelId)), - userName = newUserName, - repositoryName = newRepositoryName - )) :_*) // TODO Drop transfered owner from collaborators? Collaborators.insertAll(collaborators.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) @@ -168,6 +165,7 @@ Milestones .filter(_.byRepository(userName, repositoryName)).delete WebHooks .filter(_.byRepository(userName, repositoryName)).delete WebHookEvents .filter(_.byRepository(userName, repositoryName)).delete + DeployKeys .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/servlet/GitAuthenticationFilter.scala b/src/main/scala/gitbucket/core/servlet/GitAuthenticationFilter.scala index 1136787..7e0ba97 100644 --- a/src/main/scala/gitbucket/core/servlet/GitAuthenticationFilter.scala +++ b/src/main/scala/gitbucket/core/servlet/GitAuthenticationFilter.scala @@ -18,9 +18,9 @@ private val logger = LoggerFactory.getLogger(classOf[GitAuthenticationFilter]) def init(config: FilterConfig) = {} - + def destroy(): Unit = {} - + def doFilter(req: ServletRequest, res: ServletResponse, chain: FilterChain): Unit = { val request = req.asInstanceOf[HttpServletRequest] val response = res.asInstanceOf[HttpServletResponse] @@ -85,11 +85,16 @@ auth <- Option(request.getHeader("Authorization")) Array(username, password) = AuthUtil.decodeAuthHeader(auth).split(":", 2) account <- authenticate(settings, username, password) - } yield if (isUpdating || repository.repository.isPrivate) { + } yield if (isUpdating) { if (hasDeveloperRole(repository.owner, repository.name, Some(account))) { request.setAttribute(Keys.Request.UserName, account.userName) true } else false + } else if(repository.repository.isPrivate){ + if (hasGuestRole(repository.owner, repository.name, Some(account))) { + request.setAttribute(Keys.Request.UserName, account.userName) + true + } else false } else true passed.getOrElse(false) } @@ -114,4 +119,4 @@ action() } -} \ No newline at end of file +} diff --git a/src/main/scala/gitbucket/core/servlet/PluginAssetsServlet.scala b/src/main/scala/gitbucket/core/servlet/PluginAssetsServlet.scala index 0d43af3..6fe1ed3 100644 --- a/src/main/scala/gitbucket/core/servlet/PluginAssetsServlet.scala +++ b/src/main/scala/gitbucket/core/servlet/PluginAssetsServlet.scala @@ -26,6 +26,7 @@ val bytes = IOUtils.toByteArray(in) resp.setContentLength(bytes.length) resp.setContentType(FileUtil.getContentType(path, bytes)) + resp.setHeader("Cache-Control", "max-age=3600") resp.getOutputStream.write(bytes) } finally { in.close() diff --git a/src/main/scala/gitbucket/core/ssh/SshUtil.scala b/src/main/scala/gitbucket/core/ssh/SshUtil.scala index 42167ed..62eb97a 100644 --- a/src/main/scala/gitbucket/core/ssh/SshUtil.scala +++ b/src/main/scala/gitbucket/core/ssh/SshUtil.scala @@ -1,8 +1,8 @@ package gitbucket.core.ssh import java.security.PublicKey +import java.util.Base64 -import org.apache.commons.codec.binary.Base64 import org.apache.sshd.common.config.keys.KeyUtils import org.apache.sshd.common.util.buffer.ByteArrayBuffer import org.eclipse.jgit.lib.Constants @@ -22,7 +22,7 @@ } try { val encodedKey = parts(1) - val decode = Base64.decodeBase64(Constants.encodeASCII(encodedKey)) + val decode = Base64.getDecoder.decode(Constants.encodeASCII(encodedKey)) Some(new ByteArrayBuffer(decode).getRawPublicKey) } catch { case e: Throwable => diff --git a/src/main/scala/gitbucket/core/util/AuthUtil.scala b/src/main/scala/gitbucket/core/util/AuthUtil.scala index 7a83c13..5d937ca 100644 --- a/src/main/scala/gitbucket/core/util/AuthUtil.scala +++ b/src/main/scala/gitbucket/core/util/AuthUtil.scala @@ -1,5 +1,6 @@ package gitbucket.core.util +import java.util.Base64 import javax.servlet.http.HttpServletResponse /** @@ -13,9 +14,9 @@ def decodeAuthHeader(header: String): String = { try { - new String(new sun.misc.BASE64Decoder().decodeBuffer(header.substring(6))) + new String(Base64.getDecoder.decode(header.substring(6))) } catch { case _: Throwable => "" } } -} \ No newline at end of file +} diff --git a/src/main/scala/gitbucket/core/util/JGitUtil.scala b/src/main/scala/gitbucket/core/util/JGitUtil.scala index e15af9b..ac1a9b5 100644 --- a/src/main/scala/gitbucket/core/util/JGitUtil.scala +++ b/src/main/scala/gitbucket/core/util/JGitUtil.scala @@ -538,6 +538,7 @@ } else { // initial commit using(new TreeWalk(git.getRepository)){ treeWalk => + treeWalk.setRecursive(true) treeWalk.addTree(revCommit.getTree) val buffer = new scala.collection.mutable.ListBuffer[DiffInfo]() while(treeWalk.next){ diff --git a/src/main/scala/gitbucket/core/util/StringUtil.scala b/src/main/scala/gitbucket/core/util/StringUtil.scala index 587df35..d1eadf3 100644 --- a/src/main/scala/gitbucket/core/util/StringUtil.scala +++ b/src/main/scala/gitbucket/core/util/StringUtil.scala @@ -1,12 +1,12 @@ package gitbucket.core.util import java.net.{URLDecoder, URLEncoder} +import java.util.Base64 import org.mozilla.universalchardet.UniversalDetector import SyntaxSugars._ import org.apache.commons.io.input.BOMInputStream import org.apache.commons.io.IOUtils -import org.apache.commons.codec.binary.Base64 import scala.util.control.Exception._ @@ -34,14 +34,14 @@ val spec = new javax.crypto.spec.SecretKeySpec(BlowfishKey.getBytes(), "Blowfish") val cipher = javax.crypto.Cipher.getInstance("Blowfish") cipher.init(javax.crypto.Cipher.ENCRYPT_MODE, spec) - new String(Base64.encodeBase64(cipher.doFinal(value.getBytes("UTF-8"))), "UTF-8") + Base64.getEncoder.encodeToString(cipher.doFinal(value.getBytes("UTF-8"))) } def decodeBlowfish(value: String): String = { val spec = new javax.crypto.spec.SecretKeySpec(BlowfishKey.getBytes(), "Blowfish") val cipher = javax.crypto.Cipher.getInstance("Blowfish") cipher.init(javax.crypto.Cipher.DECRYPT_MODE, spec) - new String(cipher.doFinal(Base64.decodeBase64(value)), "UTF-8") + new String(cipher.doFinal(Base64.getDecoder.decode(value)), "UTF-8") } def urlEncode(value: String): String = URLEncoder.encode(value, "UTF-8").replace("+", "%20") diff --git a/src/main/scala/gitbucket/core/util/TextAvatarUtil.scala b/src/main/scala/gitbucket/core/util/TextAvatarUtil.scala new file mode 100644 index 0000000..48c0317 --- /dev/null +++ b/src/main/scala/gitbucket/core/util/TextAvatarUtil.scala @@ -0,0 +1,127 @@ +package gitbucket.core.util + +import java.io.ByteArrayOutputStream +import java.awt.image.BufferedImage +import javax.imageio.ImageIO +import java.awt.{Color, Font, Graphics2D, RenderingHints} +import java.awt.font.{FontRenderContext, TextLayout} +import java.awt.geom.AffineTransform + + +object TextAvatarUtil { + private val iconSize = 200 + private val fontSize = 180 + private val roundSize = 60 + private val shadowSize = 20 + private val bgSaturation = 0.68f + private val bgBlightness = 0.73f + private val shadowBlightness = 0.23f + private val font = new Font(Font.SANS_SERIF, Font.PLAIN, fontSize) + private val transparent = new Color(0, 0, 0, 0) + + // https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef + private def relativeLuminance(c: Color): Double = { + val rgb = Seq(c.getRed, c.getGreen, c.getBlue).map{_/255.0}.map{x => if (x <= 0.03928) x / 12.92 else math.pow((x + 0.055) / 1.055, 2.4)} + 0.2126 * rgb(0) + 0.7152 * rgb(1) + 0.0722 * rgb(2) + } + + // https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef + private def contrastRatio(c1: Color, c2: Color): Double = { + val l1 = relativeLuminance(c1) + val l2 = relativeLuminance(c2) + if (l1 > l2) (l1 + 0.05) / (l2 + 0.05) else (l2 + 0.05) / (l1 + 0.05) + } + + private def goodContrastColor(base: Color, c1: Color, c2: Color): Color = { + if (contrastRatio(base, c1) > contrastRatio(base, c2)) c1 else c2 + } + + private def strToHue(text: String): Float = { + Integer.parseInt(StringUtil.md5(text).substring(0, 2), 16) / 256f + } + + private def getCenterToDraw(drawText: String, font: Font, w: Int, h: Int): (Int, Int) = { + val context = new FontRenderContext(new AffineTransform(), true, true) + val txt = new TextLayout(drawText, font, context) + + val bounds = txt.getBounds + + val x: Int = ((w - bounds.getWidth) / 2 - bounds.getX).toInt + val y: Int = ((h - bounds.getHeight) / 2 - bounds.getY).toInt + (x, y) + } + + private def textImage(drawText: String, bgColor: Color, fgColor: Color): Array[Byte] = { + val canvas = new BufferedImage(iconSize, iconSize, BufferedImage.TYPE_INT_ARGB) + val g = canvas.createGraphics() + val center = getCenterToDraw(drawText, font, iconSize, iconSize) + + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON) + + g.setColor(transparent) + g.fillRect(0, 0, iconSize, iconSize) + + g.setColor(bgColor) + g.fillRoundRect(0, 0, iconSize, iconSize, roundSize, roundSize) + + g.setColor(fgColor) + g.setFont(font) + g.drawString(drawText, center._1, center._2) + + g.dispose() + + val stream = new ByteArrayOutputStream + ImageIO.write(canvas, "png", stream) + stream.toByteArray + } + + def textAvatar(nameText: String): Option[Array[Byte]] = { + val drawText = nameText.substring(0, 1) + + val bgHue = strToHue(nameText) + val bgColor = Color.getHSBColor(bgHue, bgSaturation, bgBlightness) + val fgColor = goodContrastColor(bgColor, Color.BLACK, Color.WHITE) + + val font = new Font(Font.SANS_SERIF, Font.PLAIN, fontSize) + if (font.canDisplayUpTo(drawText) == -1) Some(textImage(drawText, bgColor, fgColor)) else None + } + + private def textGroupImage(drawText: String, bgColor: Color, fgColor: Color, shadowColor: Color): Array[Byte] = { + val canvas = new BufferedImage(iconSize, iconSize, BufferedImage.TYPE_INT_ARGB) + val g = canvas.createGraphics() + val center = getCenterToDraw(drawText, font, iconSize - shadowSize, iconSize - shadowSize) + + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON) + + g.setColor(transparent) + g.fillRect(0, 0, iconSize, iconSize) + + g.setColor(shadowColor) + g.fillRect(shadowSize, shadowSize, iconSize, iconSize) + + g.setColor(bgColor) + g.fillRect(0, 0, iconSize - shadowSize, iconSize - shadowSize) + + g.setColor(fgColor) + + g.setFont(font) + g.drawString(drawText, center._1, center._2) + + g.dispose() + + val stream = new ByteArrayOutputStream + ImageIO.write(canvas, "png", stream) + stream.toByteArray + } + + def textGroupAvatar(nameText: String): Option[Array[Byte]] = { + val drawText = nameText.substring(0, 1) + + val bgHue = strToHue(nameText) + val bgColor = Color.getHSBColor(bgHue, bgSaturation, bgBlightness) + val fgColor = goodContrastColor(bgColor, Color.BLACK, Color.WHITE) + val shadowColor = Color.getHSBColor(bgHue, bgSaturation, shadowBlightness) + + if (font.canDisplayUpTo(drawText) == -1) Some(textGroupImage(drawText, bgColor, fgColor, shadowColor)) else None + } +} diff --git a/src/main/scala/gitbucket/core/view/AvatarImageProvider.scala b/src/main/scala/gitbucket/core/view/AvatarImageProvider.scala index 8d316ab..8cd64d0 100644 --- a/src/main/scala/gitbucket/core/view/AvatarImageProvider.scala +++ b/src/main/scala/gitbucket/core/view/AvatarImageProvider.scala @@ -20,7 +20,7 @@ if(account.image.isEmpty && context.settings.gravatar){ s"""https://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress.toLowerCase)}?s=${size}&d=retro&r=g""" } else { - s"""${context.path}/${account.userName}/_avatar""" + s"""${context.path}/${account.userName}/_avatar?${helpers.hashDate(account.updatedDate)}""" } } getOrElse { s"""${context.path}/_unknown/_avatar""" @@ -31,7 +31,7 @@ if(account.image.isEmpty && context.settings.gravatar){ s"""https://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress.toLowerCase)}?s=${size}&d=retro&r=g""" } else { - s"""${context.path}/${account.userName}/_avatar""" + s"""${context.path}/${account.userName}/_avatar?${helpers.hashDate(account.updatedDate)}""" } } getOrElse { if(context.settings.gravatar){ @@ -49,4 +49,4 @@ } } -} \ No newline at end of file +} diff --git a/src/main/scala/gitbucket/core/view/Markdown.scala b/src/main/scala/gitbucket/core/view/Markdown.scala index 4b36da3..88f8fff 100644 --- a/src/main/scala/gitbucket/core/view/Markdown.scala +++ b/src/main/scala/gitbucket/core/view/Markdown.scala @@ -44,7 +44,6 @@ val renderer = new GitBucketMarkedRenderer(options, repository, enableWikiLink, enableRefsLink, enableAnchor, enableTaskList, hasWritePermission, pages) - //helpers.decorateHtml(Marked.marked(source, options, renderer), repository) Marked.marked(source, options, renderer) } diff --git a/src/main/scala/gitbucket/core/view/helpers.scala b/src/main/scala/gitbucket/core/view/helpers.scala index dc4e1c1..b308c5f 100644 --- a/src/main/scala/gitbucket/core/view/helpers.scala +++ b/src/main/scala/gitbucket/core/view/helpers.scala @@ -75,6 +75,21 @@ def date(date: Date): String = new SimpleDateFormat("yyyy-MM-dd").format(date) /** + * Format java.util.Date to "yyyyMMDDHHmmss" (for url hash ex. /some/path.css?19800101010203 + */ + def hashDate(date: Date): String = new SimpleDateFormat("yyyyMMddHHmmss").format(date) + + /** + * java.util.Date of boot timestamp. + */ + val bootDate: Date = new Date() + + /** + * hashDate of bootDate for /assets, /plugin-assets + */ + def hashQuery: String = hashDate(bootDate) + + /** * Returns singular if count is 1, otherwise plural. * If plural is not specified, returns singular + "s" as plural. */ @@ -209,9 +224,15 @@ /** * Returns the url to the root of assets. */ + @deprecated("Use assets(path: String)(implicit context: Context) instead.", "4.11.0") def assets(implicit context: Context): String = s"${context.path}/assets" /** + * Returns the url to the path of assets. + */ + def assets(path: String)(implicit context: Context): String = s"${context.path}/assets${path}?${hashQuery}" + + /** * Generates the text link to the account page. * If user does not exist or disabled, this method returns user name as text without link. */ @@ -344,6 +365,10 @@ decorateHtml(HtmlFormat.fill(out).toString, repository) } + /** + * Decorate a given HTML by TextDecorators which are provided by plug-ins. + * TextDecorators are applied to only text parts of a given HTML. + */ def decorateHtml(html: String, repository: RepositoryInfo)(implicit context: Context): String = { PluginRegistry().getTextDecorators.foldLeft(html){ case (html, decorator) => val text = new StringBuilder() diff --git a/src/main/twirl/gitbucket/core/account/activity.scala.html b/src/main/twirl/gitbucket/core/account/activity.scala.html index ed986ef..e3b1307 100644 --- a/src/main/twirl/gitbucket/core/account/activity.scala.html +++ b/src/main/twirl/gitbucket/core/account/activity.scala.html @@ -4,7 +4,7 @@ @import gitbucket.core.view.helpers @gitbucket.core.account.html.main(account, groupNames, "activity"){
@gitbucket.core.helper.html.activities(activities) } diff --git a/src/main/twirl/gitbucket/core/helper/diff.scala.html b/src/main/twirl/gitbucket/core/helper/diff.scala.html index 17fdea2..a5e2ddf 100644 --- a/src/main/twirl/gitbucket/core/helper/diff.scala.html +++ b/src/main/twirl/gitbucket/core/helper/diff.scala.html @@ -16,9 +16,9 @@ Showing @diffs.size changed @helpers.plural(diffs.size, "file") -