/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#ifndef nsFrameMessageManager_h__
#define nsFrameMessageManager_h__

#include "nsIMessageManager.h"
#include "nsIObserver.h"
#include "nsCOMPtr.h"
#include "nsAutoPtr.h"
#include "nsCOMArray.h"
#include "nsTArray.h"
#include "nsIAtom.h"
#include "nsCycleCollectionParticipant.h"
#include "nsTArray.h"
#include "nsIPrincipal.h"
#include "nsIXPConnect.h"
#include "nsDataHashtable.h"
#include "nsClassHashtable.h"
#include "mozilla/Services.h"
#include "mozilla/StaticPtr.h"
#include "nsIObserverService.h"
#include "nsThreadUtils.h"
#include "nsWeakPtr.h"
#include "mozilla/Attributes.h"
#include "js/RootingAPI.h"
#include "nsTObserverArray.h"
#include "mozilla/dom/SameProcessMessageQueue.h"
#include "mozilla/dom/ipc/StructuredCloneData.h"
#include "mozilla/jsipc/CpowHolder.h"

class nsIFrameLoader;

namespace mozilla {
namespace dom {

class nsIContentParent;
class nsIContentChild;
class ClonedMessageData;
class MessageManagerReporter;

namespace ipc {

enum MessageManagerFlags {
  MM_CHILD = 0,
  MM_CHROME = 1,
  MM_GLOBAL = 2,
  MM_PROCESSMANAGER = 4,
  MM_BROADCASTER = 8,
  MM_OWNSCALLBACK = 16
};

class MessageManagerCallback
{
public:
  virtual ~MessageManagerCallback() {}

  virtual bool DoLoadMessageManagerScript(const nsAString& aURL, bool aRunInGlobalScope)
  {
    return true;
  }

  virtual bool DoSendBlockingMessage(JSContext* aCx,
                                     const nsAString& aMessage,
                                     StructuredCloneData& aData,
                                     JS::Handle<JSObject*> aCpows,
                                     nsIPrincipal* aPrincipal,
                                     nsTArray<StructuredCloneData>* aRetVal,
                                     bool aIsSync)
  {
    return true;
  }

  virtual nsresult DoSendAsyncMessage(JSContext* aCx,
                                      const nsAString& aMessage,
                                      StructuredCloneData& aData,
                                      JS::Handle<JSObject*> aCpows,
                                      nsIPrincipal* aPrincipal)
  {
    return NS_OK;
  }

  virtual bool CheckPermission(const nsAString& aPermission)
  {
    return false;
  }

  virtual bool CheckManifestURL(const nsAString& aManifestURL)
  {
    return false;
  }

  virtual bool CheckAppHasPermission(const nsAString& aPermission)
  {
    return false;
  }

  virtual bool CheckAppHasStatus(unsigned short aStatus)
  {
    return false;
  }

  virtual bool KillChild()
  {
    // By default, does nothing.
    return false;
  }

protected:
  bool BuildClonedMessageDataForParent(nsIContentParent* aParent,
                                       StructuredCloneData& aData,
                                       ClonedMessageData& aClonedData);
  bool BuildClonedMessageDataForChild(nsIContentChild* aChild,
                                      StructuredCloneData& aData,
                                      ClonedMessageData& aClonedData);
};

void UnpackClonedMessageDataForParent(const ClonedMessageData& aClonedData,
                                      StructuredCloneData& aData);

void UnpackClonedMessageDataForChild(const ClonedMessageData& aClonedData,
                                     StructuredCloneData& aData);

} // namespace ipc
} // namespace dom
} // namespace mozilla

struct nsMessageListenerInfo
{
  bool operator==(const nsMessageListenerInfo& aOther) const
  {
    return &aOther == this;
  }

  // Exactly one of mStrongListener and mWeakListener must be non-null.
  nsCOMPtr<nsIMessageListener> mStrongListener;
  nsWeakPtr mWeakListener;
  bool mListenWhenClosed;
};


class MOZ_STACK_CLASS SameProcessCpowHolder : public mozilla::jsipc::CpowHolder
{
public:
  SameProcessCpowHolder(JS::RootingContext* aRootingCx, JS::Handle<JSObject*> aObj)
    : mObj(aRootingCx, aObj)
  {
  }

  virtual bool ToObject(JSContext* aCx, JS::MutableHandle<JSObject*> aObjp)
    override;

private:
  JS::Rooted<JSObject*> mObj;
};

class nsFrameMessageManager final : public nsIContentFrameMessageManager,
                                    public nsIMessageBroadcaster,
                                    public nsIFrameScriptLoader,
                                    public nsIGlobalProcessScriptLoader,
                                    public nsIProcessChecker
{
  friend class mozilla::dom::MessageManagerReporter;
  typedef mozilla::dom::ipc::StructuredCloneData StructuredCloneData;
public:
  nsFrameMessageManager(mozilla::dom::ipc::MessageManagerCallback* aCallback,
                        nsFrameMessageManager* aParentManager,
                        /* mozilla::dom::ipc::MessageManagerFlags */ uint32_t aFlags);

private:
  ~nsFrameMessageManager();

public:
  NS_DECL_CYCLE_COLLECTING_ISUPPORTS
  NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_AMBIGUOUS(nsFrameMessageManager,
                                                         nsIContentFrameMessageManager)
  NS_DECL_NSIMESSAGELISTENERMANAGER
  NS_DECL_NSIMESSAGESENDER
  NS_DECL_NSIMESSAGEBROADCASTER
  NS_DECL_NSISYNCMESSAGESENDER
  NS_DECL_NSIMESSAGEMANAGERGLOBAL
  NS_DECL_NSICONTENTFRAMEMESSAGEMANAGER
  NS_DECL_NSIFRAMESCRIPTLOADER
  NS_DECL_NSIPROCESSSCRIPTLOADER
  NS_DECL_NSIGLOBALPROCESSSCRIPTLOADER
  NS_DECL_NSIPROCESSCHECKER

  static nsFrameMessageManager*
  NewProcessMessageManager(bool aIsRemote);

  nsresult ReceiveMessage(nsISupports* aTarget, nsIFrameLoader* aTargetFrameLoader,
                          const nsAString& aMessage,
                          bool aIsSync, StructuredCloneData* aCloneData,
                          mozilla::jsipc::CpowHolder* aCpows, nsIPrincipal* aPrincipal,
                          nsTArray<StructuredCloneData>* aRetVal);

  void AddChildManager(nsFrameMessageManager* aManager);
  void RemoveChildManager(nsFrameMessageManager* aManager)
  {
    mChildManagers.RemoveObject(aManager);
  }
  void Disconnect(bool aRemoveFromParent = true);
  void Close();

  void InitWithCallback(mozilla::dom::ipc::MessageManagerCallback* aCallback);
  void SetCallback(mozilla::dom::ipc::MessageManagerCallback* aCallback);
  mozilla::dom::ipc::MessageManagerCallback* GetCallback()
  {
    return mCallback;
  }

  nsresult DispatchAsyncMessage(const nsAString& aMessageName,
                                const JS::Value& aJSON,
                                const JS::Value& aObjects,
                                nsIPrincipal* aPrincipal,
                                const JS::Value& aTransfers,
                                JSContext* aCx,
                                uint8_t aArgc);

  nsresult DispatchAsyncMessageInternal(JSContext* aCx,
                                        const nsAString& aMessage,
                                        StructuredCloneData& aData,
                                        JS::Handle<JSObject*> aCpows,
                                        nsIPrincipal* aPrincipal);
  void RemoveFromParent();
  nsFrameMessageManager* GetParentManager() { return mParentManager; }
  void SetParentManager(nsFrameMessageManager* aParent)
  {
    NS_ASSERTION(!mParentManager, "We have parent manager already!");
    NS_ASSERTION(mChrome, "Should not set parent manager!");
    mParentManager = aParent;
  }
  bool IsGlobal() { return mGlobal; }
  bool IsBroadcaster() { return mIsBroadcaster; }

  static nsFrameMessageManager* GetParentProcessManager()
  {
    return sParentProcessManager;
  }
  static nsFrameMessageManager* GetChildProcessManager()
  {
    return sChildProcessManager;
  }
  static void SetChildProcessManager(nsFrameMessageManager* aManager)
  {
    sChildProcessManager = aManager;
  }

  void SetInitialProcessData(JS::HandleValue aInitialData);

  void LoadPendingScripts();

private:
  nsresult SendMessage(const nsAString& aMessageName,
                       JS::Handle<JS::Value> aJSON,
                       JS::Handle<JS::Value> aObjects,
                       nsIPrincipal* aPrincipal,
                       JSContext* aCx,
                       uint8_t aArgc,
                       JS::MutableHandle<JS::Value> aRetval,
                       bool aIsSync);

  nsresult ReceiveMessage(nsISupports* aTarget, nsIFrameLoader* aTargetFrameLoader,
                          bool aTargetClosed, const nsAString& aMessage,
                          bool aIsSync, StructuredCloneData* aCloneData,
                          mozilla::jsipc::CpowHolder* aCpows, nsIPrincipal* aPrincipal,
                          nsTArray<StructuredCloneData>* aRetVal);

  NS_IMETHOD LoadScript(const nsAString& aURL,
                        bool aAllowDelayedLoad,
                        bool aRunInGlobalScope);
  NS_IMETHOD RemoveDelayedScript(const nsAString& aURL);
  NS_IMETHOD GetDelayedScripts(JSContext* aCx, JS::MutableHandle<JS::Value> aList);

protected:
  friend class MMListenerRemover;
  // We keep the message listeners as arrays in a hastable indexed by the
  // message name. That gives us fast lookups in ReceiveMessage().
  nsClassHashtable<nsStringHashKey,
                   nsAutoTObserverArray<nsMessageListenerInfo, 1>> mListeners;
  nsCOMArray<nsIContentFrameMessageManager> mChildManagers;
  bool mChrome;     // true if we're in the chrome process
  bool mGlobal;     // true if we're the global frame message manager
  bool mIsProcessManager; // true if the message manager belongs to the process realm
  bool mIsBroadcaster; // true if the message manager is a broadcaster
  bool mOwnsCallback;
  bool mHandlingMessage;
  bool mClosed;    // true if we can no longer send messages
  bool mDisconnected;
  mozilla::dom::ipc::MessageManagerCallback* mCallback;
  nsAutoPtr<mozilla::dom::ipc::MessageManagerCallback> mOwnedCallback;
  RefPtr<nsFrameMessageManager> mParentManager;
  nsTArray<nsString> mPendingScripts;
  nsTArray<bool> mPendingScriptsGlobalStates;
  JS::Heap<JS::Value> mInitialProcessData;

  void LoadPendingScripts(nsFrameMessageManager* aManager,
                          nsFrameMessageManager* aChildMM);
public:
  static nsFrameMessageManager* sParentProcessManager;
  static nsFrameMessageManager* sSameProcessParentManager;
  static nsTArray<nsCOMPtr<nsIRunnable> >* sPendingSameProcessAsyncMessages;
private:
  static nsFrameMessageManager* sChildProcessManager;
  enum ProcessCheckerType {
    PROCESS_CHECKER_PERMISSION,
    PROCESS_CHECKER_MANIFEST_URL,
    ASSERT_APP_HAS_PERMISSION
  };
  nsresult AssertProcessInternal(ProcessCheckerType aType,
                                 const nsAString& aCapability,
                                 bool* aValid);
};

/* A helper class for taking care of many details for async message sending
   within a single process.  Intended to be used like so:

   class MyAsyncMessage : public nsSameProcessAsyncMessageBase, public Runnable
   {
     NS_IMETHOD Run() override {
       ReceiveMessage(..., ...);
       return NS_OK;
     }
   };


   RefPtr<nsSameProcessAsyncMessageBase> ev = new MyAsyncMessage();
   nsresult rv = ev->Init(...);
   if (NS_SUCCEEDED(rv)) {
     NS_DispatchToMainThread(ev);
   }
*/
class nsSameProcessAsyncMessageBase
{
public:
  typedef mozilla::dom::ipc::StructuredCloneData StructuredCloneData;

  nsSameProcessAsyncMessageBase(JS::RootingContext* aRootingCx,
                                JS::Handle<JSObject*> aCpows);
  nsresult Init(const nsAString& aMessage,
                StructuredCloneData& aData,
                nsIPrincipal* aPrincipal);

  void ReceiveMessage(nsISupports* aTarget, nsIFrameLoader* aTargetFrameLoader,
                      nsFrameMessageManager* aManager);
private:
  nsSameProcessAsyncMessageBase(const nsSameProcessAsyncMessageBase&);

  JS::RootingContext* mRootingCx;
  nsString mMessage;
  StructuredCloneData mData;
  JS::PersistentRooted<JSObject*> mCpows;
  nsCOMPtr<nsIPrincipal> mPrincipal;
#ifdef DEBUG
  bool mCalledInit;
#endif
};

