|
The Lift Framework Req.scala source code
/*
* Copyright 2007-2011 WorldWide Conferencing, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.liftweb
package http
import java.io.{InputStream, ByteArrayInputStream, File, FileInputStream,
FileOutputStream}
import scala.xml._
import common._
import json._
import util._
import Helpers._
import http.provider._
import sitemap._
object UserAgentCalculator extends Factory {
/**
* The default regular expression for IE
*/
val iePattern = """MSIE ([0-9]+)""".r
/**
* You can change the mechanism by which the user agent for IE
* is calculated by doing the Factory thing with this object
*/
object ieCalcFunction extends FactoryMaker[Box[String] =>
Box[Double]](defaultIeCalcFunction _)
/**
* The built-in mechanism for calculating IE
*/
def defaultIeCalcFunction(userAgent: Box[String]): Box[Double] =
for {
ua <- userAgent
m = iePattern.pattern.matcher(ua)
ver <- if (m.find) Helpers.asDouble(m.group(1)) else Empty
} yield ver
/**
* The default regular expression for Safari
*/
val safariPattern = """Version.([0-9]+)[.0-9]+ ([^S])*Safari\/""".r
/**
* You can change the mechanism by which the user agent for Safari
* is calculated by doing the Factory thing with this object
*/
object safariCalcFunction extends FactoryMaker[Box[String] =>
Box[Double]](defaultSafariCalcFunction _)
/**
* The built-in mechanism for calculating Safari
*/
def defaultSafariCalcFunction(userAgent: Box[String]): Box[Double] =
for {
ua <- userAgent
m = safariPattern.pattern.matcher(ua)
ver <- if (m.find) Helpers.asDouble(m.group(1)) else Empty
} yield ver
/**
* The default regular expression for Firefox
*/
val firefoxPattern = """Firefox.([1-9][0-9]*\.[0-9])""".r
/**
* You can change the mechanism by which the user agent for Firefox
* is calculated by doing the Factory thing with this object
*/
object firefoxCalcFunction extends FactoryMaker[Box[String] =>
Box[Double]](defaultFirefoxCalcFunction _)
/**
* The built-in mechanism for calculating Firefox
*/
def defaultFirefoxCalcFunction(userAgent: Box[String]): Box[Double] =
for {
ua <- userAgent
m = firefoxPattern.pattern.matcher(ua)
ver <- if (m.find) Helpers.asDouble(m.group(1)) else Empty
} yield ver
/**
* The default regular expression for Chrome
*/
val chromePattern = """Chrome.([1-9][0-9]*\.[0-9])""".r
/**
* You can change the mechanism by which the user agent for Chrome
* is calculated by doing the Factory thing with this object
*/
object chromeCalcFunction extends FactoryMaker[Box[String] =>
Box[Double]](defaultChromeCalcFunction _)
/**
* You can change the mechanism by which Lift calculates
* if the User-Agent represents an iPhone. Put your
* special calculation function in here
*/
object iPhoneCalcFunction extends FactoryMaker[Box[Box[String] =>
Boolean]](Empty)
/**
* You can change the mechanism by which Lift calculates
* if the User-Agent represents an iPad. Put your
* special calculation function in here
*/
object iPadCalcFunction extends FactoryMaker[Box[Box[String] =>
Boolean]](Empty)
/**
* The built-in mechanism for calculating Chrome
*/
def defaultChromeCalcFunction(userAgent: Box[String]): Box[Double] =
for {
ua <- userAgent
m = chromePattern.pattern.matcher(ua)
ver <- if (m.find) Helpers.asDouble(m.group(1)) else Empty
} yield ver
}
trait UserAgentCalculator {
lazy val ieVersion: Box[Int] = UserAgentCalculator.ieCalcFunction.vend.apply(userAgent).map(_.toInt)
lazy val isIE6: Boolean = ieVersion.map(_ == 6) openOr false
lazy val isIE7: Boolean = ieVersion.map(_ == 7) openOr false
lazy val isIE8: Boolean = ieVersion.map(_ == 8) openOr false
lazy val isIE9: Boolean = ieVersion.map(_ == 9) openOr false
lazy val isIE = ieVersion.map(_ >= 6) openOr false
lazy val safariVersion: Box[Int] =
UserAgentCalculator.safariCalcFunction.vend.apply(userAgent).map(_.toInt)
def isSafari2: Boolean = false
lazy val isSafari3: Boolean = safariVersion.map(_ == 3) openOr false
lazy val isSafari4: Boolean = safariVersion.map(_ == 4) openOr false
lazy val isSafari5: Boolean = safariVersion.map(_ == 5) openOr false
def isSafari3_+ = safariVersion.map(_ >= 3) openOr false
def isSafari = safariVersion.isDefined
/**
* Is the Req coming from an iPhone
*/
lazy val isIPhone: Boolean =
UserAgentCalculator.iPhoneCalcFunction.vend.
map(_.apply(userAgent)) openOr
isSafari && (userAgent.map(s =>
s.indexOf("(iPhone") >= 0) openOr false)
/**
* Is the Req coming from an iPad
*/
lazy val isIPad: Boolean =
UserAgentCalculator.iPadCalcFunction.vend.
map(_.apply(userAgent)) openOr
isSafari && (userAgent.map(s =>
s.indexOf("(iPad") >= 0) openOr false)
lazy val firefoxVersion: Box[Double] =
UserAgentCalculator.firefoxCalcFunction.vend.apply(userAgent)
lazy val isFirefox2: Boolean = firefoxVersion.map(v => v >= 2d && v < 3d) openOr false
lazy val isFirefox3: Boolean = firefoxVersion.map(v => v >= 3d && v < 3.5d) openOr false
lazy val isFirefox35: Boolean = firefoxVersion.map(v => v >= 3.5d && v < 3.6d) openOr false
lazy val isFirefox36: Boolean = firefoxVersion.map(v => v >= 3.6d && v < 4d) openOr false
lazy val isFirefox40: Boolean = firefoxVersion.map(v => v >= 4d) openOr false
def isFirefox35_+ : Boolean = firefoxVersion.map(_ >= 3.5d) openOr false
def isFirefox = firefoxVersion.isDefined
lazy val chromeVersion: Box[Double] =
UserAgentCalculator.chromeCalcFunction.vend.apply(userAgent)
lazy val isChrome2 = chromeVersion.map(v => v >= 2d && v < 3d) openOr false
lazy val isChrome3 = chromeVersion.map(v => v >= 3d && v < 4d) openOr false
lazy val isChrome4 = chromeVersion.map(v => v >= 4d && v < 5d) openOr false
lazy val isChrome5 = chromeVersion.map(v => v >= 5d && v < 6d) openOr false
lazy val isChrome6 = chromeVersion.map(v => v >= 6d && v < 7d) openOr false
def isChrome3_+ = chromeVersion.map(_ >= 3d) openOr false
def isChrome = chromeVersion.isDefined
lazy val isOpera9: Boolean = (userAgent.map(s => s.indexOf("Opera/9.") >= 0) openOr false)
def isOpera = isOpera9
/**
* What's the user agent?
*/
def userAgent: Box[String]
}
@serializable
sealed trait ParamHolder {
def name: String
}
@serializable
final case class NormalParamHolder(name: String, value: String) extends ParamHolder
/**
* A FileParamHolder contains a file uploaded via a multipart
* form.
*
* @param name The name of the form field for this file
* @param mimeType the mime type, as specified in the Content-Type field
* @param fileName The local filename on the client
*/
@serializable
abstract class FileParamHolder(val name: String, val mimeType: String,
val fileName: String) extends ParamHolder
{
/**
* Returns the contents of the uploaded file as a Byte array.
*/
def file: Array[Byte]
/**
* Returns an input stream that can be used to read the
* contents of the uploaded file.
*/
def fileStream: InputStream
/**
* Returns the length of the uploaded file.
*/
def length : Long
}
/**
* This FileParamHolder stores the uploaded file directly into memory.
*
* @param name The name of the form field for this file
* @param mimeType the mime type, as specified in the Content-Type field
* @param fileName The local filename on the client
* @param file The contents of the uploaded file in a byte array
*/
class InMemFileParamHolder(override val name: String, override val mimeType: String,
override val fileName: String, val file: Array[Byte]) extends
FileParamHolder(name, mimeType, fileName)
{
/**
* Returns an input stream that can be used to read the
* contents of the uploaded file.
*/
def fileStream: InputStream = new ByteArrayInputStream(file)
/**
* Returns the length of the uploaded file.
*/
def length : Long = if (file == null) 0 else file.length
}
/**
* This FileParamHolder stores the uploaded file in a
* temporary file on disk.
*
* @param name The name of the form field for this file
* @param mimeType the mime type, as specified in the Content-Type field
* @param fileName The local filename on the client
* @param localFile The local copy of the uploaded file
*/
class OnDiskFileParamHolder(override val name: String, override val mimeType: String,
override val fileName: String, val localFile: File) extends
FileParamHolder(name, mimeType, fileName)
{
/**
* Returns an input stream that can be used to read the
* contents of the uploaded file.
*/
def fileStream: InputStream = new FileInputStream(localFile)
/**
* Returns the contents of the uploaded file as a Byte array.
*/
def file: Array[Byte] = Helpers.readWholeStream(fileStream)
/**
* Returns the length of the uploaded file.
*/
def length : Long = if (localFile == null) 0 else localFile.length
protected override def finalize {
tryo(localFile.delete)
}
}
object OnDiskFileParamHolder {
def apply(n: String, mt: String, fn: String, inputStream: InputStream): OnDiskFileParamHolder =
{
val file: File = File.createTempFile("lift_mime", "upload")
val fos = new FileOutputStream(file)
val ba = new Array[Byte](8192)
def doUpload() {
inputStream.read(ba) match {
case x if x < 0 =>
case 0 => doUpload()
case x => fos.write(ba, 0, x); doUpload()
}
}
doUpload()
inputStream.close
fos.close
new OnDiskFileParamHolder(n, mt, fn, file)
}
}
object FileParamHolder {
def apply(n: String, mt: String, fn: String, file: Array[Byte]): FileParamHolder =
new InMemFileParamHolder(n, mt, fn, file)
def unapply(in: Any): Option[(String, String, String, Array[Byte])] = in match {
case f: FileParamHolder => Some((f.name, f.mimeType, f.fileName, f.file))
case _ => None
}
}
private[http] object CurrentReq extends ThreadGlobal[Req]
private final case class AvoidGAL(func: () => ParamCalcInfo) {
lazy val thunk: ParamCalcInfo = func()
}
/**
* Helper object for constructing Req instances
*/
object Req {
object NilPath extends ParsePath(Nil, "", true, false)
private[http] lazy val localHostName = {
import java.net._
InetAddress.getLocalHost.getHostName
}
def apply(original: Req, rewrite: List[LiftRules.RewritePF]): Req =
this.apply(original, rewrite, Nil, Nil)
def apply(original: Req, rewrite: List[LiftRules.RewritePF], statelessTest: List[LiftRules.StatelessTestPF],
otherStatelessTest: List[LiftRules.StatelessReqTestPF]): Req = {
def processRewrite(path: ParsePath, params: Map[String, String]): RewriteResponse =
NamedPF.applyBox(RewriteRequest(path, original.requestType, original.request), rewrite) match {
case Full(resp@RewriteResponse(_, _, true)) => resp
case _: EmptyBox => RewriteResponse(path, params)
case Full(resp) => processRewrite(resp.path, params ++ resp.params)
}
val rewritten = processRewrite(original.path, Map.empty)
val wholePath = rewritten.path.wholePath
val stateless = NamedPF.applyBox(StatelessReqTest(wholePath, original.request), otherStatelessTest) or
NamedPF.applyBox(wholePath, statelessTest)
new Req(rewritten.path, original.contextPath,
original.requestType, original.contentType, original.request,
original.nanoStart, original.nanoEnd,
stateless openOr original.stateless_?,
original.paramCalculator, original.addlParams ++ rewritten.params)
}
def apply(request: HTTPRequest, rewrite: List[LiftRules.RewritePF], nanoStart: Long): Req =
this.apply(request, rewrite, Nil, Nil, nanoStart)
def apply(request: HTTPRequest, rewrite: List[LiftRules.RewritePF], statelessTest: List[LiftRules.StatelessTestPF],
otherStatelessTest: List[LiftRules.StatelessReqTestPF], nanoStart: Long): Req = {
val reqType = RequestType(request)
val contextPath = LiftRules.calculateContextPath() openOr request.contextPath
val turi = if (request.uri.length >= contextPath.length) request.uri.substring(contextPath.length) else ""
val tmpUri = if (turi.length > 0) turi else "/"
val tmpPath = parsePath(tmpUri)
def processRewrite(path: ParsePath, params: Map[String, String]): RewriteResponse =
NamedPF.applyBox(RewriteRequest(path, reqType, request), rewrite) match {
case Full(resp@RewriteResponse(_, _, true)) => resp
case _: EmptyBox => RewriteResponse(path, params)
case Full(resp) => processRewrite(resp.path, params ++ resp.params)
}
// val (uri, path, localSingleParams) = processRewrite(tmpUri, tmpPath, TreeMap.empty)
val rewritten = processRewrite(tmpPath, Map.empty)
val localParams: Map[String, List[String]] = Map(rewritten.params.toList.map {case (name, value) => name -> List(value)}: _*)
// val session = request.getSession
// val body = ()
val eMap = Map.empty[String, List[String]]
val contentType = request.contentType
// val (paramNames: List[String], params: Map[String, List[String]], files: List[FileParamHolder], body: Box[Array[Byte]]) =
// calculate the query parameters
lazy val queryStringParam: (List[String], Map[String, List[String]]) = {
val params: List[(String, String)] =
for {
queryString <- request.queryString.toList
nameVal <- queryString.split("&").toList.map(_.trim).filter(_.length > 0)
(name, value) <- nameVal.split("=").toList match {
case Nil => Empty
case n :: v :: _ => Full((urlDecode(n), urlDecode(v)))
case n :: _ => Full((urlDecode(n), ""))
}} yield (name, value)
val names: List[String] = params.map(_._1).distinct
val nvp: Map[String, List[String]] = params.foldLeft(Map[String, List[String]]()) {
case (map, (name, value)) => map + (name -> (map.getOrElse(name, Nil) ::: List(value)))
}
(names, nvp)
}
// make this a thunk so it only gets calculated once
val paramCalcInfo: AvoidGAL = new AvoidGAL(() => {
// post/put of XML or JSON... eagerly read the stream
if ((reqType.post_? ||
reqType.put_?) && contentType.dmap(false){
_.toLowerCase match {
case x =>
x.startsWith("text/xml") ||
x.startsWith("application/xml") ||
x.startsWith("text/json") ||
x.startsWith("application/json")
}}) {
ParamCalcInfo(queryStringParam._1,
queryStringParam._2 ++ localParams,
Nil,
Full(BodyOrInputStream(request.inputStream)))
// it's multipart
} else if (request multipartContent_?) {
val allInfo = request extractFiles
val normal: List[NormalParamHolder] =
allInfo.flatMap {
case v: NormalParamHolder => List(v)
case _ => Nil}
val files: List[FileParamHolder] = allInfo.flatMap {
case v: FileParamHolder => List(v)
case _ => Nil}
val params = normal.foldLeft(eMap)((a, b) =>
a + (b.name -> (a.getOrElse(b.name, Nil) ::: List(b.value))))
ParamCalcInfo((queryStringParam._1 :::
normal.map(_.name)).distinct,
queryStringParam._2 ++ localParams ++
params, files, Empty)
// it's a GET
} else if (reqType.get_?) {
ParamCalcInfo(queryStringParam._1,
queryStringParam._2 ++ localParams, Nil, Empty)
} else if (contentType.dmap(false)(_.toLowerCase.
startsWith("application/x-www-form-urlencoded"))) {
val params = localParams ++ (request.params.sortWith
{(s1, s2) => s1.name < s2.name}).
map(n => (n.name, n.values))
ParamCalcInfo(request paramNames, params, Nil, Empty)
} else {
ParamCalcInfo(queryStringParam._1,
queryStringParam._2 ++ localParams,
Nil, Full(BodyOrInputStream(request inputStream)))
}
})
val paramCalculator: () => ParamCalcInfo = () => {
paramCalcInfo.thunk
}
val wholePath = rewritten.path.wholePath
val stateless = NamedPF.applyBox(StatelessReqTest(wholePath, request), otherStatelessTest) or
NamedPF.applyBox(wholePath, statelessTest)
new Req(rewritten.path, contextPath, reqType,
contentType, request, nanoStart,
System.nanoTime, stateless openOr false, paramCalculator, Map())
}
private def fixURI(uri: String) = uri indexOf ";jsessionid" match {
case -1 => uri
case x@_ => uri.substring(0, x)
}
/**
* Create a nil request... useful for testing
*/
def nil = new Req(NilPath, "", GetRequest, Empty, null,
System.nanoTime, System.nanoTime, false,
() => ParamCalcInfo(Nil, Map.empty, Nil, Empty), Map())
def parsePath(in: String): ParsePath = {
val p1 = fixURI((in match {case null => "/"; case s if s.length == 0 => "/"; case s => s}).replaceAll("/+", "/"))
val front = p1.startsWith("/")
val back = p1.length > 1 && p1.endsWith("/")
val orgLst = p1.replaceAll("/$", "/index").split("/").
toList.map(_.trim).filter(_.length > 0)
val (lst, suffix) = NamedPF(orgLst, LiftRules.suffixSplitters.toList)
ParsePath(lst.map(urlDecode), suffix, front, back)
}
var fixHref = _fixHref _
private def _fixHref(contextPath: String, v: Seq[Node], fixURL: Boolean, rewrite: Box[String => String]): Text = {
val hv = v.text
val updated = if (hv.startsWith("/") &&
!LiftRules.excludePathFromContextPathRewriting.vend(hv)) contextPath + hv else hv
Text(if (fixURL && rewrite.isDefined &&
!updated.startsWith("mailto:") &&
!updated.startsWith("javascript:") &&
!updated.startsWith("http://") &&
!updated.startsWith("https://") &&
!updated.startsWith("#"))
rewrite.open_!.apply(updated) else updated)
}
/**
* Corrects the HTML content,such as applying context path to URI's, session information if cookies are disabled etc.
*/
def fixHtml(contextPath: String, in: NodeSeq): NodeSeq = {
val rewrite = URLRewriter.rewriteFunc
def fixAttrs(toFix: String, attrs: MetaData, fixURL: Boolean): MetaData = {
if (attrs == Null) Null
else if (attrs.key == toFix) {
new UnprefixedAttribute(toFix, Req.fixHref(contextPath, attrs.value, fixURL, rewrite), fixAttrs(toFix, attrs.next, fixURL))
} else attrs.copy(fixAttrs(toFix, attrs.next, fixURL))
}
def _fixHtml(contextPath: String, in: NodeSeq): NodeSeq = {
in.map {
v =>
v match {
case Group(nodes) => Group(_fixHtml(contextPath, nodes))
case e: Elem if e.label == "form" => Elem(v.prefix, v.label, fixAttrs("action", v.attributes, true), v.scope, _fixHtml(contextPath, v.child): _*)
case e: Elem if e.label == "script" => Elem(v.prefix, v.label, fixAttrs("src", v.attributes, false), v.scope, _fixHtml(contextPath, v.child): _*)
case e: Elem if e.label == "a" => Elem(v.prefix, v.label, fixAttrs("href", v.attributes, true), v.scope, _fixHtml(contextPath, v.child): _*)
case e: Elem if e.label == "link" => Elem(v.prefix, v.label, fixAttrs("href", v.attributes, false), v.scope, _fixHtml(contextPath, v.child): _*)
case e: Elem => Elem(v.prefix, v.label, fixAttrs("src", v.attributes, true), v.scope, _fixHtml(contextPath, v.child): _*)
case _ => v
}
}
}
_fixHtml(contextPath, in)
}
private[liftweb] def defaultCreateNotFound(in: Req) =
XhtmlResponse((<html> The Requested URL {in.contextPath + in.uri} was not found on this server |