    module Propellor.Property.Apache where

    import Propellor.Base
    import qualified Propellor.Property.File as File
    import qualified Propellor.Property.Apt as Apt
    import qualified Propellor.Property.Service as Service
    import qualified Propellor.Property.LetsEncrypt as LetsEncrypt

    installed :: Property DebianLike
    installed = Apt.installed ["apache2"]

    restarted :: Property DebianLike
    restarted = Service.restarted "apache2"

    reloaded :: Property DebianLike
    reloaded = Service.reloaded "apache2"

    type ConfigLine = String

    type ConfigFile = [ConfigLine]

    siteEnabled :: Domain -> ConfigFile -> RevertableProperty DebianLike DebianLike
    siteEnabled domain cf = siteEnabled' domain cf <!> siteDisabled domain

    siteEnabled' :: Domain -> ConfigFile -> Property DebianLike
    siteEnabled' domain cf = combineProperties ("apache site enabled " ++ domain) $ props
            & siteAvailable domain cf
                    `requires` installed
                    `onChange` reloaded
            & check (not <$> isenabled)
                    (cmdProperty "a2ensite" ["--quiet", domain])
                            `requires` installed
                            `onChange` reloaded
      where
            isenabled = boolSystem "a2query" [Param "-q", Param "-s", Param domain]

    siteDisabled :: Domain -> Property DebianLike
    siteDisabled domain = combineProperties
            ("apache site disabled " ++ domain)
            (toProps $ map File.notPresent (siteCfg domain))
                    `onChange` (cmdProperty "a2dissite" ["--quiet", domain] `assume` MadeChange)
                    `requires` installed
                    `onChange` reloaded

    siteAvailable :: Domain -> ConfigFile -> Property DebianLike
    siteAvailable domain cf = combineProperties ("apache site available " ++ domain) $
            toProps $ map tightenTargets $
                    map (`File.hasContent` (comment:cf)) (siteCfg domain)
      where
            comment = "# deployed with propellor, do not modify"

    modEnabled :: String -> RevertableProperty DebianLike DebianLike
    modEnabled modname = enable <!> disable
      where
            enable = check (not <$> isenabled)
                    (cmdProperty "a2enmod" ["--quiet", modname])
                            `describe` ("apache module enabled " ++ modname)
                            `requires` installed
                            `onChange` reloaded
            disable = check isenabled
                    (cmdProperty "a2dismod" ["--quiet", modname])
                            `describe` ("apache module disabled " ++ modname)
                            `requires` installed
                            `onChange` reloaded
            isenabled = boolSystem "a2query" [Param "-q", Param "-m", Param modname]

    -- | Control whether an apache configuration file is enabled.
    --
    -- The String is the base name of the configuration, eg "charset" or "gitweb".
    confEnabled :: String -> RevertableProperty DebianLike DebianLike
    confEnabled confname = enable <!> disable
      where
            enable = check (not <$> isenabled)
                    (cmdProperty "a2enconf" ["--quiet", confname])
                            `describe` ("apache configuration enabled " ++ confname)
                            `requires` installed
                            `onChange` reloaded
            disable = check isenabled
                    (cmdProperty "a2disconf" ["--quiet", confname])
                            `describe` ("apache configuration disabled " ++ confname)
                            `requires` installed
                            `onChange` reloaded
            isenabled = boolSystem "a2query" [Param "-q", Param "-c", Param confname]

    -- | Make apache listen on the specified ports.
    --
    -- Note that ports are also specified inside a site's config file,
    -- so that also needs to be changed.
    listenPorts :: [Port] -> Property DebianLike
    listenPorts ps = "/etc/apache2/ports.conf" `File.hasContent` map portline ps
            `onChange` restarted
      where
            portline port = "Listen " ++ val port

    -- This is a list of config files because different versions of apache
    -- use different filenames. Propellor simply writes them all.
    siteCfg :: Domain -> [FilePath]
    siteCfg domain =
            -- Debian pre-2.4
            [ "/etc/apache2/sites-available/" ++ domain
            -- Debian 2.4+
            , "/etc/apache2/sites-available/" ++ domain ++ ".conf"
            ]

    -- | Configure apache to use SNI to differentiate between
    -- https hosts.
    --
    -- This was off by default in apache 2.2.22. Newver versions enable
    -- it by default. This property uses the filename used by the old version.
    multiSSL :: Property DebianLike
    multiSSL = check (doesDirectoryExist "/etc/apache2/conf.d") $
            "/etc/apache2/conf.d/ssl" `File.hasContent`
                    [ "NameVirtualHost *:443"
                    , "SSLStrictSNIVHostCheck off"
                    ]
                    `describe` "apache SNI enabled"
                    `onChange` reloaded

    -- | Config file fragment that can be inserted into a <Directory>
    -- stanza to allow global read access to the directory.
    --
    -- Works with multiple versions of apache that have different ways to do
    -- it.
    allowAll :: ConfigLine
    allowAll = unlines
            [ "<IfVersion < 2.4>"
            , "Order allow,deny"
            , "allow from all"
            , "</IfVersion>"
            , "<IfVersion >= 2.4>"
            , "Require all granted"
            , "</IfVersion>"
            ]

    -- | Config file fragment that can be inserted into a <VirtualHost>
    -- stanza to allow apache to display directory index icons.
    iconDir :: ConfigLine
    iconDir = unlines
            [ "<Directory \"/usr/share/apache2/icons\">"
            , "Options Indexes MultiViews"
            , "AllowOverride None"
            , allowAll
            , "  </Directory>"
            ]

    type WebRoot = FilePath

    -- | A basic virtual host, publishing a directory, and logging to
    -- the combined apache log file. Not https capable.
    virtualHost :: Domain -> Port -> WebRoot -> RevertableProperty DebianLike DebianLike
    virtualHost domain port docroot = virtualHost' domain port docroot []

    -- | Like `virtualHost` but with additional config lines added.
    virtualHost' :: Domain -> Port -> WebRoot -> [ConfigLine] -> RevertableProperty DebianLike DebianLike
    virtualHost' domain port docroot addedcfg = siteEnabled domain $
            [ "<VirtualHost *:" ++ val port ++ ">"
            , "ServerName " ++ domain ++ ":" ++ val port
            , "DocumentRoot " ++ docroot
            , "ErrorLog /var/log/apache2/error.log"
            , "LogLevel warn"
            , "CustomLog /var/log/apache2/access.log combined"
            , "ServerSignature On"
            ]
            ++ addedcfg ++
            [ "</VirtualHost>"
            ]

    -- | A virtual host using https, with the certificate obtained
    -- using `Propellor.Property.LetsEncrypt.letsEncrypt`.
    --
    -- http connections are redirected to https.
    --
    -- Example:
    --
    -- > httpsVirtualHost "example.com" "/var/www"
    -- > 	(LetsEncrypt.AgreeTOS (Just "me@my.domain"))
    --
    -- Note that reverting this property does not remove the certificate from
    -- letsencrypt's cert store.
    httpsVirtualHost :: Domain -> WebRoot -> LetsEncrypt.AgreeTOS -> RevertableProperty DebianLike DebianLike
    httpsVirtualHost domain docroot letos = httpsVirtualHost' domain docroot letos []

    -- | Like `httpsVirtualHost` but with additional config lines added.
    httpsVirtualHost' :: Domain -> WebRoot -> LetsEncrypt.AgreeTOS -> [ConfigLine] -> RevertableProperty DebianLike DebianLike
    httpsVirtualHost' domain docroot letos addedcfg = setup <!> teardown
      where
            setup = setuphttp
                    `requires` modEnabled "rewrite"
                    `requires` modEnabled "ssl"
                    `before` setuphttps
            teardown = siteDisabled domain
            setuphttp = (siteEnabled' domain $
                    -- The sslconffile is only created after letsencrypt gets
                    -- the cert. The "*" is needed to make apache not error
                    -- when the file doesn't exist.
                    ("IncludeOptional " ++ sslconffile "*")
                    : vhost (Port 80)
                            [ "RewriteEngine On"
                            -- Pass through .well-known directory on http for the
                            -- letsencrypt acme challenge.
                            , "RewriteRule ^/.well-known/(.*) - [L]"
                            -- Everything else redirects to https
                            , "RewriteRule ^/(.*) https://" ++ domain ++ "/$1 [L,R,NE]"
                            ])
                    `requires` File.dirExists (takeDirectory cf)
            setuphttps = LetsEncrypt.letsEncrypt letos domain docroot
                    `onChange` postsetuphttps
            postsetuphttps = combineProperties (domain ++ " ssl cert installed") $ props
                    & File.hasContent cf sslvhost
                            `onChange` reloaded
                    -- always reload since the cert has changed
                    & reloaded
              where
                    sslvhost = vhost (Port 443)
                            [ "SSLEngine on"
                            , "SSLCertificateFile " ++ LetsEncrypt.certFile domain
                            , "SSLCertificateKeyFile " ++ LetsEncrypt.privKeyFile domain
                            , "SSLCertificateChainFile " ++ LetsEncrypt.chainFile domain
                            ]
            cf = sslconffile "letsencrypt"
            sslconffile s = "/etc/apache2/sites-available/ssl/" ++ domain ++ "/" ++ s ++ ".conf"
            vhost p ls =
                    [ "<VirtualHost *:" ++ val p ++">"
                    , "ServerName " ++ domain ++ ":" ++ val p
                    , "DocumentRoot " ++ docroot
                    , "ErrorLog /var/log/apache2/error.log"
                    , "LogLevel warn"
                    , "CustomLog /var/log/apache2/access.log combined"
                    , "ServerSignature On"
                    ] ++ ls ++ addedcfg ++
                    [ "</VirtualHost>"
                    ]