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

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

The newest version!
/*
 * Copyright 2007-2017 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}
import java.nio.file.{Files, Path}
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]+)|Trident/7.*rv:([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 {
      userAgent <- userAgent
      ieMatch = iePattern.pattern.matcher(userAgent)
      findResult = ieMatch.find if findResult
      ieVersionString <- Box.legacyNullTest(ieMatch.group(2)) or Box.legacyNullTest(ieMatch.group(3))
      ver <- Helpers.asDouble(ieVersionString)
    } 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 ieIE10: Boolean = ieVersion.map(_ == 10) openOr false
  lazy val isIE11: Boolean = ieVersion.map(_ == 11) openOr false
  lazy val isIE = ieVersion.isDefined

  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]
}

sealed trait ParamHolder extends Serializable{
  def name: String
}

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
 */
abstract class FileParamHolder(val name: String, val mimeType: String,
                               val fileName: String) extends ParamHolder with Serializable
{
  /**
   * 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 localPath: Path) extends
  FileParamHolder(name, mimeType, fileName)
{
  def this(name: String, mimeType: String, fileName: String, localFile: File) = {
    this(name, mimeType, fileName, localFile.toPath)
  }

  def localFile: File = localPath.toFile

  /**
   * Returns an input stream that can be used to read the
   * contents of the uploaded file.
   */
  def fileStream: InputStream = Files.newInputStream(localPath)

  /**
   * 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 (localPath == null) 0 else Files.size(localPath)

  protected override def finalize: Unit = {
    tryo(Files.delete(localPath))
  }
}

object OnDiskFileParamHolder {
  def apply(n: String, mt: String, fn: String, inputStream: InputStream): OnDiskFileParamHolder =
  {
    val file: Path = Files.createTempFile("lift_mime", "upload")
    val fos = Files.newOutputStream(file)
    val ba = new Array[Byte](8192)
    def doUpload(): Unit = {
      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)
  }

  def apply(n: String, mt: String, fn: String, file: File): OnDiskFileParamHolder =
    new OnDiskFileParamHolder(n, mt, fn, file.toPath)

}

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
  }
}

/**
 * A Thread-global containing the current Req.  Set very, very early
 */
object CurrentReq extends ThreadGlobal[Req]

/**
 * A ThreadGlobal containing the low-level HTTPRequest and HTTPResponse instances
 * Set very, very early.
 */
object CurrentHTTPReqResp extends ThreadGlobal[(HTTPRequest, HTTPResponse)]

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()) {
    override lazy val standardRequest_? = false
  }

  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)
  }

  @deprecated("Use normalizeHref instead.", "3.0.0")
  def fixHref = normalizeHref

  var normalizeHref = _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("/") &&
                      !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("//") &&
             !updated.startsWith("#"))
         rewrite.openOrThrowException("legacy code").apply(updated) else updated)
  }

  @deprecated("Use normalizeHtml instead.", "3.0.0")
  def fixHtml(contextPath: String, in: NodeSeq): NodeSeq = {
    normalizeHtml(contextPath, in)
  }

  /**
   * Corrects the HTML content,such as applying context path to URI's, session information if cookies are disabled etc.
   */
  def normalizeHtml(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.normalizeHref(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, e.minimizeEmpty, _fixHtml(contextPath, v.child): _*)
          case e: Elem if e.label == "script" => Elem(v.prefix, v.label, fixAttrs("src", v.attributes, false), v.scope, e.minimizeEmpty, _fixHtml(contextPath, v.child): _*)
          case e: Elem if e.label == "a" => Elem(v.prefix, v.label, fixAttrs("href", v.attributes, true), v.scope, e.minimizeEmpty, _fixHtml(contextPath, v.child): _*)
          case e: Elem if e.label == "link" => Elem(v.prefix, v.label, fixAttrs("href", v.attributes, false), v.scope, e.minimizeEmpty, _fixHtml(contextPath, v.child): _*)
          case e: Elem => Elem(v.prefix, v.label, fixAttrs("src", v.attributes, true), v.scope, e.minimizeEmpty, _fixHtml(contextPath, v.child): _*)
          case _ => v
        }
      }
    }
    _fixHtml(contextPath, in)
  }

  private[liftweb] def defaultCreateNotFound(in: Req) =
  XhtmlResponse(( The Requested URL {in.contextPath + in.uri} was not found on this server ),
                LiftRules.htmlProperties.vend(in).docType, List("Content-Type" -> "text/html; charset=utf-8"), Nil, 404, S.legacyIeCompatibilityMode)

  def unapply(in: Req): Option[(List[String], String, RequestType)] = Some((in.path.partPath, in.path.suffix, in.requestType))
}

/**
 * Holds either the body or the request input stream, depending on which was requested first
 */
final case class BodyOrInputStream(is: InputStream) {
  private var calc = false
  lazy val body: Box[Array[Byte]] = synchronized {
    if (calc) Empty
    else {
      calc = true
      tryo(readWholeStream(is))
    }
  }

  lazy val stream: Box[InputStream] = synchronized {
    if (calc) Empty
    else {
      calc = true
      Full(is)
    }
  }
}

final case class ParamCalcInfo(paramNames: List[String],
                               params: Map[String, List[String]],
                               uploadedFiles: List[FileParamHolder],
                               body: Box[BodyOrInputStream])


/**
 * Holds information about the content type and subtype including
 * the q parameter and extension information.
 */
final case class ContentType(theType: String, 
                             subtype: String, 
                             order: Int,
                             q: Box[Double], 
                             extension: List[(String, String)]) extends Ordered[ContentType]
  {
    /**
     * Compares this to another ContentType instance based on the q
     * and if the q matches, compare based on specialization (* vs.
     * explicit and then order.
     */
    def compare(that: ContentType): Int = ((that.q openOr 1d) compare (q openOr 1d)) match {
      case 0 => 
        def doDefault = {
          order compare that.order
        }

        (theType, that.theType, subtype, that.subtype) match {
          case ("*", "*", _, _) => doDefault
          case ("*", _, _, _) => 1
          case (_, "*", _, _) => -1
          case (_, _, "*", "*") => doDefault
          case (_, _, "*", _) => 1
          case (_, _, _, "*") => -1
          case _ => doDefault
        }
      case x => x
    }

    /**
     * Does this ContentType match the String including
     * wildcard support
     */
    def matches(contentType: (String, String)): Boolean =
      (theType == "*" || (theType == contentType._1)) &&
    (subtype == "*" || subtype == contentType._2)
    
    /**
     * Is it a wildcard
     */
    def wildCard_? = theType == "*" && subtype == "*"
  }

/**
 * The ContentType companion object that has helper methods
 * for parsing Accept headers and other things that
 * contain multiple ContentType information.
 */
object ContentType {
  /**
   * Parse the String into a series of ContentType instances,
   * returning the multiple ContentType instances
   */
  def parse(str: String): List[ContentType] = 
    (for {
      (part, index) <- str.charSplit(',').
      map(_.trim).zipWithIndex // split at comma
      content <- parseIt(part, index)
    } yield content).sortWith(_ < _)

  private object TwoType {
    def unapply(in: String): Option[(String, String)] = 
      in.charSplit('/') match {
        case a :: b :: Nil => Some(a -> b)
        case _ => None
      }
  }


  private object EqualsSplit {
    private def removeQuotes(s: String) = 
      if (s.startsWith("\"") && s.endsWith("\"")) s.substring(1, s.length - 1)
      else s

    def unapply(in: String): Option[(String, String)] = in.roboSplit("=") match {
      case a :: b :: Nil => Some(a -> removeQuotes(b))
      case _ => None
    }
  }

  private def parseIt(content: String, index: Int): Box[ContentType] = content.roboSplit(";") match {
    case TwoType(typ, subType) :: xs => {
      val kv = xs.flatMap(EqualsSplit.unapply) // get the key/value pairs
      val q: Box[Double] = first(kv){
        case (k, v) if k == "q" => Helpers.asDouble(v)
        case _ => Empty
      }
      Full(ContentType(typ, subType, index, q, kv.filter{_._1 != "q"}))
    }

    case _ => Empty
  }
}



/**
 * Contains request information
 */
class Req(val path: ParsePath,
          val contextPath: String,
          val requestType: RequestType,
          val contentType: Box[String],
          val request: HTTPRequest,
          val nanoStart: Long,
          val nanoEnd: Long,
          _stateless_? : Boolean,
          private[http] val paramCalculator: () => ParamCalcInfo,
          private[http] val addlParams: Map[String, String]) extends HasParams with UserAgentCalculator
{
  override def toString = "Req(" + paramNames + ", " + params + ", " + path +
  ", " + contextPath + ", " + requestType + ", " + contentType + ")"

  def this(_path: ParsePath,
           _contextPath: String,
          _requestType: RequestType,
          _contentType: Box[String],
          _request: HTTPRequest,
          _nanoStart: Long,
          _nanoEnd: Long,
          _paramCalculator: () => ParamCalcInfo,
          _addlParams: Map[String, String]) = this(_path,
                                                   _contextPath,
                                                   _requestType,
                                                   _contentType,
                                                   _request,
                                                   _nanoStart,
                                                   _nanoEnd,
                                                   false,
                                                   _paramCalculator,
                                                   _addlParams)

  /**
   * Build a new Req, the same except with a different path.
   * Useful for creating Reqs with sub-paths.
   */
  def withNewPath(newPath: ParsePath): Req = {
    val outer = this

    new Req(newPath,
            contextPath,
            requestType,
            contentType,
            request,
            nanoStart,
            nanoEnd,
            _stateless_?,
            paramCalculator,
            addlParams) {
      override lazy val json: Box[JsonAST.JValue] = outer.json

      override lazy val xml: Box[Elem] = outer.xml

      override lazy val location: Box[Loc[_]] = outer.location

      override lazy val buildMenu: CompleteMenu = outer.buildMenu

      /**
       * the accept header
       */
      override lazy val accepts: Box[String] = outer.accepts
    
      /**
       * What is the content type in order of preference by the requester
       * calculated via the Accept header
       */
      override lazy val weightedAccept: List[ContentType] = 
        outer.weightedAccept

      /**
       * Returns true if the request accepts XML
       */
      override lazy val acceptsXml_? = outer.acceptsXml_?

      /**
       * Returns true if the request accepts JSON
       */
      override lazy val acceptsJson_? = outer.acceptsJson_?

      /**
       * Is the Accepts Header * / *
       */
      override lazy val acceptsStarStar : Boolean = outer.acceptsStarStar

      /**
       * Returns true if the request accepts JavaScript
       */
      override lazy val acceptsJavaScript_? = 
        outer.acceptsJavaScript_?
    }
  }

  /**
   * Should the request be treated as stateless (no session created for it)?
   */
  lazy val stateless_? = {
    val ret = _stateless_? || (location.map(_.stateless_?) openOr false)
    ret
  }

  /**
   * Returns true if the content-type is text/xml or application/xml
   */
  def xml_? = contentType != null && contentType.dmap(false){
    _.toLowerCase match {
      case x if x.startsWith("text/xml") => true
      case x if x.startsWith("application/xml") => true
      case _ => false
    }
  }

  /**
   * Returns true if the content-type is text/json or application/json
   */
  def json_? = contentType != null && contentType.dmap(false){
    _.toLowerCase match {
      case x if x.startsWith("text/json") => true
      case x if x.startsWith("application/json") => true
      case _ => false
    }
  }

  /**
   * Returns true if the X-Requested-With header is set to XMLHttpRequest.
   *
   * Most ajax frameworks, including jQuery and Prototype, set this header
   * when doing any ajax request.
   */
  def ajax_? =
    request.headers.toList.exists { header =>
      (header.name equalsIgnoreCase "x-requested-with") &&
      (header.values.exists(_ equalsIgnoreCase "xmlhttprequest"))
    }

  /**
   * A request that is neither Ajax or Comet
   */
  lazy val standardRequest_? : Boolean = path.partPath match {
    case x :: _ if x == LiftRules.liftContextRelativePath => false
    case _ => true
  }

  /**
   * Make the servlet session go away
   */
  def destroyServletSession(): Unit = {
    for {
      httpReq <- Box !! request
    } httpReq.destroyServletSession()
  }

  /**
   * A snapshot of the request that can be passed off the current thread
   *
   * @return a copy of the Req
   */
  def snapshot: Req = {
    val paramCalc = paramCalculator()
    paramCalc.body.map(_.body) // make sure we grab the body
    new Req(path,
            contextPath,
            requestType,
            contentType,
            request.snapshot,
            nanoStart,
            nanoEnd,
            stateless_?,
            () => paramCalc,
            addlParams)
  }
  val section = path(0) match {case null => "default"; case s => s}
  val view = path(1) match {case null => "index"; case s@_ => s}

  val id = pathParam(0)

  def pathParam(n: Int) = path.wholePath.drop(n + 2).headOption getOrElse ""

  def path(n: Int): String = path.wholePath.drop(n).headOption getOrElse ""

  def param(n: String): Box[String] =
    params.get(n) match {
    case Some(s :: _) => Full(s)
    case _ => Empty
  }

  lazy val headers: List[(String, String)] =
  for (h <- request.headers;
       p <- h.values
  ) yield (h.name, p)


  def headers(name: String): List[String] = headers.filter(_._1.equalsIgnoreCase(name)).map(_._2)

  def header(name: String): Box[String] = headers(name) match {
    case x :: _ => Full(x)
    case _ => Empty
  }

  /**
   * Get the name of the params
   */
  def paramNames: List[String] = _paramNames

  /**
   * the raw parameters, use params
   */
  def _params: Map[String, List[String]] = __params

  /**
   * The uploaded files
   */
  def uploadedFiles: List[FileParamHolder] = _uploadedFiles

  /**
   * The POST or PUT body.  This will be empty if the content
   * type is application/x-www-form-urlencoded or a multipart mime.
   * It will also be empty if rawInputStream is accessed
   */
  def body: Box[Array[Byte]] = _body.flatMap(_.body)

  /**
   * The raw input stream of a POST or PUT that is not
   * application/x-www-form-urlencoded or a multipart mime
   * and if this method is called before the body method.
   * Remember to close the stream when done.
   */
  def rawInputStream: Box[InputStream] = _body.flatMap(_.stream)

  private lazy val ParamCalcInfo(_paramNames /*: List[String]*/,
            __params /*: Map[String, List[String]]*/,
            _uploadedFiles /*: List[FileParamHolder]*/,
            _body /*: Box[BodyOrInputStream]*/) = {
    val ret = paramCalculator()
    ret
  }

  lazy val params: Map[String, List[String]] = addlParams.foldLeft(_params){
    case (map, (key, value)) => map + (key -> (value :: map.getOrElse(key, Nil)))
  }

  lazy val cookies = request.cookies match {
    case null => Nil
    case ca => ca.toList
  }

  /**
   * Get the session ID if there is one without creating on
   */
  def sessionId: Box[String] =
    for {
      httpRequest <- Box !! request
      sid <- httpRequest.sessionId
    } yield sid

  /**
   * The JValue representation of this Req's body, if the body is JSON-parsable
   * AND the content-type of the request is JSON. Returns a Failure if
   * the request is not considered a JSON request (see json_?), or if
   * there was an error parsing the JSON.
   *
   * If you want to forcibly evaluate the request body as JSON, ignoring
   * content type, see `forcedBodyAsJson`.
   */
  lazy val json: Box[JsonAST.JValue] = {
    if (!json_?) {
      Failure("Cannot parse non-JSON request as JSON; please check content-type.")
    } else {
      forcedBodyAsJson
    }
  }

  /**
   * Forcibly tries to parse the request body as JSON. Does not perform any
   * content type checks, unlike the json method.
   */
  lazy val forcedBodyAsJson: Box[JsonAST.JValue] = {
    try {
      import java.io._

      def r = """; *charset=(.*)""".r
      def r2 = """[^=]*$""".r

      def charSet: String = contentType.flatMap(ct => r.findFirstIn(ct).flatMap(r2.findFirstIn)).getOrElse("UTF-8")
      
      body.map(b => 
        JsonParser.parse(new 
                         InputStreamReader(new 
                                           ByteArrayInputStream(b), charSet)))
    } catch {
      case e: LiftFlowOfControlException => throw e
      case e: Exception => Failure(e.getMessage, Full(e), Empty)
    }
  }

  private def containerRequest = Box !! request
    /**
   * The hostname to which the request was sent. This is taken from the "Host" HTTP header, or if that
   * does not exist, the DNS name or IP address of the server.
   */
  lazy val hostName: String = containerRequest.map(_.serverName) openOr Req.localHostName

  /**
   * The host and path of the request up to and including the context path. This does
   * not include the template path or query string.
   */
  lazy val hostAndPath: String =
    containerRequest.map(r => (r.scheme, r.serverPort) match {
      case ("http", 80) if r.header("X-SSL").isDefined => "https://" + r.serverName + contextPath
      case ("http", 80) => "http://" + r.serverName + contextPath
      case ("https", 443) => "https://" + r.serverName + contextPath
      case (sch, port) => sch + "://" + r.serverName + ":" + port + contextPath
    }) openOr ""

  /**
   * The Elem representation of this Req's body, if the body is XML-parsable
   * AND the content-type of the request is XML. Returns a Failure if
   * the request is not considered a XML request (see xml_?), or if
   * there was an error parsing the XML.
   *
   * If you want to forcibly evaluate the request body as XML, ignoring
   * content type, see `forcedBodyAsXml`.
   */
  lazy val xml: Box[Elem] = {
    if (!xml_?) {
      Failure("Cannot parse non-XML request as XML; please check content-type.")
    } else  {
      forcedBodyAsXml
    }
  }

  /**
   * Forcibly tries to parse the request body as XML. Does not perform any
   * content type checks, unlike the xml method.
   */
  lazy val forcedBodyAsXml: Box[Elem] = {
    try {
      import java.io._
      body.map(b => secureXML.load(new ByteArrayInputStream(b)))
    } catch {
      case e: LiftFlowOfControlException => throw e
      case e: Exception => Failure(e.getMessage, Full(e), Empty)
    }
  }

  /**
   * The SiteMap Loc associated with this Req
   */
  lazy val location: Box[Loc[_]] = LiftRules.siteMap.flatMap(_.findLoc(this))

  /**
   * Test the current SiteMap Loc for access control to insure
   * that this Req is allowed to access the page
   */
  def testLocation: Either[Boolean, Box[LiftResponse]] = {
    if (LiftRules.siteMap.isEmpty) Left(true)
    else location.map(_.testAccess) match {
      case Full(Left(true)) => Left(true)
      case Full(Right(Full(resp))) =>
        object theResp extends RequestVar(resp.apply())
        Right(Full(theResp.is))
      case _ => Right(Empty)
    }
  }


  lazy val buildMenu: CompleteMenu = location.map(_.buildMenu) openOr
  CompleteMenu(Nil)

  private def initIfUnitted[T](f: T): T = S.session match {
    case Full(_) => f
    case _ => S.statelessInit(this)(f)
  }


  /**
   * Compute the Not Found via a Template
   */
  private def notFoundViaTemplate(path: ParsePath): LiftResponse = {
    this.initIfUnitted {
      (for {
        session <- S.session
        template =  Templates(path.partPath)
        resp <- session.processTemplate(template, this, path, 404)
      } yield resp) match {
        case Full(resp) => resp
        case _ => Req.defaultCreateNotFound(this)
      }
    }
  }

  def createNotFound: LiftResponse = 
    NamedPF((this, Empty), LiftRules.uriNotFound.toList) match {
      case DefaultNotFound => Req.defaultCreateNotFound(this)
      case NotFoundAsTemplate(path) => notFoundViaTemplate(path)
      case NotFoundAsResponse(resp) => resp
      case NotFoundAsNode(node) => LiftRules.convertResponse((node, 404),
        S.getResponseHeaders(LiftRules.defaultHeaders((node, this))),
        S.responseCookies,
        this)
    }

  def createNotFound(f: Failure): LiftResponse = 
    NamedPF((this, Full(f)), LiftRules.uriNotFound.toList) match {
      case DefaultNotFound => Req.defaultCreateNotFound(this)
      case NotFoundAsTemplate(path) => notFoundViaTemplate(path)
      case NotFoundAsResponse(resp) => resp
      case NotFoundAsNode(node) => LiftRules.convertResponse((node, 404),
        S.getResponseHeaders(LiftRules.defaultHeaders((node, this))),
        S.responseCookies,
        this)
    }


  private[http] def createNotFound(f: (ParsePath) => Box[LiftResponse]): Box[LiftResponse] = 
    NamedPF((this, Empty), LiftRules.uriNotFound.toList) match {
      case DefaultNotFound => Full(Req.defaultCreateNotFound(this))
      case NotFoundAsResponse(resp) => Full(resp)
      case NotFoundAsTemplate(path) => 
         val newReq = new Req(path, 
                              this.contextPath, 
                              this.requestType, 
                              this.contentType, 
                              this.request,
                              this.nanoStart, 
                              this.nanoEnd, 
                              true,
                              this.paramCalculator, 
                              this.addlParams)
         S.withReq(Full(newReq)) {
          f(path)
         }
      case NotFoundAsNode(node) => Full(LiftRules.convertResponse((node, 404),
        S.getResponseHeaders(LiftRules.defaultHeaders((node, this))),
        S.responseCookies,
        this))
    }
  
  val post_? = requestType.post_?

  val patch_? = requestType.patch_?

  val get_? = requestType.get_?

  val put_? = requestType.put_?

  val options_? = requestType.options_?

  @deprecated("Use normalizeHtml instead.", "3.0.0")
  def fixHtml(in: NodeSeq): NodeSeq = normalizeHtml(in)
  
  def normalizeHtml(in: NodeSeq): NodeSeq = Req.normalizeHtml(contextPath, in)

  lazy val uri: String = request match {
    case null => "Outside HTTP Request (e.g., on Actor)"
    case request =>
      val ret = for {uri <- Box.legacyNullTest(request.uri)
                     cp <- Box.legacyNullTest(contextPath)
                     part <- (request.uri.length >= cp.length) match {
                              case true => Full(request.uri.substring(cp.length))
                              case _ => Empty}} yield {
           part match {
            case "" => "/"
            case x => Req.fixURI(x)
          }
      }
      ret openOr "/"
  }

  /**
   * The IP address of the request
   */
  def remoteAddr: String = request.remoteAddress

  /**
   * Parse the if-modified-since header and return the milliseconds since epoch
   * of the parsed date.
   */
  lazy val ifModifiedSince: Box[java.util.Date] =
  for{req <- Box !! request
      ims <- req.header("if-modified-since")
      id <- boxParseInternetDate(ims)
  } yield id

  def testIfModifiedSince(when: Long): Boolean = (when == 0L) ||
  ((when / 1000L) > ((ifModifiedSince.map(_.getTime) openOr 0L) / 1000L))

  def testFor304(lastModified: Long, headers: (String, String)*): Box[LiftResponse] =
  if (!testIfModifiedSince(lastModified))
  Full(InMemoryResponse(new Array[Byte](0), headers.toList, Nil, 304))
  else
  Empty

  /**
   * The user agent of the browser that sent the request
   */
  lazy val userAgent: Box[String] =
  for (r <- Box.legacyNullTest(request);
       uah <- request.header("User-Agent"))
  yield uah


  /**
   * the accept header
   */
  lazy val accepts: Box[String] = {
    request.headers.toList.
    filter(_.name equalsIgnoreCase "accept").flatMap(_.values) match {
      case Nil => Empty
      case xs => Full(xs.mkString(", "))
    }
  }
    
  /**
   * What is the content type in order of preference by the requester
   * calculated via the Accept header
   */
  lazy val weightedAccept: List[ContentType] = accepts match {
    case Full(a) => ContentType.parse(a)
    case _ => Nil
  }

  /**
   * Returns true if the request accepts XML
   */
  lazy val acceptsXml_? =
    (weightedAccept.find(_.matches("text" -> "xml")) orElse
     weightedAccept.find(_.matches("application" -> "xml"))).isDefined

  /**
   * Returns true if the request accepts JSON
   */
  lazy val acceptsJson_? =
    (weightedAccept.find(_.matches("text" -> "json")) orElse
     weightedAccept.find(_.matches("application" -> "json"))).isDefined

  /**
   * Is the Accepts Header * / *
   */
  lazy val acceptsStarStar : Boolean = accepts.map(_ == "*/*") openOr false

  /**
   * Returns true if the request accepts JavaScript
   */
  lazy val acceptsJavaScript_? = 
    (weightedAccept.find(_.matches("text" -> "javascript")) orElse
     weightedAccept.find(_.matches("application" -> "javascript"))).
    isDefined

  def updateWithContextPath(uri: String): String = if (uri.startsWith("/")) contextPath + uri else uri
}

