All Downloads are FREE. Search and download functionalities are using the official Maven repository.

gitbucket.core.view.helpers.scala Maven / Gradle / Ivy

The newest version!
package gitbucket.core.view

import java.text.SimpleDateFormat
import java.util.{Date, Locale, TimeZone}

import gitbucket.core.controller.Context
import gitbucket.core.model.CommitState
import gitbucket.core.model.PullRequest
import gitbucket.core.plugin.{PluginRegistry, RenderRequest}
import gitbucket.core.service.RepositoryService.RepositoryInfo
import gitbucket.core.service.{RepositoryService, RequestCache}
import gitbucket.core.util.{FileUtil, JGitUtil, StringUtil}
import org.apache.commons.codec.digest.DigestUtils
import org.json4s.Formats
import play.twirl.api.{Html, HtmlFormat}

/**
 * Provides helper methods for Twirl templates.
 */
object helpers extends AvatarImageProvider with LinkConverter with RequestCache {

  /**
   * Format java.util.Date to "yyyy-MM-dd HH:mm:ss".
   */
  def datetime(date: Date): String = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date)

  val timeUnits = List(
    (1000L, "second"),
    (1000L * 60, "minute"),
    (1000L * 60 * 60, "hour"),
    (1000L * 60 * 60 * 24, "day"),
    (1000L * 60 * 60 * 24 * 30, "month"),
    (1000L * 60 * 60 * 24 * 365, "year")
  ).reverse

  /**
   * Format java.util.Date to "x {seconds/minutes/hours/days/months/years} ago"
   */
  def datetimeAgo(date: Date): String = {
    val duration = new Date().getTime - date.getTime
    timeUnits.find(tuple => duration / tuple._1 > 0) match {
      case Some((unitValue, unitString)) =>
        val value = duration / unitValue
        s"${value} ${unitString}${if (value > 1) "s" else ""} ago"
      case None => "just now"
    }
  }

  /**
   * Format java.util.Date to "x {seconds/minutes/hours/days} ago"
   * If duration over 1 month, format to "d MMM (yyyy)"
   */
  def datetimeAgoRecentOnly(date: Date): String = {
    val duration = new Date().getTime - date.getTime
    timeUnits.find(tuple => duration / tuple._1 > 0) match {
      case Some((_, "month")) => s"on ${new SimpleDateFormat("d MMM", Locale.ENGLISH).format(date)}"
      case Some((_, "year"))  => s"on ${new SimpleDateFormat("d MMM yyyy", Locale.ENGLISH).format(date)}"
      case Some((unitValue, unitString)) =>
        val value = duration / unitValue
        s"${value} ${unitString}${if (value > 1) "s" else ""} ago"
      case None => "just now"
    }
  }

  /**
   * Format java.util.Date to "yyyy-MM-dd'T'hh:mm:ss'Z'".
   */
  def datetimeRFC3339(date: Date): String = {
    val sf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'")
    sf.setTimeZone(TimeZone.getTimeZone("UTC"))
    sf.format(date)
  }

  /**
   * Format java.util.Date to "yyyy-MM-dd".
   */
  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.
   */
  def plural(count: Int, singular: String, plural: String = ""): String =
    if (count == 1) singular else if (plural.isEmpty) singular + "s" else plural

  /**
   * Converts Markdown of Wiki pages to HTML.
   */
  def markdown(
    markdown: String,
    repository: RepositoryService.RepositoryInfo,
    branch: String,
    enableWikiLink: Boolean,
    enableRefsLink: Boolean,
    enableLineBreaks: Boolean,
    enableAnchor: Boolean = true,
    enableTaskList: Boolean = false,
    hasWritePermission: Boolean = false,
    pages: List[String] = Nil
  )(implicit context: Context): Html =
    Html(
      Markdown.toHtml(
        markdown = markdown,
        repository = repository,
        branch = branch,
        enableWikiLink = enableWikiLink,
        enableRefsLink = enableRefsLink,
        enableAnchor = enableAnchor,
        enableLineBreaks = enableLineBreaks,
        enableTaskList = enableTaskList,
        hasWritePermission = hasWritePermission,
        pages = pages
      )
    )

  /**
   * Render the given source (only markdown is supported in default) as HTML.
   * You can test if a file is renderable in this method by [[isRenderable()]].
   */
  def renderMarkup(
    filePath: List[String],
    fileContent: String,
    branch: String,
    repository: RepositoryService.RepositoryInfo,
    enableWikiLink: Boolean,
    enableRefsLink: Boolean,
    enableAnchor: Boolean
  )(implicit context: Context): Html = {

    val fileName = filePath.last.toLowerCase
    val extension = FileUtil.getExtension(fileName)
    val renderer = PluginRegistry().getRenderer(extension)
    renderer.render(
      RenderRequest(filePath, fileContent, branch, repository, enableWikiLink, enableRefsLink, enableAnchor, context)
    )
  }

  /**
   * Tests whether the given file is renderable. It's tested by the file extension.
   */
  def isRenderable(fileName: String): Boolean = {
    PluginRegistry().renderableExtensions.exists(extension => fileName.toLowerCase.endsWith("." + extension))
  }

  /**
   * Creates a link to the issue or the pull request from the issue id.
   */
  def issueLink(owner: String, repository: String, issueId: Int, title: String)(implicit
    context: Context
  ): Html = {
    Html(createIssueLink(owner, repository, issueId, title))
  }

  /**
   * Creates a global link to the issue or the pull request from the issue id.
   */
  def issueGlobalLink(owner: String, repository: String, issueId: Int, title: String)(implicit
    context: Context
  ): Html = {
    Html(createGlobalIssueLink(owner, repository, issueId, title))
  }

  /**
   * Returns <img> which displays the avatar icon for the given user name.
   * This method looks up Gravatar if avatar icon has not been configured in user settings.
   */
  def avatar(userName: String, size: Int, tooltip: Boolean = false, mailAddress: String = "")(implicit
    context: Context
  ): Html =
    getAvatarImageHtml(userName, size, mailAddress, tooltip)

  /**
   * Returns <img> which displays the avatar icon for the given mail address.
   * This method looks up Gravatar if avatar icon has not been configured in user settings.
   */
  def avatar(commit: JGitUtil.CommitInfo, size: Int)(implicit context: Context): Html =
    getAvatarImageHtml(commit.authorName, size, commit.authorEmailAddress)

  /**
   * Converts commit id, issue id and username to the link.
   */
  def link(value: String, repository: RepositoryService.RepositoryInfo)(implicit context: Context): Html =
    Html(decorateHtml(convertRefsLinks(value, repository), repository))

  import scala.util.matching.Regex._
  implicit class RegexReplaceString(private val s: String) extends AnyVal {
    def replaceAll(pattern: String)(replacer: Match => String): String = {
      pattern.r.replaceAllIn(s, (m: Match) => replacer(m).replace("$", "\\$"))
    }
  }

  /**
   * Convert link notations in the activity message.
   */
  // format: off
  def activityMessage(message: String)(implicit context: Context): Html =
    Html(
      message
        .replaceAll("\\[issue:([^\\s]+?)/([^\\s]+?)#((\\d+))\\]"){ m =>
          getIssueFromCache(m.group(1), m.group(2), m.group(3)) match {
            case Some(issue) =>
              s"""${m.group(1)}/${m.group(2)}#${m.group(3)}"""
            case None =>
              s"${m.group(1)}/${m.group(2)}#${m.group(3)}"
          }
        }
        .replaceAll("\\[pullreq:([^\\s]+?)/([^\\s]+?)#((\\d+))\\]"){ m =>
          getIssueFromCache(m.group(1), m.group(2), m.group(3)) match {
            case Some(pullreq) =>
              s"""${m.group(1)}/${m.group(2)}#${m.group(3)}"""
            case None =>
              s"${m.group(1)}/${m.group(2)}#${m.group(3)}"
          }
        }
        .replaceAll("\\[repo:([^\\s]+?)/([^\\s]+?)\\]") { m =>
          if (getRepositoryInfoFromCache(m.group(1), m.group(2)).isDefined) {
            s"""${m.group(1)}/${m.group(2)}"""
          } else {
            s"${m.group(1)}/${m.group(2)}"
          }
        }
        .replaceAll("\\[branch:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]") { m =>
          if (getRepositoryInfoFromCache(m.group(1), m.group(2)).isDefined) {
            s"""${StringUtil.escapeHtml(m.group(3))}"""
          } else {
            StringUtil.escapeHtml(m.group(3))
          }
        }
        .replaceAll("\\[tag:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]") { m =>
          if (getRepositoryInfoFromCache(m.group(1), m.group(2)).isDefined) {
            s"""${StringUtil.escapeHtml(m.group(3))}"""
          } else {
            StringUtil.escapeHtml(m.group(3))
          }
        }
        .replaceAll("\\[user:([^\\s]+?)\\]") { m =>
          user(m.group(1)).body
        }
        .replaceAll("\\[commit:([^\\s]+?)/([^\\s]+?)\\@([^\\s]+?)\\]") { m =>
          if (getRepositoryInfoFromCache(m.group(1), m.group(2)).isDefined) {
            s"""${m.group(1)}/${m.group(2)}@${m.group(3).substring(0, 7)}"""
          } else {
            s"${m.group(1)}/${m.group(2)}@${m.group(3).substring(0, 7)}"
          }
        }
        .replaceAll("\\[release:([^\\s]+?)/([^\\s]+?)/([^\\s]+?):(.+)\\]") { m =>
          if (getRepositoryInfoFromCache(m.group(1), m.group(2)).isDefined) {
            s"""${StringUtil.escapeHtml(m.group(4))}"""
          } else {
            StringUtil.escapeHtml(m.group(4))
          }
        }
    )
  // format: off

  /**
   * Remove html tags from the given Html instance.
   */
  def removeHtml(html: Html): Html = Html(html.body.replaceAll("<.+?>", ""))

  /**
   * URL encode except '/'.
   */
  def encodeRefName(value: String): String = StringUtil.encodeRefName(value)

  def urlEncode(value: String): String = StringUtil.urlEncode(value)

  def urlEncode(value: Option[String]): String = value.map(urlEncode).getOrElse("")

  /**
   * Generates the url to the repository.
   */
  def url(repository: RepositoryService.RepositoryInfo)(implicit context: Context): String =
    s"${context.path}/${encodeRefName(repository.owner)}/${encodeRefName(repository.name)}"

  /**
   * Generates the url to the account page.
   */
  def url(userName: String)(implicit context: Context): String = s"${context.path}/${encodeRefName(userName)}"

  /**
   * Generates the url to the pull request base branch.
   */
  def basePRBranchUrl(pullreq: PullRequest)(implicit context: Context): String =
    s"${context.path}/${encodeRefName(pullreq.userName)}/${encodeRefName(pullreq.repositoryName)}/tree/${encodeRefName(pullreq.branch)}"

  /**
   * Generates the url to the pull request branch.
   */
  def requestPRBranchUrl(pullreq: PullRequest)(implicit context: Context): String =
    s"${context.path}/${encodeRefName(pullreq.requestUserName)}/${encodeRefName(pullreq.repositoryName)}/tree/${encodeRefName(pullreq.requestBranch)}"

  /**
   * 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.
   */
  def user(userName: String, mailAddress: String = "", styleClass: String = "")(implicit context: Context): Html =
    userWithContent(userName, mailAddress, styleClass)(Html(StringUtil.escapeHtml(userName)))

  /**
   * Generates the avatar link to the account page.
   * If user does not exist or disabled, this method returns avatar image without link.
   */
  def avatarLink(
    userName: String,
    size: Int,
    mailAddress: String = "",
    tooltip: Boolean = false,
    label: Boolean = false
  )(implicit context: Context): Html = {

    val avatarHtml = avatar(userName, size, tooltip, mailAddress)
    val contentHtml = if (label) Html(avatarHtml.body + " " + userName) else avatarHtml

    userWithContent(userName, mailAddress)(contentHtml)
  }

  /**
   * Generates the avatar link to the account page.
   * If user does not exist or disabled, this method returns avatar image without link.
   */
  def avatarLink(commit: JGitUtil.CommitInfo, size: Int)(implicit context: Context): Html =
    userWithContent(commit.authorName, commit.authorEmailAddress)(avatar(commit, size))

  private def userWithContent(userName: String, mailAddress: String = "", styleClass: String = "")(
    content: Html
  )(implicit context: Context): Html =
    (if (mailAddress.isEmpty) {
       getAccountByUserNameFromCache(userName)
     } else {
       getAccountByMailAddressFromCache(mailAddress)
     }).map { account =>
      Html(s"""${content}""")
    } getOrElse content

  /**
   * Test whether the given Date is past date.
   */
  def isPast(date: Date): Boolean = System.currentTimeMillis > date.getTime

  def pre(value: Html): Html = Html(s"
${value.body.trim.split("\n").map(_.trim).mkString("\n")}
") /** * Implicit conversion to add mkHtml() to Seq[Html]. */ implicit class RichHtmlSeq(private val seq: Seq[Html]) extends AnyVal { def mkHtml(separator: String) = Html(seq.mkString(separator)) def mkHtml(separator: scala.xml.Elem) = Html(seq.mkString(separator.toString)) } def commitStateIcon(state: CommitState) = Html(state match { case CommitState.PENDING => """""" case CommitState.SUCCESS => """""" case CommitState.ERROR => """""" case CommitState.FAILURE => """""" }) def commitStateText(state: CommitState, commitId: String) = state match { case CommitState.PENDING => "Waiting to hear about " + commitId.substring(0, 8) case CommitState.SUCCESS => "All is well" case CommitState.ERROR => "Failed" case CommitState.FAILURE => "Failed" } /** * Render a given object as the JSON string. */ def json(obj: AnyRef): String = { implicit val formats: Formats = org.json4s.DefaultFormats org.json4s.jackson.Serialization.write(obj) } // This pattern comes from: http://stackoverflow.com/a/4390768/1771641 (extract-url-from-string) private[this] val urlRegex = """(?i)\b((?:https?://|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,13}/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))""".r def urlLink(text: String): String = { val matches = urlRegex.findAllMatchIn(text).toSeq val (x, pos) = matches.foldLeft((collection.immutable.Seq.empty[Html], 0)) { case ((x, pos), m) => val url = m.group(0) val href = url.replace("\"", """) ( x ++ (Seq( if (pos < m.start) Some(HtmlFormat.escape(text.substring(pos, m.start))) else None, Some(Html(s"""${url}""")) ).flatten), m.end ) } // append rest fragment val out = if (pos < text.length) x :+ HtmlFormat.escape(text.substring(pos)) else x HtmlFormat.fill(out).toString } /** * 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() val result = new StringBuilder() var tag = false html.foreach { c => c match { case '<' if tag == false => { tag = true if (text.nonEmpty) { result.append(decorator.decorate(text.toString, repository)) text.setLength(0) } result.append(c) } case '>' if tag => { tag = false result.append(c) } case _ if tag == false => { text.append(c) } case _ if tag => { result.append(c) } } } if (text.nonEmpty) { result.append(decorator.decorate(text.toString, repository)) } result.toString } } /** * a human-readable display value (includes units - EB, PB, TB, GB, MB, KB or bytes) * * @param size total size of object in bytes */ def readableSize(size: Option[Long]): String = FileUtil.readableSize(size.getOrElse(0)) /** * Make HTML fragment of the partial diff for a comment on a line of diff. * * @param jsonString JSON string which is stored in COMMIT_COMMENT table. * @return HTML fragment of diff */ def diff(jsonString: String): Html = { import org.json4s._ import org.json4s.jackson.JsonMethods._ implicit val formats: Formats = DefaultFormats val diff = parse(jsonString).extract[Seq[CommentDiffLine]] val sb = new StringBuilder() sb.append("") diff.foreach { line => sb.append("") sb.append(s"""") sb.append(s"""") sb.append(s"""") sb.append("") } sb.append("
""") line.oldLine.foreach { oldLine => sb.append(oldLine) } sb.append("""") line.newLine.foreach { newLine => sb.append(newLine) } sb.append("""") sb.append(StringUtil.escapeHtml(line.text)) sb.append("
") Html(sb.toString()) } case class CommentDiffLine(newLine: Option[String], oldLine: Option[String], `type`: String, text: String) def appendQueryString(baseUrl: String, queryString: String): String = { s"$baseUrl${if (baseUrl.contains("?")) "&" else "?"}$queryString" } def md5(value: String): String = DigestUtils.md5Hex(value) }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy