Skip to content

Commit

Permalink
Merge pull request lift#1665 from lift/extract-o-matic
Browse files Browse the repository at this point in the history
Extract-o-matic: Extract event handlers to page JS

The main functionality here moves event handler attachment from inline
attributes to the page JavaScript that Lift 3 now supports. The reason for
this is so that, out of the box, Lift will be compatible with very restrictive
Content Security Policy settings when using built-in Lift features.

To do this, we add a lift.onEvent function that has both jQuery and vanilla
implementations. There are also a couple of fixes to existing JS functionality
here.
  • Loading branch information
Shadowfiend committed Jan 21, 2015
2 parents aceac3f + 67e924f commit a21dfa0
Show file tree
Hide file tree
Showing 2 changed files with 189 additions and 25 deletions.
45 changes: 33 additions & 12 deletions web/webkit/src/main/resources/toserve/lift.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,11 @@
ajaxGet: function() {
consoleOrAlert("ajaxGet function must be defined in settings");
},
onEvent: function(elementOrId, eventName, fn) {
consoleOrAlert("onEvent function must be defined in settings");
},
onDocumentReady: function(fn) {
consoleOrAlert("documentReady function must be defined in settings");
consoleOrAlert("onDocumentReady function must be defined in settings");
},
cometGetTimeout: 140000,
cometFailureRetryTimeout: 10000,
Expand Down Expand Up @@ -554,7 +557,7 @@
this.extend(settings, options);

var lift = this;
options.onDocumentReady(function() {
settings.onDocumentReady(function() {
var attributes = document.body.attributes,
cometGuid, cometVersion,
comets = {};
Expand Down Expand Up @@ -584,7 +587,8 @@
doCycleIn200();
});
},
logError: settings.logError,
logError: function() { settings.logError.apply(this, arguments) },
onEvent: function() { settings.onEvent.apply(this, arguments) },
ajax: appendToQueue,
startGc: successRegisterGC,
ajaxOnSessionLost: function() {
Expand Down Expand Up @@ -638,6 +642,13 @@
})();

window.liftJQuery = {
onEvent: function(elementOrId, eventName, fn) {
if (typeof elementOrId == 'string') {
elementOrId = '#' + elementOrId;
}

jQuery(elementOrId).on(eventName, fn);
},
onDocumentReady: jQuery(document).ready,
ajaxPost: function(url, data, dataType, onSuccess, onFailure) {
var processData = true,
Expand Down Expand Up @@ -679,16 +690,26 @@
};

window.liftVanilla = {
// This and onDocumentReady adapted from https://github.com/dperini/ContentLoaded/blob/master/src/contentloaded.js,
// as also used (with modifications) in jQuery.
onEvent: function(elementOrId, eventName, fn) {
var win = window,
doc = win.document,
add = doc.addEventListener ? 'addEventListener' : 'attachEvent',
pre = doc.addEventListener ? '' : 'on';

var element = elementOrId;
if (typeof elementOrId == 'string') {
element = document.getElementById(elementOrId);
}

element[add](pre + eventName, fn, false);
},
onDocumentReady: function(fn) {
// Taken from https://github.com/dperini/ContentLoaded/blob/master/src/contentloaded.js,
// as also used (with modifications) in jQuery.
var done = false, top = true,

win = window, doc = win.document, root = doc.documentElement,

add = doc.addEventListener ? 'addEventListener' : 'attachEvent',
pre = doc.addEventListener ? '' : 'on';
rem = doc.addEventListener ? 'removeEventListener' : 'detachEvent',
pre = doc.addEventListener ? '' : 'on',

init = function(e) {
if (e.type == 'readystatechange' && doc.readyState != 'complete') return;
Expand All @@ -708,9 +729,9 @@
try { top = !win.frameElement; } catch(e) { }
if (top) poll();
}
doc[add](pre + 'DOMContentLoaded', init, false);
doc[add](pre + 'readystatechange', init, false);
win[add](pre + 'load', init, false);
liftVanilla.onEvent(doc, 'DOMContentLoaded', init);
liftVanilla.onEvent(doc, 'readystatechange', init);
liftVanilla.onEvent(win, 'load', init);
}
},
ajaxPost: function(url, data, dataType, onSuccess, onFailure, onUploadProgress) {
Expand Down
169 changes: 156 additions & 13 deletions web/webkit/src/main/scala/net/liftweb/http/LiftMerge.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,42 @@
package net.liftweb
package http

import scala.collection.Map
import scala.collection.mutable.{HashMap, ArrayBuffer, ListBuffer}
import scala.xml._

import net.liftweb.util._
import net.liftweb.common._
import net.liftweb.http.js._
import JsCmds.Noop
import JE.{AnonFunc,Call,JsRaw}
import 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 {
/**
* A map from attribute name to event name for attributes that support being
* set to javascript:(//)-style values 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.
*/
val eventsByAttributeName =
Map(
"action" -> "submit",
"href" -> "click"
)

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

private[http] trait LiftMerge {
self: LiftSession =>

Expand All @@ -32,12 +61,21 @@ private[http] trait LiftMerge {
}

// Gather all page-specific JS into one JsCmd.
private def assemblePageSpecificJavaScript: JsCmd = {
private def assemblePageSpecificJavaScript(eventAttributesByElementId: Map[String,List[EventAttribute]]): JsCmd = {
val eventJs =
for {
(elementId, eventAttributes) <- eventAttributesByElementId
EventAttribute(eventName, jsString) <- eventAttributes
} yield {
Call("lift.onEvent", elementId, eventName, AnonFunc("event", JsRaw(jsString).cmd)).cmd
}

val allJs =
LiftRules.javaScriptSettings.vend().map { settingsFn =>
LiftJavaScript.initCmd(settingsFn(this))
}.toList ++
S.jsToAppend
S.jsToAppend ++
eventJs

allJs.foldLeft(js.JsCmds.Noop)(_ & _)
}
Expand Down Expand Up @@ -99,17 +137,97 @@ private[http] trait LiftMerge {
addlHead ++= S.forHead()
val addlTail = new ListBuffer[Node]
addlTail ++= S.atEndOfBody()
val eventAttributesByElementId = new HashMap[String,List[EventAttribute]]
val rewrite = URLRewriter.rewriteFunc
val fixHref = Req.fixHref

val contextPath: String = S.contextPath

def fixAttrs(original: MetaData, toFix: String, attrs: MetaData, fixURL: Boolean): MetaData = attrs match {
case Null => Null
case u: UnprefixedAttribute if u.key == toFix =>
new UnprefixedAttribute(toFix, fixHref(contextPath, attrs.value, fixURL, rewrite), fixAttrs(original, toFix, attrs.next, fixURL))
case _ => attrs.copy(fixAttrs(original, toFix, attrs.next, fixURL))
// Fix URLs using Req.fixHref 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 fixed metadata.
// - A list of extracted `EventAttribute`s.
def fixAttrs(original: MetaData, toFix: String, attrs: MetaData, fixURL: Boolean, eventAttributes: List[EventAttribute] = Nil): (Option[String], MetaData, List[EventAttribute]) = {
attrs match {
case Null => (None, Null, eventAttributes)

case attribute @ UnprefixedAttribute(
EventAttribute.EventForAttribute(eventName),
attributeValue,
remainingAttributes
) if attributeValue.text.startsWith("javascript:") =>
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, return false is implied.
List(strippedJs + "; event.preventDefault();")
}
}

val updatedEventAttributes = attributeJavaScript.map(EventAttribute(eventName, _)) ::: eventAttributes
fixAttrs(original, toFix, remainingAttributes, fixURL, updatedEventAttributes)

case u: UnprefixedAttribute if u.key == toFix =>
val (id, fixedAttributes, updatedEventAttributes) = fixAttrs(original, toFix, attrs.next, fixURL)

(id, new UnprefixedAttribute(toFix, fixHref(contextPath, attrs.value, fixURL, rewrite), fixedAttributes), updatedEventAttributes)

case u: UnprefixedAttribute if u.key.startsWith("on") =>
fixAttrs(original, toFix, attrs.next, fixURL, EventAttribute(u.key.substring(2), u.value.text) :: eventAttributes)

case u: UnprefixedAttribute if u.key == "id" =>
val (_, fixedAttributes, updatedEventAttributes) = fixAttrs(original, toFix, attrs.next, fixURL, eventAttributes)

(Option(u.value.text).filter(_.nonEmpty), attrs.copy(fixedAttributes), updatedEventAttributes)

case _ =>
val (id, fixedAttributes, updatedEventAttributes) = fixAttrs(original, toFix, attrs.next, fixURL, eventAttributes)

(id, attrs.copy(fixedAttributes), updatedEventAttributes)
}
}

// Fix the element's `attributeToFix` using `fixAttrs` and extract JS event
// attributes for putting into page JS. Return a fixed version of this
// element with fixed children. Adds a lift-generated id if the given
// element needs to have event handlers attached but doesn't already have an
// id.
def fixElementAndAttributes(element: Elem, attributeToFix: String, fixURL: Boolean, fixedChildren: NodeSeq) = {
val (id, fixedAttributes, eventAttributes) = fixAttrs(element.attributes, attributeToFix, element.attributes, fixURL)

id.map { foundId =>
eventAttributesByElementId += (foundId -> eventAttributes)

element.copy(
attributes = fixedAttributes,
child = fixedChildren
)
} getOrElse {
if (eventAttributes.nonEmpty) {
val generatedId = s"lift-event-js-$nextFuncName"
eventAttributesByElementId += (generatedId -> eventAttributes)

element.copy(
attributes = new UnprefixedAttribute("id", generatedId, fixedAttributes),
child = fixedChildren
)
} else {
element.copy(
attributes = fixedAttributes,
child = fixedChildren
)
}
}
}

def _fixHtml(in: NodeSeq, _inHtml: Boolean, _inHead: Boolean, _justHead: Boolean, _inBody: Boolean, _justBody: Boolean, _bodyHead: Boolean, _bodyTail: Boolean, doMergy: Boolean): NodeSeq = {
Expand Down Expand Up @@ -156,11 +274,36 @@ private[http] trait LiftMerge {
node <- _fixHtml(nodes, inHtml, inHead, justHead, inBody, justBody, bodyHead, bodyTail, doMergy)
} yield node

case e: Elem if e.label == "form" => Elem(v.prefix, v.label, fixAttrs(v.attributes, "action", v.attributes, true), v.scope, e.minimizeEmpty, _fixHtml(v.child, inHtml, inHead, justHead, inBody, justBody, bodyHead, bodyTail, doMergy): _*)
case e: Elem if e.label == "script" => Elem(v.prefix, v.label, fixAttrs(v.attributes, "src", v.attributes, false), v.scope, e.minimizeEmpty, _fixHtml(v.child, inHtml, inHead, justHead, inBody, justBody, bodyHead, bodyTail, doMergy): _*)
case e: Elem if e.label == "a" => Elem(v.prefix, v.label, fixAttrs(v.attributes, "href", v.attributes, true), v.scope, e.minimizeEmpty, _fixHtml(v.child, inHtml, inHead, justHead, inBody, justBody, bodyHead, bodyTail, doMergy): _*)
case e: Elem if e.label == "link" => Elem(v.prefix, v.label, fixAttrs(v.attributes, "href", v.attributes, false), v.scope, e.minimizeEmpty,_fixHtml(v.child, inHtml, inHead, justHead, inBody, justBody, bodyHead, bodyTail, doMergy): _*)
case e: Elem => Elem(v.prefix, v.label, fixAttrs(v.attributes, "src", v.attributes, true), v.scope, e.minimizeEmpty, _fixHtml(v.child, inHtml, inHead, justHead, inBody, justBody, bodyHead, bodyTail, doMergy): _*)
case e: Elem if e.label == "form" =>
fixElementAndAttributes(
e, "action", fixURL = true,
_fixHtml(e.child, inHtml, inHead, justHead, inBody, justBody, bodyHead, bodyTail, doMergy)
)

case e: Elem if e.label == "script" =>
fixElementAndAttributes(
e, "src", fixURL = false,
_fixHtml(e.child, inHtml, inHead, justHead, inBody, justBody, bodyHead, bodyTail, doMergy)
)

case e: Elem if e.label == "a" =>
fixElementAndAttributes(
e, "href", fixURL = true,
_fixHtml(e.child, inHtml, inHead, justHead, inBody, justBody, bodyHead, bodyTail, doMergy)
)

case e: Elem if e.label == "link" =>
fixElementAndAttributes(
e, "href", fixURL = false,
_fixHtml(e.child, inHtml, inHead, justHead, inBody, justBody, bodyHead, bodyTail, doMergy)
)

case e: Elem =>
fixElementAndAttributes(
e, "src", fixURL = true,
_fixHtml(e.child, inHtml, inHead, justHead, inBody, justBody, bodyHead, bodyTail, doMergy)
)

case c: Comment if stripComments => NodeSeq.Empty
case _ => v
}
Expand Down Expand Up @@ -203,7 +346,7 @@ private[http] trait LiftMerge {
bodyChildren += nl
}

val pageJs = assemblePageSpecificJavaScript
val pageJs = assemblePageSpecificJavaScript(eventAttributesByElementId)
if (pageJs.toJsCmd.trim.nonEmpty) {
addlTail += pageScopedScriptFileWith(pageJs)
}
Expand Down

0 comments on commit a21dfa0

Please sign in to comment.