packages feed

salvia-0.1: src/Network/Protocol/Uri.hs

{- | See rfc2396 for more info. -}

{-# LANGUAGE TemplateHaskell #-}
module Network.Protocol.Uri (

    Scheme
  , IPv4
  , Domain
  , RegName
  , Port
  , Query
  , Fragment
  , Hash
  , UserInfo
  , PathSegment
  , Parameters
  , Path
  , Host
  , Authority
  , URI

  -- * Creating (parts of) URIs.

  , encode
  , decode

  , mkURI
  , mkScheme
  , mkPath
  , mkAuthority
  , mkQuery
  , mkFragment
  , mkUserinfo
  , mkHost
  , mkPort

  -- * Accessing parts of URIs.

  , domain
  , regname
  , ipv4
  , userinfo
  , host
  , port
  , relative
  , scheme
  , authority
  , path
  , query
  , fragment

  -- * Helper labels.

  , absolute
  , segments
  , _host
  , _port

  -- * Parsing URIs.

  , parseURI
  , parseAbsoluteURI
  , parseAuthority
  , parsePath
  , parseHost

  , pUriReference
  , pAbsoluteURI
--   , pAuthority
--   , pPath
--   , pHost

  -- * Handling query parameters.

  , parseQueryParams
  , queryParams

  -- * Filename related utilities.

  , extension

  , mkPathRelative
  , mimetype
  , normalize
  , jail
  , (/+)

  ) where

import Control.Applicative
import Data.Bits
import Data.Char (ord, chr, isDigit, isAlphaNum, intToDigit, isHexDigit, toLower)
import Data.List (intercalate, isPrefixOf)
import Data.Maybe (mapMaybe, fromJust)
import Data.Record.Label
import Misc.Misc (eitherToMaybe, (@@), bool, pMaybe, split)
import Network.Protocol.Mime
import System.FilePath.Posix
import Text.ParserCombinators.Parsec hiding (many, optional, (<|>))

-------[ data type definition of URIs ]----------------------------------------

type Scheme      = String
type IPv4        = [Int] -- actually 4-tupel
type Domain      = [String]
type RegName     = String
type Port        = Int
type Query       = String
type Fragment    = String
type Hash        = String
type UserInfo    = String
type PathSegment = String
type Parameters  = [(String, Maybe String)]

data Path = Path {
    _absolute :: Bool
  , _segments :: [PathSegment]
  }
  deriving (Eq, Ord)

data Host =
    Hostname { __domain  :: Domain }
  | RegName  { __regname :: String }
  | IPv4     { __ipv4    :: IPv4   }
  deriving (Eq, Ord)

data Authority = Authority {
    __userinfo :: UserInfo
  , __host     :: Host
  , __port     :: Port
  }
  deriving (Eq, Ord)

data URI = URI {
    _relative  :: Bool
  , _scheme    :: Scheme
  , _authority :: Authority
  , __path     :: Path
  , __query     :: Query
  , _fragment  :: Fragment
  }
  deriving (Eq, Ord)

$(mkLabels [''Path, ''Host, ''Authority, ''URI])

_domain   :: Label Host Domain
_host     :: Label Authority Host
_ipv4     :: Label Host IPv4
_path     :: Label URI Path
_port     :: Label Authority Port
_query    :: Label URI Query
_regname  :: Label Host String
_userinfo :: Label Authority UserInfo
absolute  :: Label Path Bool
authority :: Label URI Authority
domain    :: Label URI Domain
fragment  :: Label URI Fragment
host      :: Label URI String
ipv4      :: Label URI IPv4
path      :: Label URI FilePath
port      :: Label URI Port
query     :: Label URI Query
regname   :: Label URI String
relative  :: Label URI Bool
scheme    :: Label URI Scheme
segments  :: Label Path [PathSegment]
userinfo  :: Label URI UserInfo

-- Public label based on private labels.

domain    = _domain   % _host % authority
regname   = _regname  % _host % authority
ipv4      = _ipv4     % _host % authority
userinfo  = _userinfo % authority
port      = _port     % authority

query = Label
  (decode . lget _query)
  (lset _query . encode)

host = Label
  (show . lget (_host % authority))
  (lset (_host % authority) . maybe (Hostname []) id . parseHost)

path = Label
  (decode . show . lget _path)
  (lset _path . maybe (Path True []) id . parsePath . encode)

-------[ creating, selection and modifying URIs ]------------------------------

{- | Constructors for making empty URI. -}

mkURI :: URI
mkURI = URI False mkScheme mkAuthority mkPath mkQuery mkFragment

{- | Constructors for making empty `Scheme`. -}

mkScheme :: Scheme
mkScheme = ""

{- | Constructors for making empty `Path`. -}

mkPath :: Path
mkPath = Path False []

{- | Constructors for making empty `Authority`. -}

mkAuthority :: Authority
mkAuthority = Authority "" mkHost mkPort

{- | Constructors for making empty `Query`. -}

mkQuery :: Query
mkQuery = ""

{- | Constructors for making empty `Fragment`. -}

mkFragment :: Fragment
mkFragment = ""

{- | Constructors for making empty `UserInfo`. -}

mkUserinfo :: UserInfo
mkUserinfo = ""

{- | Constructors for making empty `Host`. -}

mkHost :: Host
mkHost = Hostname []

{- | Constructors for making empty `Port`. -}

mkPort :: Port
mkPort = (-1)

-------[ path encoding and decoding ]------------------------------------------

{- | URI encode a string. -}

encode :: String -> String
encode = concatMap encodeChr
  where
    encodeChr c
      | unreserved c || genDelims c || subDelims c = [c]
      | otherwise = '%' :
          intToDigit (shiftR (ord c) 4) :
          intToDigit ((ord c) .&. 0x0F) : []

{- | URI decode a string. -}

decode :: String -> String
decode [] = []
decode ('%':d:e:ds) | isHexDigit d && isHexDigit e = (chr $ digs d * 16 + digs e) : decode ds 
  where digs a = fromJust $ lookup (toLower a) $ zip "0123456789abcdef" [0..]
decode (d:ds) = d : decode ds

-------[ show instance for URIs ]----------------------------------------------

-- TODO: cleanup and test this mess: QuickCheck property for testing:
-- parse $ show u == u /\ show $ parse u' = u'

-- TODO: ShowS

instance Show Path where
  show (Path a s) =
       (if a then "/" else "")
    ++ (intercalate "/" s)

instance Show Host where
  show (Hostname d) = if null d then "" else intercalate "." d
  show (IPv4     a) = intercalate "." $ map show a
  show (RegName  r) = r

instance Show Authority where
  show (Authority u h p) =
       (if hst h then "//" else "")
    ++ (if null u then "" else u ++ "@")
    ++  show h
    ++ (if p == -1 then "" else ":" ++ show p)
    where hst (Hostname d) = not $ null d
          hst (RegName  d) = not $ null d
          hst _            = True

instance Show URI where
  show (URI r s a p q f) =
       (if not r then (if null s then "" else s ++ ":") else "")
    ++ (show a)
    ++ (show p)
    ++ (if null q then "" else "?" ++ q)
    ++ (if null f then "" else "#" ++ f)

-------[ global URI parse interface ]------------------------------------------

{- | Parse string into a `URI`. -}

parseURI :: String -> Maybe URI
parseURI = eitherToMaybe . parse pUriReference ""

{- | Parse string into a `URI` and only accept absolute URIs. -}

parseAbsoluteURI :: String -> Maybe URI
parseAbsoluteURI = eitherToMaybe . parse pAbsoluteURI ""

{- | Parse string into a `Authority` object. -}

parseAuthority :: String -> Maybe Authority
parseAuthority = eitherToMaybe . parse pAuthority ""

{- | Parse string into a `Path`. -}

parsePath :: String -> Maybe Path
parsePath = eitherToMaybe . parse pPath ""

{- | Parse string into a `Host`. -}

parseHost :: String -> Maybe Host
parseHost = eitherToMaybe . parse pHost ""

-------[ parsing URIs according to rfc2396 ]-----------------------------------

-- D.2.  Modifications
pAlpha, pDigit, pAlphanum :: GenParser Char st Char

pAlpha    = letter
pDigit    = digit
pAlphanum = alphaNum

-- 2.3.  Unreserved Characters
unreserved :: Char -> Bool
unreserved c = isAlphaNum c || elem c "-._~"

pUnreserved :: GenParser Char st Char
pUnreserved  = pAlphanum <|> oneOf "-._~"

-- 2.2.  Reserved Characters
genDelims :: Char -> Bool
genDelims = flip elem ":/?#[]@"

subDelims :: Char -> Bool
subDelims = flip elem "!$&'()*+,;="

{-
pReserved :: GenParser Char st Char
pReserved  = pGenDelims <|> pSubDelims
pGenDelims :: GenParser Char st Char
pGenDelims = oneOf ":/?#[]@"
-}

pSubDelims :: GenParser Char st Char
pSubDelims = oneOf "!$&'()*+,;="

-- 2.1.  Percent-Encoding
pPctEncoded :: GenParser Char st String
pPctEncoded = (:) <$> char '%' <*> pHex

pHex :: GenParser Char st String
pHex = (\a b -> a:b:[])
    <$> hexDigit
    <*> hexDigit

-- 3.  Syntax Components

-- With the hier-part integrated.
pUri :: GenParser Char st URI
pUri = (\a (b,c) d e -> URI False a b c d e)
  <$> (pScheme <* string ":")
  <*> (ap <|> p)
  <*> option "" (string "?" *> pQuery)
  <*> option "" (string "#" *> pFragment)
  where
    ap = (,) <$> (string "//" *> pAuthority) <*> pPathAbempty
    p  = ((,) mkAuthority) <$> (pPathAbsolute <|> pPathRootless {-<|> pPathEmpty-})

-- 3.1.  Scheme
pScheme :: GenParser Char st Scheme
pScheme = (:) <$> pAlpha <*> many (pAlphanum <|> oneOf "+_.")

-- 3.2.  Authority
pAuthority :: GenParser Char st Authority
pAuthority = Authority
  <$> option mkUserinfo (try (pUserinfo <* string "@"))
  <*> pHost
  <*> option mkPort (string ":" *> pPort)

-- 3.2.1.  User Information
pUserinfo :: GenParser Char st String
pUserinfo = concat <$> many (
      (pure <$> pUnreserved)
  <|> (         pPctEncoded)
  <|> (pure <$> pSubDelims)
  <|> (pure <$> oneOf ":")
  )

-- 3.2.2.  Host
pHost :: GenParser Char st Host
pHost = diff <$> pRegName -- <|> RegName <$> pRegName
  where
    diff  a = maybe (RegName a) sep (pHostname @@ a)
    sep   a = bool (Hostname a) (ipreg a) $ hst a
    ipreg a = bool (IPv4 $ map read a) (RegName $ intercalate "." a) $ ip a
    hst     = not . all isDigit . head . dropWhile null . reverse
    ip    a = length a == 4 && length (mapMaybe (pDecOctet @@) a) == 4

{-
pfff, ipv6 is sooo not gonna make it..

pIPLiteral = "[" ( IPv6address <|> IPvFuture  ) "]"

pIPvFuture = "v" 1*HEXDIG "." 1*( unreserved <|> subDelims <|> ":" )

pIPv6address =
                                 6( h16 ":" ) ls32
  <|>                       "::" 5( h16 ":" ) ls32
  <|> [               h16 ] "::" 4( h16 ":" ) ls32
  <|> [ *1( h16 ":" ) h16 ] "::" 3( h16 ":" ) ls32
  <|> [ *2( h16 ":" ) h16 ] "::" 2( h16 ":" ) ls32
  <|> [ *3( h16 ":" ) h16 ] "::"    h16 ":"   ls32
  <|> [ *4( h16 ":" ) h16 ] "::"              ls32
  <|> [ *5( h16 ":" ) h16 ] "::"              h16
  <|> [ *6( h16 ":" ) h16 ] "::"

pH16           = 1*4HEXDIG
pLs32          = ( h16 ":" h16 ) <|> IPv4address
-}

{-
pIPv4address :: GenParser Char st [Int]
pIPv4address = (:) <$> pDecOctet <*> (count 3 $ char '.' *> pDecOctet)
-}

pDecOctet :: GenParser Char st Int
pDecOctet = read <$> choice [
    try ((\a b c -> [a,b,c]) <$> char '2' <*> char '5'      <*> oneOf "012345")
  , try ((\a b c -> [a,b,c]) <$> char '2' <*> oneOf "01234" <*> digit)
  , try ((\a b c -> [a,b,c]) <$> char '1' <*> digit         <*> digit)
  , try ((\a b   -> [a,b])   <$>              digit         <*> digit)
  ,     (pure                <$>                                digit)
  ]

pRegName :: GenParser Char st String
pRegName = concat <$> many1 (
      (pure <$> pUnreserved)
  <|>           pPctEncoded
  <|> (pure <$> pSubDelims))

-- Not actually part of the rfc3986, but comptability with the rfc2396.
-- This information can be useful, so why throw away.
pHostname :: GenParser Char st Domain
pHostname = sepBy (option "" pDomainlabel) (string ".")

pDomainlabel :: GenParser Char st String
pDomainlabel = intercalate "-" <$> sepBy1 (some pAlphanum) (string "-")

-- 3.2.3.  Port
pPort :: GenParser Char st Port
pPort = read <$> some pDigit

-- 3.4.  Query
pQuery :: GenParser Char st String
pQuery = concat <$> many (pPchar <|> pure <$> oneOf "/?")

-- 3.5.  Fragment
pFragment :: GenParser Char st String
pFragment = concat <$> many (pPchar <|> pure <$> oneOf "/?" )

-- 3.3.  Path

pPath, pPathAbempty, pPathAbsolute, pPathNoscheme, pPathRootless, pPathEmpty
  :: GenParser Char st Path

pPath =
      try pPathAbsolute -- begins with "/" but not "//"
  <|> try pPathNoscheme -- begins with a nonColon segment
  <|> try pPathRootless -- begins with a segment
  <|> pPathEmpty        -- zero characters

pPathAbempty  = (Path True) <$> _pSlashSegments
pPathAbsolute = (char '/' *>) $ (Path True) <$> (option [] $ (:) <$> pSegmentNz <*> _pSlashSegments)
pPathNoscheme = (Path False) <$> ((:) <$> pSegmentNzNc <*> _pSlashSegments)
pPathRootless = (Path False) <$> ((:) <$> pSegmentNz    <*> _pSlashSegments)
pPathEmpty    = (Path False []) <$ string ""

pSegment, pSegmentNz, pSegmentNzNc :: GenParser Char st String
pSegment     = concat <$> many pPchar
pSegmentNz   = concat <$> some pPchar
pSegmentNzNc = concat <$> some (
      (pure <$> pUnreserved)
  <|>           pPctEncoded
  <|> (pure <$> pSubDelims)
  <|> (pure <$> oneOf "@" ))

_pSlashSegments :: GenParser Char st [PathSegment]
_pSlashSegments = (many $ (:) <$> char '/' *> pSegment)

pPchar :: GenParser Char st String
pPchar = choice [
    pure <$> pUnreserved
  , pPctEncoded
  , pure <$> pSubDelims
  , pure <$> oneOf ":@"
  ]

-- 4.1.  URI Reference

pUriReference :: GenParser Char st URI
pUriReference = try pAbsoluteURI <|> pRelativeRef

-- 4.2.  Relative Reference

-- With the relative-part integrated.
pRelativeRef :: GenParser Char st URI
pRelativeRef = ($)
  <$> (try pRelativePart
  <|> ((URI True mkScheme mkAuthority)
  <$> (pPathAbsolute <|> pPathRootless <|> pPathEmpty)))
  <*> option "" (string "?" *> pQuery)
  <*> option "" (string "#" *> pFragment)

pRelativePart :: GenParser Char st (Query -> Fragment -> URI)
pRelativePart = (URI True mkScheme) <$> (string "//" *> pAuthority) <*> pPathAbempty

-- 4.3.  Absolute URI

pAbsoluteURI :: GenParser Char st URI
pAbsoluteURI = pUri

-------[ parsing query parameters ]--------------------------------------------

pQueryParams :: GenParser Char st Parameters
pQueryParams = 
      filter (not . null . fst)
  <$> sepBy ((,)
  <$> many (noneOf "=&")
  <*> pMaybe (char '='
   *> (translateParam <$> many (noneOf "&"))))
      (char '&')

{- | Parse a pre-decoded query string into key value pairs parameters. -}

parseQueryParams :: String -> Maybe Parameters
parseQueryParams = eitherToMaybe . parse pQueryParams  ""

{- | Fetch the query parameters form a URI. -}

queryParams :: URI -> Parameters
queryParams = maybe [] id . parseQueryParams . lget query

-- Translate special characters in a parameter.
translateParam :: String -> String
translateParam []       = []
translateParam ('+':xs) = ' ' : translateParam xs
translateParam (x:xs)   = x   : translateParam xs

-------[ filename path utilities ]---------------------------------------------

{- | Label to access the extension of a filename. -}

extension :: Label FilePath (Maybe String)
extension = Label {lget = getExt, lset = setExt}
  where
    splt     p = (\(a,b) -> (reverse a, reverse b)) $ break (=='.') $ reverse p
    isExt e  p = '/' `elem` e || not ('.' `elem` p)
    getExt   p = let (u, v) = splt p in
                 if isExt u v then Nothing else Just u
    setExt e p = let (u, v) = splt p in
                 (if isExt u v then p else init v) ++ maybe "" ('.':) e

{- |
Try to guess the correct mime type for the input file based on the file
extension.
-}

mimetype :: FilePath -> Maybe String
mimetype p = lget extension p >>= mime

{- |
Normalize a path by removing or merging all dot or dot-dot segments and double
slashes. 
-}

-- Todo: cleanup, remove fixp.

normalize :: FilePath -> FilePath
normalize [] = []
normalize p = fixAbs absolut $ intercalate "/" $ norm
  where
    fixAbs True          = ('/':)
    fixAbs False         = id
    absolut              = head p == '/'
    norm                 = fixp False $ split '/' p
    fixp False xs        = let ys = merge xs in fixp (xs == ys) ys
    fixp True  xs        = xs
    merge ("":xs)        = merge xs
    merge (".":xs)       = merge xs
    merge ("..":"..":xs) = ".." : ".." : merge xs
    merge (_:"..":xs)    = merge xs
    merge (x:xs)         = x : merge xs
    merge xs             = xs

{- | Make a path relative by removing the first slash when there is one. -}

mkPathRelative :: FilePath -> FilePath
mkPathRelative = dropWhile (=='/')

{- | Jail a filepath within a jail directory. -}

jail
  :: FilePath         -- ^ Jail directory.
  -> FilePath         -- ^ Filename to jail.
  -> Maybe FilePath
jail jailDir p =
  let nj = normalize jailDir
      np = normalize p in
  if nj `isPrefixOf` np -- && not (".." `isPrefixOf` np)
    then Just np
    else Nothing

{- | Concatenate and normalize two filepaths. -}

(/+) :: FilePath -> FilePath -> FilePath
a /+ b = normalize (a ++ "/" ++ b)