/**
 * This case class is used for pattern matching.  See LiftRules.statelessRewrite and LiftRules.statefulRewrite
 */
final case class RewriteRequest(path: ParsePath, requestType: RequestType, httpRequest: HTTPRequest)

/**
 * The representation of an URI path
 */
case class ParsePath(partPath: List[String], suffix: String, absolute: Boolean, endSlash: Boolean) {
  def drop(cnt: Int) = ParsePath(partPath.drop(cnt), suffix, absolute, endSlash)

  lazy val wholePath = 
    if (suffix.length > 0) {
      partPath.dropRight(1) ::: List((partPath match {
        case Nil => ""
        case xs => xs.last}) + "." + suffix)
    } else {
      partPath
    }
}

final case class RewriteResponse(path: ParsePath, 
                                 params: Map[String, String],
                                 stopRewriting: Boolean)

/**
 * Maintains the context of resolving the URL when cookies are disabled from container. It maintains
 * low coupling such as code within request processing is not aware of the actual response that
 * encodes the URL.
 */
object RewriteResponse {
  def apply(path: List[String], params: Map[String, String]) = new RewriteResponse(ParsePath(path, "", true, false), params, false)

  def apply(path: List[String]) = new RewriteResponse(ParsePath(path, "", true, false), Map.empty, false)

  def apply(path: List[String], stopRewriting: Boolean) = 
    new RewriteResponse(ParsePath(path, "", true, false),
                        Map.empty, stopRewriting)

  def apply(path: List[String], suffix: String) = new RewriteResponse(ParsePath(path, suffix, true, false), Map.empty, false)

  def apply(path: ParsePath, params: Map[String, String]) = new RewriteResponse(path, params, false)
}

/**
 * Provides access to a thread-local URL rewriter. Typically uses either an
 * applicable entry in `[[LiftRules.urlDecorate]]` or the container's built-in
 * URL decoration which may append the session id to the URL (dependent on
 * `[[LiftRules.encodeJSessionIdInUrl_?]]`).
 */
object URLRewriter {
  private val funcHolder = new ThreadGlobal[(String) => String]

  def doWith[R](f: (String) => String)(block: => R): R = {
    funcHolder.doWith(f) {
      block
    }
  }

  def rewriteFunc: Box[(String) => String] = Box.legacyNullTest(funcHolder.value)
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy