packages feed

haste-app-0.1.0.0: src/Haste/App/Sandbox.hs

{-# LANGUAGE GADTs, OverloadedStrings, ScopedTypeVariables, GeneralizedNewtypeDeriving, TypeOperators #-}
{-# LANGUAGE FlexibleInstances, MultiParamTypeClasses, TypeFamilies #-}
-- | Sandboxed FFI for Haste.App.
module Haste.App.Sandbox
  ( invokeSandbox, dependOn, withDepends
  , Perms (..), CustomSandbox, Sandbox, module Sbx
  , callSandbox, createAppSandbox, initAppSandbox, isInSandbox
  ) where
import Control.Monad
import Control.Monad.IO.Class
import Control.Monad.Reader
import Data.Default
import Data.Proxy
import Data.Typeable
import GHC.StaticPtr
import Haste
import Haste.Concurrent
import Haste.Events
import Haste.Foreign
import Haste.JSON
import Haste.Serialize
import Haste.App.Protocol
import Haste.App.Routing
import Haste.App.Sandbox.Internal
import qualified Haste.App.Sandbox.Internal as Sbx
  hiding (createSandbox, withSandbox, isInSandbox)

-- For dynamically loading JS dependencies.
import Haste.DOM.JSString

registerSbx :: String -> Window -> IO ()
registerSbx = ffi "(function(name,sbx){window['__haste_app_sbx'][name] = sbx;})"

lookupSbx :: String -> IO (Maybe Window)
lookupSbx = ffi "(function(name){return window['__haste_app_sbx'][name];})"

initSbxRegistry :: IO ()
initSbxRegistry = ffi "(function(){if(!window['__haste_app_sbx']) {window['__haste_app_sbx'] = {};};})"

-- | Returns @True@ if run from a sandbox which has already been initialized,
--   otherwise @False@. Also marks the sandbox as initialized.
initialized :: IO Bool
initialized = ffi "(function(){\
    \var prev = !!window['__haste_app_sbx_inited'];\
    \window['__haste_app_sbx_inited'] = true;\
    \return prev;\
  \})"

-- | Called in sandbox to report to host that it's done loading dependencies.
doneLoadingDeps :: IO ()
doneLoadingDeps = ffi "(function(){parent.postMessage(__haste_prog_id,'*');})"

-- | Create a Haste.App sandbox and set it up to listen to requests.
--   Only used internally, to start 'LocalNode' nodes. Called OUTSIDE sandbox
--   only.
createAppSandbox :: forall c m env.
                    (Perms m, Node m)
                 => Proxy m
                 -> CIO ()
createAppSandbox p = do
    Just sbx <- createSandbox (perms p)
    awaitLoadingDeps
    liftIO $ do
      initSbxRegistry
      registerSbx ident sbx
  where
    awaitLoadingDeps = do
      v <- newEmptyMVar
      pid <- getProgramId
      h <- window `onEvent` Message $ \msg -> do
        md <- liftIO $ fromAny (messageData msg)
        when (md == pid) $ putMVar v ()
      takeMVar v
      unregisterHandler h
    LocalNode ident = endpoint p

-- | Initialize a sandbox. Called INSIDE the sandbox only.
initAppSandbox :: (Perms m, Node m) => Proxy m -> CIO ()
initAppSandbox p = do
    alreadyInited <- liftIO initialized
    unless alreadyInited $ do
      _ <- initSandbox =<< Haste.App.Routing.init p
      liftIO $ doneLoadingDeps
      return ()
  where
    initSandbox env = do
      window `onEvent` Message $ \msg -> do
        mess <- liftIO $ fromAny (messageData msg)
        case fromJSON =<< decodeJSON mess of
          Right (ServerCall n m a) -> handleCall (messageSource msg) env n m a
          Left _                   -> return ()

    handleCall parent env nonce method args = do
      mm <- liftIO $ unsafeLookupStaticPtr method
      case mm of
        Just m -> do
          result <- deRefStaticPtr m env args
          postMessage parent $ encodeJSON $ toJSON $ ServerReply
            { srNonce = nonce
            , srResult = result
            }
        Nothing -> do
          postMessage parent $ encodeJSON $ toJSON $
            ServerEx nonce $ "no such method: " ++ show method


-- | Send a 'ServerCall' to a sandbox and wait for a reply.
callSandbox :: Nonce -> Endpoint -> JSString -> CIO JSON
callSandbox nonce (LocalNode ident) outgoing = do
  msbx <- liftIO $ lookupSbx ident
  case msbx of
    Nothing  -> error $ "no such sandbox: " ++ ident
    Just sbx -> do
      v <- newEmptyMVar
      h <- window `onEvent` Message $ \msg -> do
        m <- liftIO $ fromAny (messageData msg)
        case fromJSON =<< decodeJSON m of
          Right (ServerReply n result) | nonce == n -> do
            preventDefault
            putMVar v result
          _ -> do
            return ()
      postMessage sbx outgoing
      res <- takeMVar v
      unregisterHandler h
      return res

-- | A standard sandbox, with no environment and no extra permissions.
type Sandbox = CustomSandbox AllowNone ()

class Perms m where
  perms :: Proxy (m :: * -> *) -> JSString

instance Permission perms => Perms (CustomSandbox perms env t) where
  perms _ = showPermissions (Proxy :: Proxy perms)

-- | A node executing in a sandboxed iframe. The permissions of the sandbox
--   are given by the @perm@ type argument; see 'Permission' for details.
newtype CustomSandbox perm env t a = Sandbox (EnvServer env a)
  deriving (Functor, Applicative, Monad, MonadIO, MonadConc, MonadReader env)

-- | Invoke a sandboxed computation. Use for @Mapping Sandbox@ instances.
--   However, in pretty much all circumstances, the provided @Mapping@ instance
--   should be the only one needed for any sandbox.
invokeSandbox :: env -> CustomSandbox perm env t a -> CIO a
invokeSandbox env (Sandbox m) = invokeServer env m

-- | Initialize a sandbox by loading the given dependencies and then running
--   the given initialization function. Dependencies are loaded in order,
--   in case some of them depend on each other.
--   Use this for the @init@ method when creating @Node@ instances for
--   sandboxes:
--
-- > instance Node (Sandbox SomePermissions SomeEnv) where
-- >   init = initializeStuff `withDepends` ["http://.../script.js", ...]
-- >   ...
withDepends :: CIO env -> [URL] -> Proxy (m :: * -> *) -> CIO env
withDepends extraInit deps _ = do
    v <- newEmptyMVar
    addScript v deps
    takeMVar v
  where
    addScript v (url:us) = do
      e <- newElem "script"
      appendChild documentBody e
      e `onEvent` Load $ \_ -> addScript v us
      setProp e "src" url
    addScript v _ = do
      extraInit >>= putMVar v

-- | Initialize a sandbox by loading the given dependencies and then returning
--   the default value for the sandbox' environment.
dependOn :: Default env => [URL] -> Proxy (m :: * -> *) -> CIO env
dependOn deps = pure def `withDepends` deps

instance env ~ Env (CustomSandbox perm env t) => Mapping (CustomSandbox perm env t) a where
  invoke = invokeSandbox