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

net.liftweb.http.HtmlNormalizer.scala Maven / Gradle / Ivy

The newest version!
package net.liftweb
package http

import scala.xml._
import scala.collection.immutable.Vector

import js.JsCmd
import js.JsCmds.Noop
import js.JE.{AnonFunc, Call, JsRaw}

import util.Helpers._

/**
 * Represents an HTML attribute for an event handler. Carries the event name and
 * the JS that should run when that event is triggered as a String.
 */
private case class EventAttribute(eventName: String, jsString: String)
private object EventAttribute {
  /**
   * Some elements allow a URL attribute to take a `javascript:(//)`-style
   * URL instead of setting an `on*` event in order to invoke JS. For example,
   * you can (and Lift does) set a form's `action` attribute to
   * `javascript://(some JS)` instead of setting `onsubmit` to `(someJS);
   * return false`.
   *
   * This is a map from those attribute names to the corresponding JS event
   * that would be used to execute that JS when it isn't run in line.
   */
  val eventsByAttributeName =
    Map(
      "action" -> "submit",
      "href" -> "click"
    )

  object EventForAttribute {
    def unapply(attributeName: String): Option[String] = {
      eventsByAttributeName.get(attributeName)
    }
  }
}

case class NodesAndEventJs(nodes: NodeSeq, js: JsCmd) {
  def append(newNodesAndEventJs: NodesAndEventJs): NodesAndEventJs = {
    this.copy(
      nodes = nodes ++ newNodesAndEventJs.nodes,
      js = js & newNodesAndEventJs.js
    )
  }
  def append(newNodeAndEventJs: NodeAndEventJs): NodesAndEventJs = {
    append(newNodeAndEventJs.node, newNodeAndEventJs.js)
  }
  def append(newNode: Node): NodesAndEventJs = {
    this.copy(nodes = nodes :+ newNode)
  }
  def append(newJs: JsCmd): NodesAndEventJs = {
    this.copy(js = js & newJs)
  }
  def append(newNode: Node, newJs: JsCmd): NodesAndEventJs = {
    this.copy(nodes = nodes :+ newNode, js = js & newJs)
  }
}
private[http] case class NodeAndEventJs(node: Node, js: JsCmd) {
  def append(newJs: JsCmd): NodeAndEventJs = {
    this.copy(js = js & newJs)
  }
}

/**
 * Helper class that performs certain Lift-specific normalizations of HTML
 * represented as `NodeSeq`s:
 *  - Fixes URLs for `link`, `a`, `form`, and `script` elements to properly
 *    prepend the container's context path in case these are absolute URLs.
 *  - Extracts event attributes (replacing them with
 *    `[[LiftRules.attributeForRemovedEventAttributes]]` if needed), returning
 *    instead JavaScript that will attach the corresponding event handler to
 *    that element.
 *  - Provides for caller-specific additional processing for each node.
 */
private[http] final object HtmlNormalizer {
  // Fix URLs using Req.normalizeHref and extract JS event attributes for
  // putting into page JS. Returns a triple of:
  //  - The optional id that was found in this set of attributes.
  //  - The normalized metadata.
  //  - A list of extracted `EventAttribute`s.
  private[this] def normalizeUrlAndExtractEvents(
    attributeToNormalize: String,
    attributes: MetaData,
    contextPath: String,
    shouldRewriteUrl: Boolean, // whether to apply URLRewrite.rewriteFunc
    extractInlineJavaScript: Boolean,
    eventAttributes: List[EventAttribute] = Nil
  ): (Option[String], MetaData, List[EventAttribute]) = {
    if (attributes == Null) {
      (None, Null, eventAttributes)
    } else {
      // Note: we don't do this tail-recursively because we have to preserve
      // attribute order!
      val (id, normalizedRemainingAttributes, remainingEventAttributes) =
        normalizeUrlAndExtractEvents(
          attributeToNormalize,
          attributes.next,
          contextPath,
          shouldRewriteUrl,
          extractInlineJavaScript,
          eventAttributes
        )

      attributes match {
        case attribute @ UnprefixedAttribute(
               EventAttribute.EventForAttribute(eventName),
               attributeValue,
               remainingAttributes
             ) if attributeValue.text.startsWith("javascript:") && extractInlineJavaScript =>
          val attributeJavaScript = {
            // Could be javascript: or javascript://.
            val base = attributeValue.text.substring(11)
            val strippedJs =
              if (base.startsWith("//"))
                base.substring(2)
              else
                base

            if (strippedJs.trim.isEmpty) {
              Nil
            } else {
              // When using javascript:-style URIs, event.preventDefault is implied.
              List(strippedJs + "; event.preventDefault()")
            }
          }

          val updatedEventAttributes =
            attributeJavaScript.map(EventAttribute(eventName, _)) :::
            remainingEventAttributes

          (id, normalizedRemainingAttributes, updatedEventAttributes)

        case UnprefixedAttribute(name, _, _) if name == attributeToNormalize =>
          val normalizedUrl =
            Req.normalizeHref(
              contextPath,
              attributes.value,
              shouldRewriteUrl,
              URLRewriter.rewriteFunc
            )

          val newMetaData =
            new UnprefixedAttribute(
              attributeToNormalize,
              normalizedUrl,
              normalizedRemainingAttributes
            )

          (id, newMetaData, remainingEventAttributes)

        case UnprefixedAttribute(name, attributeValue, _) if name.startsWith("on") && extractInlineJavaScript =>
          val updatedEventAttributes =
            EventAttribute(name.substring(2), attributeValue.text) ::
            remainingEventAttributes

          (id, normalizedRemainingAttributes, updatedEventAttributes)

        case UnprefixedAttribute("id", attributeValue, _) =>
          val idValue = Option(attributeValue.text).filter(_.nonEmpty)

          (idValue, attributes.copy(normalizedRemainingAttributes), remainingEventAttributes)

        case _ =>
          (id, attributes.copy(normalizedRemainingAttributes), remainingEventAttributes)
      }
    }
  }

  // Given an element id and the `EventAttribute`s to apply to elements with
  // that id, return a JsCmd that binds all those event handlers to that id.
  private[this] def jsForEventAttributes(elementId: String, eventAttributes: List[EventAttribute]): JsCmd = {
    eventAttributes.map {
      case EventAttribute(name, handlerJs) =>
        Call(
          "lift.onEvent",
          elementId,
          name,
          AnonFunc("event", JsRaw(handlerJs).cmd)
        ).cmd
    }.foldLeft(Noop)(_ & _)
  }

  private[http] def normalizeElementAndAttributes(
    element: Elem,
    attributeToNormalize: String,
    contextPath: String,
    shouldRewriteUrl: Boolean,
    extractEventJavaScript: Boolean
  ): NodeAndEventJs = {
    val (id, normalizedAttributes, eventAttributes) =
      normalizeUrlAndExtractEvents(
        attributeToNormalize,
        element.attributes,
        contextPath,
        shouldRewriteUrl,
        extractEventJavaScript
      )

    val attributesIncludingEventsAsData =
      LiftRules.attributeForRemovedEventAttributes match {
        case Some(attribute) if eventAttributes.nonEmpty =>
          val removedAttributes = eventAttributes.map {
            case EventAttribute(event, _) =>
              s"on$event"
          }
          new UnprefixedAttribute(attribute, removedAttributes.mkString(" "), normalizedAttributes)

        case _ =>
          normalizedAttributes
      }

    id.map { foundId =>
      NodeAndEventJs(
        element.copy(attributes = attributesIncludingEventsAsData),
        jsForEventAttributes(foundId, eventAttributes)
      )
    } getOrElse {
      if (eventAttributes.nonEmpty) {
        val generatedId = s"lift-event-js-${nextFuncName}"

        NodeAndEventJs(
          element.copy(attributes = new UnprefixedAttribute("id", generatedId, attributesIncludingEventsAsData)),
          jsForEventAttributes(generatedId, eventAttributes)
        )
      } else {
        NodeAndEventJs(
          element.copy(attributes = attributesIncludingEventsAsData),
          Noop
        )
      }
    }
  }

  private[http] def normalizeNode(
    node: Node,
    contextPath: String,
    stripComments: Boolean,
    extractEventJavaScript: Boolean
  ): Option[NodeAndEventJs] = {
    node match {
      case element: Elem =>
        val (attributeToFix, shouldRewriteUrl) =
          element.label match {
            case "form" =>
              ("action", true)

            case "a" =>
              ("href", true)
            case "link" =>
              ("href", false)

            case "script" =>
              ("src", false)
            case _ =>
              ("src", true)
          }

        Some(
          normalizeElementAndAttributes(
            element,
            attributeToFix,
            contextPath,
            shouldRewriteUrl,
            extractEventJavaScript
          )
        )

      case _: Comment if stripComments =>
        None

      case otherNode =>
        Some(NodeAndEventJs(otherNode, Noop))
    }
  }

  /**
   * Normalizes `nodes` to adjust URLs with the given `contextPath`, stripping
   * comments if `stripComments` is `true`, and extracting event JavaScript
   * into the `js` part of the returned `NodesAndEventJs` if
   * `extractEventJavaScript` is `true`.
   *
   * See `[[LiftMerge.merge]]` for sample usage.
   */
  def normalizeHtmlAndEventHandlers(
    nodes: NodeSeq,
    contextPath: String,
    stripComments: Boolean,
    extractEventJavaScript: Boolean
  ): NodesAndEventJs = {
    nodes.foldLeft(NodesAndEventJs(Vector[Node](), Noop)) { (soFar, nodeToNormalize) =>
      normalizeNode(nodeToNormalize, contextPath, stripComments, extractEventJavaScript).map {
        case NodeAndEventJs(normalizedElement: Elem, js: JsCmd) =>
          val NodesAndEventJs(normalizedChildren, childJs) =
            normalizeHtmlAndEventHandlers(
              normalizedElement.child,
              contextPath,
              stripComments,
              extractEventJavaScript
            )

          soFar
            .append(js)
            .append(normalizedElement.copy(child = normalizedChildren), childJs)

        case node =>
          soFar.append(node)
      } getOrElse {
        soFar
      }
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy