packages feed

adblock2privoxy-1.0.0: src/UrlBlocker.hs

module UrlBlocker (
BlockMethod(..),
TaggerType(..),
urlBlock
) where
import InputParser
import Control.Applicative
import Control.Monad
import Data.List
import Data.Char (toLower)
import Data.Monoid
import OptionsConverter
import Utils 
import Control.Monad.State
import qualified Templates 
import qualified Data.Map as Map
import Data.String.Utils (split)
import Data.Maybe   
import System.IO  
import System.FilePath
import PatternConverter          

data TaggerType = Client | Server
data TaggerForwarder = Forward (Maybe Filter) String | CancelTagger String
data Tagger = Tagger { _taggerCode :: String, _forwarding :: [TaggerForwarder], _headerType :: HeaderType }

data ActionType   = TaggerAction Tagger | BlockAction | TerminalAction BlockMethod
data ActionSwitch = Switch Bool ActionType
data Action = Action { _actionCode :: String, _switches :: [ActionSwitch], _patterns :: [Pattern], _hasTag :: Bool }

data ChainType = Regular | Nested | Negate deriving (Eq, Ord)
type UrlBlockData = ([Tagger], [Action])
data BlockMethod = Request | Xframe | Elem | Dnt | Xpopup deriving (Show, Eq)
data FilteringNode = Node { _pattern :: [Pattern], _filters :: HeaderFilters, _nodeType :: ChainType, 
    _policy :: Policy, _method :: BlockMethod }


class Named a where
    name :: a -> String

urlBlock :: String -> [String] -> [Line] -> IO()
urlBlock path info = writeBlockData . urlBlockData 
    where    
    writeBlockData :: UrlBlockData -> IO()
    writeBlockData (taggers, actions) = 
        do writeContent (path </> "ab2p.filter") Templates.filtersFilePrefix taggers
           writeContent (path </> "ab2p.action") Templates.actionsFilePrefix actions
    writeContent filename header content = 
         do outFile <- openFile filename WriteMode
            hPutStrLn outFile (header) 
            _ <- mapM (hPutStrLn outFile) $ ('#':) <$> info
            hPutStrLn outFile $ intercalate "\n\n" $ show <$> content
            hClose outFile

urlBlockData :: [Line] -> UrlBlockData 
urlBlockData lns = filterBlockData $ result
    where
    result = mconcat [nodeResult node | node <- shortenNodes $ sortBy cmpPolicy $ filterNodesList blockLines]
    cmpPolicy node1 node2 = compare (_policy node1) (_policy node2)
    blockLines = lns >>= blockLine
        where 
        blockLine (Line position (RequestBlock policy pattern options)) 
            = filteringNodes policy (errorToPattern expandedPatterns) options
            where 
            expandedPatterns = makePattern (_matchCase options) <<$> parseUrl pattern
            sourceText = recordSourceText position 
            errorToPattern (Left parseError) = ["# ERROR: " ++ sourceText  ++ " - " ++ show parseError]
            errorToPattern (Right patterns') = ("# " ++ sourceText) : patterns'
        blockLine _ = []
    
filterNodesList :: [FilteringNode] -> [FilteringNode]
filterNodesList nodes = Map.foldr (:) [] $ Map.fromListWith joinNodes $ list
    where
    list = [(name node, node) | node <- nodes]
    joinNodes (Node patterns1 filters1 type1 policy1 method1) 
              (Node patterns2 _ type2 _ _) 
        = Node (patterns1 ++ patterns2) filters1 (max type1 type2) policy1 method1

filterBlockData :: UrlBlockData -> UrlBlockData
filterBlockData blockData = (result, snd blockData)
    where
    result = Map.foldr (:) [] $ Map.fromListWith joinTaggers taggerItems
    taggerItems = [(name tagger, tagger) | tagger <- fst blockData]
    metric = length._forwarding
    joinTaggers tagger1 tagger2 | metric tagger1 >= metric tagger2 = tagger1
                                | otherwise                        = tagger2
         
shortenNodes :: [FilteringNode] -> [FilteringNode]      
shortenNodes nodes = evalState (mapM shortenNode nodes) initialState
    where 
    initialState = Map.empty :: Map.Map String String
    shortenNode node = (\f -> node {_filters = f}) <$> ((mapM.mapM) shortenFilter $ _filters node)       
    shortenFilter headerFilter@(HeaderFilter headerType flt) 
        = let filterCode = _code flt 
          in do 
             dictionary <- get 
             case Map.lookup filterCode dictionary of 
                 Just shortenCode -> return $ HeaderFilter headerType flt { _code = shortenCode }
                 Nothing -> case break (=='[') filterCode of
                    (_,[]) -> return headerFilter
                    (start, rest) -> 
                        let end = last $ split "]" rest 
                            shortenCode' = start ++ (show $ Map.size dictionary + 1) ++  end 
                        in do put $ Map.insert filterCode shortenCode' dictionary
                              return $ HeaderFilter headerType flt { _code = shortenCode' }
                            

filteringNodes :: Policy -> [Pattern] -> RequestOptions -> [FilteringNode]
filteringNodes policy patterns requestOptions 
    = join.join $  [mainResult, subdocumentResult, elemhideResult, dntResult, popupResult]
    where 
    mainResult = optionsToNodes mainOptions $> Request
    subdocumentResult = maybeToList (optionsToNodes (singleTypeOptions Subdocument) $> Xframe)
    elemhideResult = maybeToList (optionsToNodes (boolOptions _elemHide) $> Elem)
    dntResult = maybeToList (optionsToNodes (boolOptions _doNotTrack) $> Dnt)
    popupResult = maybeToList (optionsToNodes (singleTypeOptions Popup) $> Xpopup)
    requestType = _requestType requestOptions
    mainOptions = [requestOptions {_requestType = requestType { _positive = mainPosRequestTypes } }]
    mainPosRequestTypes = filter (`notElem` [Subdocument]) <$> (_positive requestType)
    boolOptions getter = case getter requestOptions of
        False -> Nothing
        True  -> Just requestOptions {_requestType = Restrictions Nothing [], _thirdParty = Nothing}
    singleTypeOptions singleType = 
        do
        foundTypes <- filter (== singleType) <$> (_positive requestType)
        foundType <- listToMaybe foundTypes
        return requestOptions {_requestType = requestType { _positive = Just [foundType] } }
    optionsToNodes options = collectNodes patterns <$> headerFilters policy 2 <$> options
    nestedOrRegular True = Nested
    nestedOrRegular False = Regular
    collectNodes :: [Pattern] -> Maybe HeaderFilters -> BlockMethod -> [FilteringNode]
    collectNodes _ Nothing _ = [] 
    collectNodes patterns' (Just []) method = [Node patterns' [] (nestedOrRegular $ null patterns') policy method]
    collectNodes patterns' (Just filters@(levelFilters: next)) method
            = Node patterns' filters (nestedOrRegular $ null patterns') policy method 
              : (levelFilters >>= negateNode) 
              ++ collectNodes [] (Just next) method
        where 
        negateNode negateFilter@(HeaderFilter _ (Filter {_orEmpty = True})) 
                = [Node [] ([negateFilter] : next) Negate policy method]
        negateNode _ = [] 
          
nodeResult :: FilteringNode -> UrlBlockData
nodeResult node@(Node patterns [] nodeType policy method) = ([], [baseAction])
    where baseAction = Action (name node) [Switch (policy == Block) $ TerminalAction method] patterns (nodeType == Nested)
nodeResult node@(Node _ ([flt] : nextLevelFilters) Negate policy method)
    = ([negateTagger], [negateAction])
    where
    negateAction = Action (name node) [Switch False $ TaggerAction negateTagger] [] True
    negateTagger = newTagger flt nextLevelFilters policy method Negate []
nodeResult node@(Node patterns (levelFilters : nextLevelFilters) nodeType policy method)
    = (taggers, [action])
    where 
    action = Action { _actionCode = name node,
                      _switches   = appendIf (policy == Unblock && method == Request) 
                                        (Switch False BlockAction)
                                        (Switch True . TaggerAction <$> taggers),
                      _patterns   = patterns,
                      _hasTag     = (nodeType == Nested) }  
    taggers = filterTaggers <$> levelFilters
    filterTaggers flt@(HeaderFilter _ (Filter _ _ orEmpty))  
        = newTagger flt nextLevelFilters policy method Regular moreForwarding
        where
        orEmptyTaggerCode   = filtersCode ([flt] : nextLevelFilters) Negate  policy method ""
        moreForwarding  | orEmpty = [CancelTagger orEmptyTaggerCode]
                        | otherwise = []
            
newTagger :: HeaderFilter -> HeaderFilters -> Policy -> BlockMethod -> ChainType -> [TaggerForwarder] -> Tagger
newTagger flt@(HeaderFilter headerType filter') nextLevelFilters policy method chainType moreForwarding
   = Tagger { _taggerCode = taggerCode,
              _forwarding = Forward filter'' nextLevelActionCode : moreForwarding,
              _headerType = headerType }     
   where
   filter'' | chainType == Negate = Nothing
            | otherwise           = Just filter'
   taggerCode          = filtersCode ([flt] : nextLevelFilters) chainType policy method ""        
   nextLevelActionCode = filtersCode nextLevelFilters  Nested policy method ""   
           
instance Named FilteringNode where
    name (Node _ filters Negate policy method)  = '-' : filtersCode filters Negate policy method "" 
    name (Node _ filters _ policy method)  = filtersCode filters Nested policy method "" 
    
filtersCode :: HeaderFilters -> ChainType -> Policy -> BlockMethod -> String -> String
filtersCode [] _ policy method rest 
    = join [Templates.ab2pPrefix, toLower <$> show policy, "-" ,toLower <$> show method,(if null rest then "" else "-"), rest]
filtersCode (levelFilters : nextLevelFilters) chainType policy method rest 
    = filtersCode nextLevelFilters Nested policy method $ join [levelCode, (if null rest then "" else "-when-"), rest]
    where 
    levelCode = (intercalate "-" $ filterCode <$> levelFilters)
    filterCode (HeaderFilter HeaderType {_typeCode = typeCode} (Filter code _ orEmpty))
        | chainType == Negate            = negateCode
        | chainType == Nested && orEmpty = negateCode ++ '-' : mainCode  
        | otherwise                      = mainCode
        where 
        mainCode = typeCode : code
        negateCode = 'n' : [typeCode]

instance Show TaggerType where
    show Client = "CLIENT-HEADER-TAGGER"
    show Server = "SERVER-HEADER-TAGGER"

instance Named TaggerType where
    name = fmap toLower . show

instance Named Tagger where
    name = _taggerCode

instance Show Tagger where
    show (Tagger code forwarding HeaderType {_name = headerName, _taggerType =  taggerType }) 
        = intercalate "\n" (caption : (forward <$> forwarding))
        where caption = show taggerType ++ (':' : ' ' : code)
              forward (Forward (Just filter') tagret) = forwardRegex headerName (_regex filter') ":" "" tagret
              forward (Forward Nothing tagret) = forwardRegex "" "" "" "" tagret
              forward (CancelTagger taggerCode) = forwardRegex headerName "" ":" "-" taggerCode
              forwardRegex header expression value tagPrefix tagret
                = let  (modifier, lookahead' : additionalLines) 
                            | '\n' `elem` expression = ("i", split "\n" expression) -- the case for third-party
                            | otherwise              = ("Ti", [expression])
                  in intercalate "\n" $ additionalLines ++ 
                        [join ["s@^", header, lookahead', value, ".*@", tagPrefix, tagret, "@", modifier]] 

instance Named Bool where
    name True = "+"
    name False = "-"                  

instance Show ActionSwitch where
    show (Switch enable (TerminalAction method)) = Templates.terminalActionSwitch enable method
    show (Switch enable BlockAction) = name enable ++ "block"
    show (Switch enable (TaggerAction tagger)) 
        = intercalate " \\\n " $ mainText : (_forwarding tagger >>= cancelTaggerText)
        where 
        mainText = join [name enable, name $ _taggerType $ _headerType $ tagger, "{", name tagger,  "}" ]
        cancelTaggerText (CancelTagger cancelTaggerCode) 
            = [join [name enable, name $ _taggerType $ _headerType $ tagger, "{", cancelTaggerCode,  "}" ]]
        cancelTaggerText _ = []                
    
instance Named Action where
    name = _actionCode
    
instance Show Action where
    show (Action code switches patterns hasTag)
        = intercalate "\n" (caption : switches' : patterns')
        where caption = '#' : code
              switches' = join ["{", intercalate " \\\n " (show <$> switches), " \\\n}"]
              patterns' | hasTag    = join ["TAG:^", code, "$"] : patterns
                        | otherwise = patterns