class nsScriptCacheCleaner;

struct nsMessageManagerScriptHolder
{
  nsMessageManagerScriptHolder(JSContext* aCx,
                               JSScript* aScript,
                               bool aRunInGlobalScope)
   : mScript(aCx, aScript), mRunInGlobalScope(aRunInGlobalScope)
  { MOZ_COUNT_CTOR(nsMessageManagerScriptHolder); }

  ~nsMessageManagerScriptHolder()
  { MOZ_COUNT_DTOR(nsMessageManagerScriptHolder); }

  bool WillRunInGlobalScope() { return mRunInGlobalScope; }

  JS::PersistentRooted<JSScript*> mScript;
  bool mRunInGlobalScope;
};

class nsMessageManagerScriptExecutor
{
public:
  static void PurgeCache();
  static void Shutdown();
  already_AddRefed<nsIXPConnectJSObjectHolder> GetGlobal()
  {
    nsCOMPtr<nsIXPConnectJSObjectHolder> ref = mGlobal;
    return ref.forget();
  }

  void MarkScopesForCC();
protected:
  friend class nsMessageManagerScriptCx;
  nsMessageManagerScriptExecutor() { MOZ_COUNT_CTOR(nsMessageManagerScriptExecutor); }
  ~nsMessageManagerScriptExecutor() { MOZ_COUNT_DTOR(nsMessageManagerScriptExecutor); }

  void DidCreateGlobal();
  void LoadScriptInternal(const nsAString& aURL, bool aRunInGlobalScope);
  void TryCacheLoadAndCompileScript(const nsAString& aURL,
                                    bool aRunInGlobalScope,
                                    bool aShouldCache,
                                    JS::MutableHandle<JSScript*> aScriptp);
  void TryCacheLoadAndCompileScript(const nsAString& aURL,
                                    bool aRunInGlobalScope);
  bool InitChildGlobalInternal(nsISupports* aScope, const nsACString& aID);
  void Trace(const TraceCallbacks& aCallbacks, void* aClosure);
  nsCOMPtr<nsIXPConnectJSObjectHolder> mGlobal;
  nsCOMPtr<nsIPrincipal> mPrincipal;
  AutoTArray<JS::Heap<JSObject*>, 2> mAnonymousGlobalScopes;

  static nsDataHashtable<nsStringHashKey, nsMessageManagerScriptHolder*>* sCachedScripts;
  static mozilla::StaticRefPtr<nsScriptCacheCleaner> sScriptCacheCleaner;
};

class nsScriptCacheCleaner final : public nsIObserver
{
  ~nsScriptCacheCleaner() {}

  NS_DECL_ISUPPORTS

  nsScriptCacheCleaner()
  {
    nsCOMPtr<nsIObserverService> obsSvc = mozilla::services::GetObserverService();
    if (obsSvc) {
      obsSvc->AddObserver(this, "message-manager-flush-caches", false);
      obsSvc->AddObserver(this, "xpcom-shutdown", false);
    }
  }

  NS_IMETHOD Observe(nsISupports *aSubject,
                     const char *aTopic,
                     const char16_t *aData) override
  {
    if (strcmp("message-manager-flush-caches", aTopic) == 0) {
      nsMessageManagerScriptExecutor::PurgeCache();
    } else if (strcmp("xpcom-shutdown", aTopic) == 0) {
      nsMessageManagerScriptExecutor::Shutdown();
    }
    return NS_OK;
  }
};

#endif
