/* -*- 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/. */

#include "mozilla/dom/Notification.h"

#include "mozilla/Move.h"
#include "mozilla/OwningNonNull.h"
#include "mozilla/Preferences.h"
#include "mozilla/Services.h"
#include "mozilla/Telemetry.h"
#include "mozilla/unused.h"

#include "mozilla/dom/AppNotificationServiceOptionsBinding.h"
#include "mozilla/dom/BindingUtils.h"
#include "mozilla/dom/ContentChild.h"
#include "mozilla/dom/NotificationEvent.h"
#include "mozilla/dom/PermissionMessageUtils.h"
#include "mozilla/dom/Promise.h"
#include "mozilla/dom/PromiseWorkerProxy.h"
#include "mozilla/dom/ServiceWorkerGlobalScopeBinding.h"

#include "nsAlertsUtils.h"
#include "nsComponentManagerUtils.h"
#include "nsContentPermissionHelper.h"
#include "nsContentUtils.h"
#include "nsCRTGlue.h"
#include "nsDOMJSUtils.h"
#include "nsGlobalWindow.h"
#include "nsIAlertsService.h"
#include "nsIAppsService.h"
#include "nsIContentPermissionPrompt.h"
#include "nsIDocument.h"
#include "nsILoadContext.h"
#include "nsINotificationStorage.h"
#include "nsIPermissionManager.h"
#include "nsIPermission.h"
#include "nsIScriptSecurityManager.h"
#include "nsIServiceWorkerManager.h"
#include "nsISimpleEnumerator.h"
#include "nsIUUIDGenerator.h"
#include "nsIXPConnect.h"
#include "nsNetUtil.h"
#include "nsProxyRelease.h"
#include "nsServiceManagerUtils.h"
#include "nsStructuredCloneContainer.h"
#include "nsThreadUtils.h"
#include "nsToolkitCompsCID.h"
#include "nsXULAppAPI.h"
#include "ServiceWorkerManager.h"
#include "WorkerPrivate.h"
#include "WorkerRunnable.h"
#include "WorkerScope.h"

#ifdef MOZ_B2G
#include "nsIDOMDesktopNotification.h"
#endif

#ifndef MOZ_SIMPLEPUSH
#include "nsIPushService.h"
#endif

namespace mozilla {
namespace dom {

using namespace workers;

struct NotificationStrings
{
  const nsString mID;
  const nsString mTitle;
  const nsString mDir;
  const nsString mLang;
  const nsString mBody;
  const nsString mTag;
  const nsString mIcon;
  const nsString mData;
  const nsString mBehavior;
  const nsString mServiceWorkerRegistrationID;
};

class ScopeCheckingGetCallback : public nsINotificationStorageCallback
{
  const nsString mScope;
public:
  explicit ScopeCheckingGetCallback(const nsAString& aScope)
    : mScope(aScope)
  {}

  NS_IMETHOD Handle(const nsAString& aID,
                    const nsAString& aTitle,
                    const nsAString& aDir,
                    const nsAString& aLang,
                    const nsAString& aBody,
                    const nsAString& aTag,
                    const nsAString& aIcon,
                    const nsAString& aData,
                    const nsAString& aBehavior,
                    const nsAString& aServiceWorkerRegistrationID) final
  {
    AssertIsOnMainThread();
    MOZ_ASSERT(!aID.IsEmpty());

    // Skip scopes that don't match when called from getNotifications().
    if (!mScope.IsEmpty() && !mScope.Equals(aServiceWorkerRegistrationID)) {
      return NS_OK;
    }

    NotificationStrings strings = {
      nsString(aID),
      nsString(aTitle),
      nsString(aDir),
      nsString(aLang),
      nsString(aBody),
      nsString(aTag),
      nsString(aIcon),
      nsString(aData),
      nsString(aBehavior),
      nsString(aServiceWorkerRegistrationID),
    };

    mStrings.AppendElement(Move(strings));
    return NS_OK;
  }

  NS_IMETHOD Done() override = 0;

protected:
  virtual ~ScopeCheckingGetCallback()
  {}

  nsTArray<NotificationStrings> mStrings;
};

class NotificationStorageCallback final : public ScopeCheckingGetCallback
{
public:
  NS_DECL_CYCLE_COLLECTING_ISUPPORTS
  NS_DECL_CYCLE_COLLECTION_CLASS(NotificationStorageCallback)

  NotificationStorageCallback(nsIGlobalObject* aWindow, const nsAString& aScope,
                              Promise* aPromise)
    : ScopeCheckingGetCallback(aScope),
      mWindow(aWindow),
      mPromise(aPromise)
  {
    AssertIsOnMainThread();
    MOZ_ASSERT(aWindow);
    MOZ_ASSERT(aPromise);
  }

  NS_IMETHOD Done() final
  {
    ErrorResult result;
    AutoTArray<RefPtr<Notification>, 5> notifications;

    for (uint32_t i = 0; i < mStrings.Length(); ++i) {
      RefPtr<Notification> n =
        Notification::ConstructFromFields(mWindow,
                                          mStrings[i].mID,
                                          mStrings[i].mTitle,
                                          mStrings[i].mDir,
                                          mStrings[i].mLang,
                                          mStrings[i].mBody,
                                          mStrings[i].mTag,
                                          mStrings[i].mIcon,
                                          mStrings[i].mData,
                                          /* mStrings[i].mBehavior, not
                                           * supported */
                                          mStrings[i].mServiceWorkerRegistrationID,
                                          result);

      n->SetStoredState(true);
      Unused << NS_WARN_IF(result.Failed());
      if (!result.Failed()) {
        notifications.AppendElement(n.forget());
      }
    }

    mPromise->MaybeResolve(notifications);
    return NS_OK;
  }

private:
  virtual ~NotificationStorageCallback()
  {}

  nsCOMPtr<nsIGlobalObject> mWindow;
  RefPtr<Promise> mPromise;
  const nsString mScope;
};

NS_IMPL_CYCLE_COLLECTING_ADDREF(NotificationStorageCallback)
NS_IMPL_CYCLE_COLLECTING_RELEASE(NotificationStorageCallback)
NS_IMPL_CYCLE_COLLECTION(NotificationStorageCallback, mWindow, mPromise);

NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(NotificationStorageCallback)
  NS_INTERFACE_MAP_ENTRY(nsINotificationStorageCallback)
  NS_INTERFACE_MAP_ENTRY(nsISupports)
NS_INTERFACE_MAP_END

class NotificationGetRunnable final : public Runnable
{
  const nsString mOrigin;
  const nsString mTag;
  nsCOMPtr<nsINotificationStorageCallback> mCallback;
public:
  NotificationGetRunnable(const nsAString& aOrigin,
                          const nsAString& aTag,
                          nsINotificationStorageCallback* aCallback)
    : mOrigin(aOrigin), mTag(aTag), mCallback(aCallback)
  {}

  NS_IMETHOD
  Run() override
  {
    nsresult rv;
    nsCOMPtr<nsINotificationStorage> notificationStorage =
      do_GetService(NS_NOTIFICATION_STORAGE_CONTRACTID, &rv);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return rv;
    }

    rv = notificationStorage->Get(mOrigin, mTag, mCallback);
    //XXXnsm Is it guaranteed mCallback will be called in case of failure?
    Unused << NS_WARN_IF(NS_FAILED(rv));
    return rv;
  }
};

class NotificationPermissionRequest : public nsIContentPermissionRequest,
                                      public nsIRunnable
{
public:
  NS_DECL_CYCLE_COLLECTING_ISUPPORTS
  NS_DECL_NSICONTENTPERMISSIONREQUEST
  NS_DECL_NSIRUNNABLE
  NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(NotificationPermissionRequest,
                                           nsIContentPermissionRequest)

  NotificationPermissionRequest(nsIPrincipal* aPrincipal,
                                nsPIDOMWindowInner* aWindow, Promise* aPromise,
                                NotificationPermissionCallback* aCallback)
    : mPrincipal(aPrincipal), mWindow(aWindow),
      mPermission(NotificationPermission::Default),
      mPromise(aPromise),
      mCallback(aCallback)
  {
    MOZ_ASSERT(aPromise);
    mRequester = new nsContentPermissionRequester(mWindow);
  }

protected:
  virtual ~NotificationPermissionRequest() {}

  nsresult ResolvePromise();
  nsresult DispatchResolvePromise();
  nsCOMPtr<nsIPrincipal> mPrincipal;
  nsCOMPtr<nsPIDOMWindowInner> mWindow;
  NotificationPermission mPermission;
  RefPtr<Promise> mPromise;
  RefPtr<NotificationPermissionCallback> mCallback;
  nsCOMPtr<nsIContentPermissionRequester> mRequester;
};

namespace {
class ReleaseNotificationControlRunnable final : public MainThreadWorkerControlRunnable
{
  Notification* mNotification;

public:
  explicit ReleaseNotificationControlRunnable(Notification* aNotification)
    : MainThreadWorkerControlRunnable(aNotification->mWorkerPrivate)
    , mNotification(aNotification)
  { }

  bool
  WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override
  {
    mNotification->ReleaseObject();
    return true;
  }
};

class GetPermissionRunnable final : public WorkerMainThreadRunnable
{
  NotificationPermission mPermission;

public:
  explicit GetPermissionRunnable(WorkerPrivate* aWorker)
    : WorkerMainThreadRunnable(aWorker,
                               NS_LITERAL_CSTRING("Notification :: Get Permission"))
    , mPermission(NotificationPermission::Denied)
  { }

  bool
  MainThreadRun() override
  {
    ErrorResult result;
    mPermission =
      Notification::GetPermissionInternal(mWorkerPrivate->GetPrincipal(),
                                          result);
    return true;
  }

  NotificationPermission
  GetPermission()
  {
    return mPermission;
  }
};

class FocusWindowRunnable final : public Runnable
{
  nsMainThreadPtrHandle<nsPIDOMWindowInner> mWindow;
public:
  explicit FocusWindowRunnable(const nsMainThreadPtrHandle<nsPIDOMWindowInner>& aWindow)
    : mWindow(aWindow)
  { }

  NS_IMETHOD
  Run()
  {
    AssertIsOnMainThread();
    if (!mWindow->IsCurrentInnerWindow()) {
      // Window has been closed, this observer is not valid anymore
      return NS_OK;
    }

    nsIDocument* doc = mWindow->GetExtantDoc();
    if (doc) {
      // Browser UI may use DOMWebNotificationClicked to focus the tab
      // from which the event was dispatched.
      nsContentUtils::DispatchChromeEvent(doc, mWindow->GetOuterWindow(),
                                          NS_LITERAL_STRING("DOMWebNotificationClicked"),
                                          true, true);
    }

    return NS_OK;
  }
};

nsresult
CheckScope(nsIPrincipal* aPrincipal, const nsACString& aScope)
{
  AssertIsOnMainThread();
  MOZ_ASSERT(aPrincipal);

  nsCOMPtr<nsIURI> scopeURI;
  nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), aScope, nullptr, nullptr);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  return aPrincipal->CheckMayLoad(scopeURI, /* report = */ true,
                                  /* allowIfInheritsPrincipal = */ false);
}
} // anonymous namespace

// Subclass that can be directly dispatched to child workers from the main
// thread.
class NotificationWorkerRunnable : public WorkerRunnable
{
protected:
  explicit NotificationWorkerRunnable(WorkerPrivate* aWorkerPrivate)
    : WorkerRunnable(aWorkerPrivate, WorkerThreadUnchangedBusyCount)
  {
  }

  bool
  PreDispatch(WorkerPrivate* aWorkerPrivate) override
  {
    // We don't call WorkerRunnable::PreDispatch because it would assert the
    // wrong thing about which thread we're on.
    AssertIsOnMainThread();
    return true;
  }

  void
  PostDispatch(WorkerPrivate* aWorkerPrivate, bool aDispatchResult) override
  {
    // We don't call WorkerRunnable::PostDispatch because it would assert the
    // wrong thing about which thread we're on.
    AssertIsOnMainThread();
  }

  bool
  WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override
  {
    aWorkerPrivate->AssertIsOnWorkerThread();
    aWorkerPrivate->ModifyBusyCountFromWorker(true);
    WorkerRunInternal(aWorkerPrivate);
    return true;
  }

  void
  PostRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate,
          bool aRunResult) override
  {
    aWorkerPrivate->ModifyBusyCountFromWorker(false);
  }

  virtual void
  WorkerRunInternal(WorkerPrivate* aWorkerPrivate) = 0;
};

// Overrides dispatch and run handlers so we can directly dispatch from main
// thread to child workers.
class NotificationEventWorkerRunnable final : public NotificationWorkerRunnable
{
  Notification* mNotification;
  const nsString mEventName;
public:
  NotificationEventWorkerRunnable(Notification* aNotification,
                                  const nsString& aEventName)
    : NotificationWorkerRunnable(aNotification->mWorkerPrivate)
    , mNotification(aNotification)
    , mEventName(aEventName)
  {}

  void
  WorkerRunInternal(WorkerPrivate* aWorkerPrivate) override
  {
    mNotification->DispatchTrustedEvent(mEventName);
  }
};

class ReleaseNotificationRunnable final : public NotificationWorkerRunnable
{
  Notification* mNotification;
public:
  explicit ReleaseNotificationRunnable(Notification* aNotification)
    : NotificationWorkerRunnable(aNotification->mWorkerPrivate)
    , mNotification(aNotification)
  {}

  void
  WorkerRunInternal(WorkerPrivate* aWorkerPrivate) override
  {
    mNotification->ReleaseObject();
  }
};

// Create one whenever you require ownership of the notification. Use with
// UniquePtr<>. See Notification.h for details.
class NotificationRef final {
  friend class WorkerNotificationObserver;

private:
  Notification* mNotification;
  bool mInited;

  // Only useful for workers.
  void
  Forget()
  {
    mNotification = nullptr;
  }

public:
  explicit NotificationRef(Notification* aNotification)
    : mNotification(aNotification)
  {
    MOZ_ASSERT(mNotification);
    if (mNotification->mWorkerPrivate) {
      mNotification->mWorkerPrivate->AssertIsOnWorkerThread();
    } else {
      AssertIsOnMainThread();
    }

    mInited = mNotification->AddRefObject();
  }

  // This is only required because Gecko runs script in a worker's onclose
  // handler (non-standard, Bug 790919) where calls to AddFeature() will fail.
  // Due to non-standardness and added complications if we decide to support
  // this, attempts to create a Notification in onclose just throw exceptions.
  bool
  Initialized()
  {
    return mInited;
  }

  ~NotificationRef()
  {
    if (Initialized() && mNotification) {
      Notification* notification = mNotification;
      mNotification = nullptr;
      if (notification->mWorkerPrivate && NS_IsMainThread()) {
        // Try to pass ownership back to the worker. If the dispatch succeeds we
        // are guaranteed this runnable will run, and that it will run after queued
        // event runnables, so event runnables will have a safe pointer to the
        // Notification.
        //
        // If the dispatch fails, the worker isn't running anymore and the event
        // runnables have already run or been canceled. We can use a control
        // runnable to release the reference.
        RefPtr<ReleaseNotificationRunnable> r =
          new ReleaseNotificationRunnable(notification);

        if (!r->Dispatch()) {
          RefPtr<ReleaseNotificationControlRunnable> r =
            new ReleaseNotificationControlRunnable(notification);
          MOZ_ALWAYS_TRUE(r->Dispatch());
        }
      } else {
        notification->AssertIsOnTargetThread();
        notification->ReleaseObject();
      }
    }
  }

  // XXXnsm, is it worth having some sort of WeakPtr like wrapper instead of
  // a rawptr that the NotificationRef can invalidate?
  Notification*
  GetNotification()
  {
    MOZ_ASSERT(Initialized());
    return mNotification;
  }
};

class NotificationTask : public Runnable
{
public:
  enum NotificationAction {
    eShow,
    eClose
  };

  NotificationTask(UniquePtr<NotificationRef> aRef, NotificationAction aAction)
    : mNotificationRef(Move(aRef)), mAction(aAction)
  {}

  NS_IMETHOD
  Run() override;
protected:
  virtual ~NotificationTask() {}

  UniquePtr<NotificationRef> mNotificationRef;
  NotificationAction mAction;
};

uint32_t Notification::sCount = 0;

NS_IMPL_CYCLE_COLLECTION(NotificationPermissionRequest, mWindow, mPromise,
                                                        mCallback)

NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(NotificationPermissionRequest)
  NS_INTERFACE_MAP_ENTRY(nsIContentPermissionRequest)
  NS_INTERFACE_MAP_ENTRY(nsIRunnable)
  NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIContentPermissionRequest)
NS_INTERFACE_MAP_END

NS_IMPL_CYCLE_COLLECTING_ADDREF(NotificationPermissionRequest)
NS_IMPL_CYCLE_COLLECTING_RELEASE(NotificationPermissionRequest)

NS_IMETHODIMP
NotificationPermissionRequest::Run()
{
  if (nsContentUtils::IsSystemPrincipal(mPrincipal)) {
    mPermission = NotificationPermission::Granted;
  } else {
    // File are automatically granted permission.
    nsCOMPtr<nsIURI> uri;
    mPrincipal->GetURI(getter_AddRefs(uri));

    if (uri) {
      bool isFile;
      uri->SchemeIs("file", &isFile);
      if (isFile) {
        mPermission = NotificationPermission::Granted;
      }
    }
  }

  // Grant permission if pref'ed on.
  if (Preferences::GetBool("notification.prompt.testing", false)) {
    if (Preferences::GetBool("notification.prompt.testing.allow", true)) {
      mPermission = NotificationPermission::Granted;
    } else {
      mPermission = NotificationPermission::Denied;
    }
  }

  if (mPermission != NotificationPermission::Default) {
    return DispatchResolvePromise();
  }

  return nsContentPermissionUtils::AskPermission(this, mWindow);
}

NS_IMETHODIMP
NotificationPermissionRequest::GetPrincipal(nsIPrincipal** aRequestingPrincipal)
{
  NS_ADDREF(*aRequestingPrincipal = mPrincipal);
  return NS_OK;
}

NS_IMETHODIMP
NotificationPermissionRequest::GetWindow(mozIDOMWindow** aRequestingWindow)
{
  NS_ADDREF(*aRequestingWindow = mWindow);
  return NS_OK;
}

NS_IMETHODIMP
NotificationPermissionRequest::GetElement(nsIDOMElement** aElement)
{
  NS_ENSURE_ARG_POINTER(aElement);
  *aElement = nullptr;
  return NS_OK;
}

NS_IMETHODIMP
NotificationPermissionRequest::Cancel()
{
  // `Cancel` is called if the user denied permission or dismissed the
  // permission request. To distinguish between the two, we set the
  // permission to "default" and query the permission manager in
  // `ResolvePromise`.
  mPermission = NotificationPermission::Default;
  return DispatchResolvePromise();
}

NS_IMETHODIMP
NotificationPermissionRequest::Allow(JS::HandleValue aChoices)
{
  MOZ_ASSERT(aChoices.isUndefined());

  mPermission = NotificationPermission::Granted;
  return DispatchResolvePromise();
}

NS_IMETHODIMP
NotificationPermissionRequest::GetRequester(nsIContentPermissionRequester** aRequester)
{
  NS_ENSURE_ARG_POINTER(aRequester);

  nsCOMPtr<nsIContentPermissionRequester> requester = mRequester;
  requester.forget(aRequester);
  return NS_OK;
}

inline nsresult
NotificationPermissionRequest::DispatchResolvePromise()
{
  return NS_DispatchToMainThread(NewRunnableMethod(this,
                                                   &NotificationPermissionRequest::ResolvePromise));
}

nsresult
NotificationPermissionRequest::ResolvePromise()
{
  nsresult rv = NS_OK;
  if (mPermission == NotificationPermission::Default) {
    // This will still be "default" if the user dismissed the doorhanger,
    // or "denied" otherwise.
    mPermission = Notification::TestPermission(mPrincipal);
  }
  if (mCallback) {
    ErrorResult error;
    mCallback->Call(mPermission, error);
    rv = error.StealNSResult();
  }
  Telemetry::Accumulate(
    Telemetry::WEB_NOTIFICATION_REQUEST_PERMISSION_CALLBACK, !!mCallback);
  mPromise->MaybeResolve(mPermission);
  return rv;
}

NS_IMETHODIMP
NotificationPermissionRequest::GetTypes(nsIArray** aTypes)
{
  nsTArray<nsString> emptyOptions;
  return nsContentPermissionUtils::CreatePermissionArray(NS_LITERAL_CSTRING("desktop-notification"),
                                                         NS_LITERAL_CSTRING("unused"),
                                                         emptyOptions,
                                                         aTypes);
}

NS_IMPL_ISUPPORTS(NotificationTelemetryService, nsIObserver)

NotificationTelemetryService::NotificationTelemetryService()
  : mDNDRecorded(false)
{}

NotificationTelemetryService::~NotificationTelemetryService()
{
  Unused << NS_WARN_IF(NS_FAILED(RemovePermissionChangeObserver()));
}

/* static */ already_AddRefed<NotificationTelemetryService>
NotificationTelemetryService::GetInstance()
{
  nsCOMPtr<nsISupports> telemetrySupports =
    do_GetService(NOTIFICATIONTELEMETRYSERVICE_CONTRACTID);
  if (!telemetrySupports) {
    return nullptr;
  }
  RefPtr<NotificationTelemetryService> telemetry =
    static_cast<NotificationTelemetryService*>(telemetrySupports.get());
  return telemetry.forget();
}

nsresult
NotificationTelemetryService::Init()
{
  nsresult rv = AddPermissionChangeObserver();
  NS_ENSURE_SUCCESS(rv, rv);

  RecordPermissions();

  return NS_OK;
}

nsresult
NotificationTelemetryService::RemovePermissionChangeObserver()
{
  nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
  if (!obs) {
    return NS_ERROR_OUT_OF_MEMORY;
  }
  return obs->RemoveObserver(this, "perm-changed");
}

nsresult
NotificationTelemetryService::AddPermissionChangeObserver()
{
  nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
  if (!obs) {
    return NS_ERROR_OUT_OF_MEMORY;
  }
  return obs->AddObserver(this, "perm-changed", false);
}

void
NotificationTelemetryService::RecordPermissions()
{
  if (!Telemetry::CanRecordBase() || !Telemetry::CanRecordExtended()) {
    return;
  }

  nsCOMPtr<nsIPermissionManager> permissionManager =
    services::GetPermissionManager();
  if (!permissionManager) {
    return;
  }

  nsCOMPtr<nsISimpleEnumerator> enumerator;
  nsresult rv = permissionManager->GetEnumerator(getter_AddRefs(enumerator));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return;
  }

  for (;;) {
    bool hasMoreElements;
    nsresult rv = enumerator->HasMoreElements(&hasMoreElements);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return;
    }
    if (!hasMoreElements) {
      break;
    }
    nsCOMPtr<nsISupports> supportsPermission;
    rv = enumerator->GetNext(getter_AddRefs(supportsPermission));
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return;
    }
    uint32_t capability;
    if (!GetNotificationPermission(supportsPermission, &capability)) {
      continue;
    }
    if (capability == nsIPermissionManager::DENY_ACTION) {
      Telemetry::Accumulate(Telemetry::WEB_NOTIFICATION_PERMISSIONS, 0);
    } else if (capability == nsIPermissionManager::ALLOW_ACTION) {
      Telemetry::Accumulate(Telemetry::WEB_NOTIFICATION_PERMISSIONS, 1);
    }
  }
}

bool
NotificationTelemetryService::GetNotificationPermission(nsISupports* aSupports,
                                                        uint32_t* aCapability)
{
  nsCOMPtr<nsIPermission> permission = do_QueryInterface(aSupports);
  if (!permission) {
    return false;
  }
  nsAutoCString type;
  permission->GetType(type);
  if (!type.Equals("desktop-notification")) {
    return false;
  }
  permission->GetCapability(aCapability);
  return true;
}

void
NotificationTelemetryService::RecordDNDSupported()
{
  if (mDNDRecorded) {
    return;
  }

  nsCOMPtr<nsIAlertsService> alertService =
    do_GetService(NS_ALERTSERVICE_CONTRACTID);
  if (!alertService) {
    return;
  }

  nsCOMPtr<nsIAlertsDoNotDisturb> alertServiceDND =
    do_QueryInterface(alertService);
  if (!alertServiceDND) {
    return;
  }

  mDNDRecorded = true;
  bool isEnabled;
  nsresult rv = alertServiceDND->GetManualDoNotDisturb(&isEnabled);
  if (NS_FAILED(rv)) {
    return;
  }

  Telemetry::Accumulate(
    Telemetry::ALERTS_SERVICE_DND_SUPPORTED_FLAG, true);
}

nsresult
NotificationTelemetryService::RecordSender(nsIPrincipal* aPrincipal)
{
  if (!Telemetry::CanRecordBase() || !Telemetry::CanRecordExtended() ||
      !nsAlertsUtils::IsActionablePrincipal(aPrincipal)) {
    return NS_OK;
  }
  nsAutoString origin;
  nsresult rv = Notification::GetOrigin(aPrincipal, origin);
  if (NS_FAILED(rv)) {
    return rv;
  }
  if (!mOrigins.Contains(origin)) {
    mOrigins.PutEntry(origin);
    Telemetry::Accumulate(Telemetry::WEB_NOTIFICATION_SENDERS, 1);
  }
  return NS_OK;
}

NS_IMETHODIMP
NotificationTelemetryService::Observe(nsISupports* aSubject,
                                      const char* aTopic,
                                      const char16_t* aData)
{
  uint32_t capability;
  if (strcmp("perm-changed", aTopic) ||
      !NS_strcmp(MOZ_UTF16("cleared"), aData) ||
      !GetNotificationPermission(aSubject, &capability)) {
    return NS_OK;
  }
  if (!NS_strcmp(MOZ_UTF16("deleted"), aData)) {
    if (capability == nsIPermissionManager::DENY_ACTION) {
      Telemetry::Accumulate(
        Telemetry::WEB_NOTIFICATION_PERMISSION_REMOVED, 0);
    } else if (capability == nsIPermissionManager::ALLOW_ACTION) {
      Telemetry::Accumulate(
        Telemetry::WEB_NOTIFICATION_PERMISSION_REMOVED, 1);
    }
  }
  return NS_OK;
}

// Observer that the alert service calls to do common tasks and/or dispatch to the
// specific observer for the context e.g. main thread, worker, or service worker.
class NotificationObserver final : public nsIObserver
{
public:
  nsCOMPtr<nsIObserver> mObserver;
  nsCOMPtr<nsIPrincipal> mPrincipal;
  bool mInPrivateBrowsing;
  NS_DECL_ISUPPORTS
  NS_DECL_NSIOBSERVER

  NotificationObserver(nsIObserver* aObserver, nsIPrincipal* aPrincipal,
                       bool aInPrivateBrowsing)
    : mObserver(aObserver), mPrincipal(aPrincipal),
      mInPrivateBrowsing(aInPrivateBrowsing)
  {
    AssertIsOnMainThread();
    MOZ_ASSERT(mObserver);
    MOZ_ASSERT(mPrincipal);
  }

protected:
  virtual ~NotificationObserver()
  {
    AssertIsOnMainThread();
  }

  nsresult AdjustPushQuota(const char* aTopic);
};

NS_IMPL_ISUPPORTS(NotificationObserver, nsIObserver)

class MainThreadNotificationObserver : public nsIObserver
{
public:
  UniquePtr<NotificationRef> mNotificationRef;
  NS_DECL_ISUPPORTS
  NS_DECL_NSIOBSERVER

  explicit MainThreadNotificationObserver(UniquePtr<NotificationRef> aRef)
    : mNotificationRef(Move(aRef))
  {
    AssertIsOnMainThread();
  }

protected:
  virtual ~MainThreadNotificationObserver()
  {
    AssertIsOnMainThread();
  }
};

NS_IMPL_ISUPPORTS(MainThreadNotificationObserver, nsIObserver)

NS_IMETHODIMP
NotificationTask::Run()
{
  AssertIsOnMainThread();

  // Get a pointer to notification before the notification takes ownership of
  // the ref (it owns itself temporarily, with ShowInternal() and
  // CloseInternal() passing on the ownership appropriately.)
  Notification* notif = mNotificationRef->GetNotification();
  notif->mTempRef.swap(mNotificationRef);
  if (mAction == eShow) {
    notif->ShowInternal();
  } else if (mAction == eClose) {
    notif->CloseInternal();
  } else {
    MOZ_CRASH("Invalid action");
  }

  MOZ_ASSERT(!mNotificationRef);
  return NS_OK;
}

// static
bool
Notification::PrefEnabled(JSContext* aCx, JSObject* aObj)
{
  if (NS_IsMainThread()) {
    return Preferences::GetBool("dom.webnotifications.enabled", false);
  }

  WorkerPrivate* workerPrivate = GetWorkerPrivateFromContext(aCx);
  if (!workerPrivate) {
    return false;
  }

  if (workerPrivate->IsServiceWorker()) {
    return workerPrivate->DOMServiceWorkerNotificationEnabled();
  }

  return workerPrivate->DOMWorkerNotificationEnabled();
}

// static
bool
Notification::IsGetEnabled(JSContext* aCx, JSObject* aObj)
{
  return NS_IsMainThread();
}

Notification::Notification(nsIGlobalObject* aGlobal, const nsAString& aID,
                           const nsAString& aTitle, const nsAString& aBody,
                           NotificationDirection aDir, const nsAString& aLang,
                           const nsAString& aTag, const nsAString& aIconUrl,
                           const NotificationBehavior& aBehavior)
  : DOMEventTargetHelper(),
    mWorkerPrivate(nullptr), mObserver(nullptr),
    mID(aID), mTitle(aTitle), mBody(aBody), mDir(aDir), mLang(aLang),
    mTag(aTag), mIconUrl(aIconUrl), mBehavior(aBehavior), mData(JS::NullValue()),
    mIsClosed(false), mIsStored(false), mTaskCount(0)
{
  if (NS_IsMainThread()) {
    // We can only call this on the main thread because
    // Event::SetEventType() called down the call chain when dispatching events
    // using DOMEventTargetHelper::DispatchTrustedEvent() will assume the event
    // is a main thread event if it has a valid owner. It will then attempt to
    // fetch the atom for the event name which asserts main thread only.
    BindToOwner(aGlobal);
  } else {
    mWorkerPrivate = GetCurrentThreadWorkerPrivate();
    MOZ_ASSERT(mWorkerPrivate);
  }
}

nsresult
Notification::Init()
{
  if (!mWorkerPrivate) {
    nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
    NS_ENSURE_TRUE(obs, NS_ERROR_FAILURE);

    nsresult rv = obs->AddObserver(this, DOM_WINDOW_DESTROYED_TOPIC, true);
    NS_ENSURE_SUCCESS(rv, rv);

    rv = obs->AddObserver(this, DOM_WINDOW_FROZEN_TOPIC, true);
    NS_ENSURE_SUCCESS(rv, rv);
  }

  return NS_OK;
}

void
Notification::SetAlertName()
{
  AssertIsOnMainThread();
  if (!mAlertName.IsEmpty()) {
    return;
  }

  nsAutoString alertName;
  nsresult rv = GetOrigin(GetPrincipal(), alertName);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return;
  }

  // Get the notification name that is unique per origin + tag/ID.
  // The name of the alert is of the form origin#tag/ID.
  alertName.Append('#');
  if (!mTag.IsEmpty()) {
    alertName.AppendLiteral("tag:");
    alertName.Append(mTag);
  } else {
    alertName.AppendLiteral("notag:");
    alertName.Append(mID);
  }

  mAlertName = alertName;
}

// May be called on any thread.
// static
already_AddRefed<Notification>
Notification::Constructor(const GlobalObject& aGlobal,
                          const nsAString& aTitle,
                          const NotificationOptions& aOptions,
                          ErrorResult& aRv)
{
  // FIXME(nsm): If the sticky flag is set, throw an error.
  ServiceWorkerGlobalScope* scope = nullptr;
  UNWRAP_WORKER_OBJECT(ServiceWorkerGlobalScope, aGlobal.Get(), scope);
  if (scope) {
    aRv.ThrowTypeError<MSG_NOTIFICATION_NO_CONSTRUCTOR_IN_SERVICEWORKER>();
    return nullptr;
  }

  nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
  RefPtr<Notification> notification =
    CreateAndShow(aGlobal.Context(), global, aTitle, aOptions,
                  EmptyString(), aRv);
  if (NS_WARN_IF(aRv.Failed())) {
    return nullptr;
  }

  // This is be ok since we are on the worker thread where this function will
  // run to completion before the Notification has a chance to go away.
  return notification.forget();
}

// static
already_AddRefed<Notification>
Notification::ConstructFromFields(
    nsIGlobalObject* aGlobal,
    const nsAString& aID,
    const nsAString& aTitle,
    const nsAString& aDir,
    const nsAString& aLang,
    const nsAString& aBody,
    const nsAString& aTag,
    const nsAString& aIcon,
    const nsAString& aData,
    const nsAString& aServiceWorkerRegistrationID,
    ErrorResult& aRv)
{
  MOZ_ASSERT(aGlobal);

  RootedDictionary<NotificationOptions> options(nsContentUtils::RootingCxForThread());
  options.mDir = Notification::StringToDirection(nsString(aDir));
  options.mLang = aLang;
  options.mBody = aBody;
  options.mTag = aTag;
  options.mIcon = aIcon;
  RefPtr<Notification> notification = CreateInternal(aGlobal, aID, aTitle,
                                                     options);

  notification->InitFromBase64(aData, aRv);
  if (NS_WARN_IF(aRv.Failed())) {
    return nullptr;
  }

  notification->SetScope(aServiceWorkerRegistrationID);

  return notification.forget();
}

nsresult
Notification::PersistNotification()
{
  AssertIsOnMainThread();
  nsresult rv;
  nsCOMPtr<nsINotificationStorage> notificationStorage =
    do_GetService(NS_NOTIFICATION_STORAGE_CONTRACTID, &rv);
  if (NS_FAILED(rv)) {
    return rv;
  }

  nsString origin;
  rv = GetOrigin(GetPrincipal(), origin);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  nsString id;
  GetID(id);

  nsString alertName;
  GetAlertName(alertName);

  nsAutoString behavior;
  if (!mBehavior.ToJSON(behavior)) {
    return NS_ERROR_FAILURE;
  }

  rv = notificationStorage->Put(origin,
                                id,
                                mTitle,
                                DirectionToString(mDir),
                                mLang,
                                mBody,
                                mTag,
                                mIconUrl,
                                alertName,
                                mDataAsBase64,
                                behavior,
                                mScope);

  if (NS_FAILED(rv)) {
    return rv;
  }

  SetStoredState(true);
  return NS_OK;
}

void
Notification::UnpersistNotification()
{
  AssertIsOnMainThread();
  if (IsStored()) {
    nsCOMPtr<nsINotificationStorage> notificationStorage =
      do_GetService(NS_NOTIFICATION_STORAGE_CONTRACTID);
    if (notificationStorage) {
      nsString origin;
      nsresult rv = GetOrigin(GetPrincipal(), origin);
      if (NS_SUCCEEDED(rv)) {
        notificationStorage->Delete(origin, mID);
      }
    }
    SetStoredState(false);
  }
}

already_AddRefed<Notification>
Notification::CreateInternal(nsIGlobalObject* aGlobal,
                             const nsAString& aID,
                             const nsAString& aTitle,
                             const NotificationOptions& aOptions)
{
  nsresult rv;
  nsString id;
  if (!aID.IsEmpty()) {
    id = aID;
  } else {
    nsCOMPtr<nsIUUIDGenerator> uuidgen =
      do_GetService("@mozilla.org/uuid-generator;1");
    NS_ENSURE_TRUE(uuidgen, nullptr);
    nsID uuid;
    rv = uuidgen->GenerateUUIDInPlace(&uuid);
    NS_ENSURE_SUCCESS(rv, nullptr);

    char buffer[NSID_LENGTH];
    uuid.ToProvidedString(buffer);
    NS_ConvertASCIItoUTF16 convertedID(buffer);
    id = convertedID;
  }

  RefPtr<Notification> notification = new Notification(aGlobal, id, aTitle,
                                                         aOptions.mBody,
                                                         aOptions.mDir,
                                                         aOptions.mLang,
                                                         aOptions.mTag,
                                                         aOptions.mIcon,
                                                         aOptions.mMozbehavior);
  rv = notification->Init();
  NS_ENSURE_SUCCESS(rv, nullptr);
  return notification.forget();
}

Notification::~Notification()
{
  mData.setUndefined();
  mozilla::DropJSObjects(this);
  AssertIsOnTargetThread();
  MOZ_ASSERT(!mFeature);
  MOZ_ASSERT(!mTempRef);
}

NS_IMPL_CYCLE_COLLECTION_CLASS(Notification)
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(Notification, DOMEventTargetHelper)
  tmp->mData.setUndefined();
NS_IMPL_CYCLE_COLLECTION_UNLINK_END

NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(Notification, DOMEventTargetHelper)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END

NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(Notification, DOMEventTargetHelper)
  NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mData)
NS_IMPL_CYCLE_COLLECTION_TRACE_END

NS_IMPL_ADDREF_INHERITED(Notification, DOMEventTargetHelper)
NS_IMPL_RELEASE_INHERITED(Notification, DOMEventTargetHelper)

NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(Notification)
  NS_INTERFACE_MAP_ENTRY(nsIObserver)
  NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)

nsIPrincipal*
Notification::GetPrincipal()
{
  AssertIsOnMainThread();
  if (mWorkerPrivate) {
    return mWorkerPrivate->GetPrincipal();
  } else {
    nsCOMPtr<nsIScriptObjectPrincipal> sop = do_QueryInterface(GetOwner());
    NS_ENSURE_TRUE(sop, nullptr);
    return sop->GetPrincipal();
  }
}

class WorkerNotificationObserver final : public MainThreadNotificationObserver
{
public:
  NS_DECL_ISUPPORTS_INHERITED
  NS_DECL_NSIOBSERVER

  explicit WorkerNotificationObserver(UniquePtr<NotificationRef> aRef)
    : MainThreadNotificationObserver(Move(aRef))
  {
    AssertIsOnMainThread();
    MOZ_ASSERT(mNotificationRef->GetNotification()->mWorkerPrivate);
  }

  void
  ForgetNotification()
  {
    AssertIsOnMainThread();
    mNotificationRef->Forget();
  }

protected:
  virtual ~WorkerNotificationObserver()
  {
    AssertIsOnMainThread();

    MOZ_ASSERT(mNotificationRef);
    Notification* notification = mNotificationRef->GetNotification();
    if (notification) {
      notification->mObserver = nullptr;
    }
  }
};

NS_IMPL_ISUPPORTS_INHERITED0(WorkerNotificationObserver, MainThreadNotificationObserver)

class ServiceWorkerNotificationObserver final : public nsIObserver
{
public:
  NS_DECL_ISUPPORTS
  NS_DECL_NSIOBSERVER

  ServiceWorkerNotificationObserver(const nsAString& aScope,
                                    nsIPrincipal* aPrincipal,
                                    const nsAString& aID,
                                    const nsAString& aTitle,
                                    const nsAString& aDir,
                                    const nsAString& aLang,
                                    const nsAString& aBody,
                                    const nsAString& aTag,
                                    const nsAString& aIcon,
                                    const nsAString& aData,
                                    const nsAString& aBehavior)
    : mScope(aScope), mID(aID), mPrincipal(aPrincipal), mTitle(aTitle)
    , mDir(aDir), mLang(aLang), mBody(aBody), mTag(aTag), mIcon(aIcon)
    , mData(aData), mBehavior(aBehavior)
  {
    AssertIsOnMainThread();
    MOZ_ASSERT(aPrincipal);
  }

private:
  ~ServiceWorkerNotificationObserver()
  {}

  const nsString mScope;
  const nsString mID;
  nsCOMPtr<nsIPrincipal> mPrincipal;
  const nsString mTitle;
  const nsString mDir;
  const nsString mLang;
  const nsString mBody;
  const nsString mTag;
  const nsString mIcon;
  const nsString mData;
  const nsString mBehavior;
};

NS_IMPL_ISUPPORTS(ServiceWorkerNotificationObserver, nsIObserver)

// For ServiceWorkers.
bool
Notification::DispatchNotificationClickEvent()
{
  MOZ_ASSERT(mWorkerPrivate);
  MOZ_ASSERT(mWorkerPrivate->IsServiceWorker());
  mWorkerPrivate->AssertIsOnWorkerThread();

  NotificationEventInit options;
  options.mNotification = this;

  ErrorResult result;
  RefPtr<EventTarget> target = mWorkerPrivate->GlobalScope();
  RefPtr<NotificationEvent> event =
    NotificationEvent::Constructor(target,
                                   NS_LITERAL_STRING("notificationclick"),
                                   options,
                                   result);
  if (NS_WARN_IF(result.Failed())) {
    return false;
  }

  event->SetTrusted(true);
  WantsPopupControlCheck popupControlCheck(event);
  target->DispatchDOMEvent(nullptr, event, nullptr, nullptr);
  // We always return false since in case of dispatching on the serviceworker,
  // there is no well defined window to focus. The script may use the
  // Client.focus() API if it wishes.
  return false;
}

bool
Notification::DispatchClickEvent()
{
  AssertIsOnTargetThread();
  RefPtr<Event> event = NS_NewDOMEvent(this, nullptr, nullptr);
  event->InitEvent(NS_LITERAL_STRING("click"), false, true);
  event->SetTrusted(true);
  WantsPopupControlCheck popupControlCheck(event);
  bool doDefaultAction = true;
  DispatchEvent(event, &doDefaultAction);
  return doDefaultAction;
}

// Overrides dispatch and run handlers so we can directly dispatch from main
// thread to child workers.
class NotificationClickWorkerRunnable final : public NotificationWorkerRunnable
{
  Notification* mNotification;
  // Optional window that gets focused if click event is not
  // preventDefault()ed.
  nsMainThreadPtrHandle<nsPIDOMWindowInner> mWindow;
public:
  NotificationClickWorkerRunnable(Notification* aNotification,
                                  const nsMainThreadPtrHandle<nsPIDOMWindowInner>& aWindow)
    : NotificationWorkerRunnable(aNotification->mWorkerPrivate)
    , mNotification(aNotification)
    , mWindow(aWindow)
  {
    MOZ_ASSERT_IF(mWorkerPrivate->IsServiceWorker(), !mWindow);
  }

  void
  WorkerRunInternal(WorkerPrivate* aWorkerPrivate) override
  {
    bool doDefaultAction = mNotification->DispatchClickEvent();
    MOZ_ASSERT_IF(mWorkerPrivate->IsServiceWorker(), !doDefaultAction);
    if (doDefaultAction) {
      RefPtr<FocusWindowRunnable> r = new FocusWindowRunnable(mWindow);
      NS_DispatchToMainThread(r);
    }
  }
};

NS_IMETHODIMP
NotificationObserver::Observe(nsISupports* aSubject, const char* aTopic,
                              const char16_t* aData)
{
  AssertIsOnMainThread();

  if (!strcmp("alertdisablecallback", aTopic)) {
    Telemetry::Accumulate(Telemetry::WEB_NOTIFICATION_MENU, 1);
    if (XRE_IsParentProcess()) {
      return Notification::RemovePermission(mPrincipal);
    }
    // Permissions can't be removed from the content process. Send a message
    // to the parent; `ContentParent::RecvDisableNotifications` will call
    // `RemovePermission`.
    ContentChild::GetSingleton()->SendDisableNotifications(
      IPC::Principal(mPrincipal));
    return NS_OK;
  } else if (!strcmp("alertclickcallback", aTopic)) {
    Telemetry::Accumulate(Telemetry::WEB_NOTIFICATION_CLICKED, 1);
  } else if (!strcmp("alertsettingscallback", aTopic)) {
    Telemetry::Accumulate(Telemetry::WEB_NOTIFICATION_MENU, 2);
    if (XRE_IsParentProcess()) {
      return Notification::OpenSettings(mPrincipal);
    }
    // `ContentParent::RecvOpenNotificationSettings` notifies observers in the
    // parent process.
    ContentChild::GetSingleton()->SendOpenNotificationSettings(
      IPC::Principal(mPrincipal));
    return NS_OK;
  } else if (!strcmp("alertshow", aTopic) ||
             !strcmp("alertfinished", aTopic)) {
    RefPtr<NotificationTelemetryService> telemetry =
      NotificationTelemetryService::GetInstance();
    if (telemetry) {
      // Record whether "do not disturb" is supported after the first
      // notification, to account for falling back to XUL alerts.
      telemetry->RecordDNDSupported();
      if (!mInPrivateBrowsing) {
        // Ignore senders in private windows.
        Unused << NS_WARN_IF(NS_FAILED(telemetry->RecordSender(mPrincipal)));
      }
    }
    Unused << NS_WARN_IF(NS_FAILED(AdjustPushQuota(aTopic)));

    if (!strcmp("alertshow", aTopic)) {
      // Record notifications actually shown (e.g. don't count if DND is on).
      Telemetry::Accumulate(Telemetry::WEB_NOTIFICATION_SHOWN, 1);
    }
  }

  return mObserver->Observe(aSubject, aTopic, aData);
}

nsresult
NotificationObserver::AdjustPushQuota(const char* aTopic)
{
#ifdef MOZ_SIMPLEPUSH
  return NS_ERROR_NOT_IMPLEMENTED;
#else
  nsCOMPtr<nsIPushQuotaManager> pushQuotaManager =
    do_GetService("@mozilla.org/push/Service;1");
  if (!pushQuotaManager) {
    return NS_ERROR_FAILURE;
  }

  nsAutoCString origin;
  nsresult rv = mPrincipal->GetOrigin(origin);
  if (NS_FAILED(rv)) {
    return rv;
  }

  if (!strcmp("alertshow", aTopic)) {
    return pushQuotaManager->NotificationForOriginShown(origin.get());
  }
  return pushQuotaManager->NotificationForOriginClosed(origin.get());
#endif
}

NS_IMETHODIMP
MainThreadNotificationObserver::Observe(nsISupports* aSubject, const char* aTopic,
                                        const char16_t* aData)
{
  AssertIsOnMainThread();
  MOZ_ASSERT(mNotificationRef);
  Notification* notification = mNotificationRef->GetNotification();
  MOZ_ASSERT(notification);
  if (!strcmp("alertclickcallback", aTopic)) {
    nsCOMPtr<nsPIDOMWindowInner> window = notification->GetOwner();
    if (NS_WARN_IF(!window || !window->IsCurrentInnerWindow())) {
      // Window has been closed, this observer is not valid anymore
      return NS_ERROR_FAILURE;
    }

    bool doDefaultAction = notification->DispatchClickEvent();
    if (doDefaultAction) {
      nsIDocument* doc = window ? window->GetExtantDoc() : nullptr;
      if (doc) {
        // Browser UI may use DOMWebNotificationClicked to focus the tab
        // from which the event was dispatched.
        nsContentUtils::DispatchChromeEvent(doc, window->GetOuterWindow(),
                                            NS_LITERAL_STRING("DOMWebNotificationClicked"),
                                            true, true);
      }
    }
  } else if (!strcmp("alertfinished", aTopic)) {
    // In b2g-desktop, if the app is closed, closing a notification still
    // triggers the observer which might be alive even though the owner window
    // was closed. Keeping this until we remove the close event (Bug 1139363)
    // from implementation.
    nsCOMPtr<nsPIDOMWindowInner> window = notification->GetOwner();
    if (NS_WARN_IF(!window || !window->IsCurrentInnerWindow())) {
      return NS_ERROR_FAILURE;
    }

    notification->UnpersistNotification();
    notification->mIsClosed = true;
    notification->DispatchTrustedEvent(NS_LITERAL_STRING("close"));
  } else if (!strcmp("alertshow", aTopic)) {
    notification->DispatchTrustedEvent(NS_LITERAL_STRING("show"));
  }
  return NS_OK;
}

NS_IMETHODIMP
WorkerNotificationObserver::Observe(nsISupports* aSubject, const char* aTopic,
                                    const char16_t* aData)
{
  AssertIsOnMainThread();
  MOZ_ASSERT(mNotificationRef);
  // For an explanation of why it is OK to pass this rawptr to the event
  // runnables, see the Notification class comment.
  Notification* notification = mNotificationRef->GetNotification();
  // We can't assert notification here since the feature could've unset it.
  if (NS_WARN_IF(!notification)) {
    return NS_ERROR_FAILURE;
  }

  MOZ_ASSERT(notification->mWorkerPrivate);

  RefPtr<WorkerRunnable> r;
  if (!strcmp("alertclickcallback", aTopic)) {
    nsPIDOMWindowInner* window = nullptr;
    if (!notification->mWorkerPrivate->IsServiceWorker()) {
      WorkerPrivate* top = notification->mWorkerPrivate;
      while (top->GetParent()) {
        top = top->GetParent();
      }

      window = top->GetWindow();
      if (NS_WARN_IF(!window || !window->IsCurrentInnerWindow())) {
        // Window has been closed, this observer is not valid anymore
        return NS_ERROR_FAILURE;
      }
    }

    // Instead of bothering with adding features and other worker lifecycle
    // management, we simply hold strongrefs to the window and document.
    nsMainThreadPtrHandle<nsPIDOMWindowInner> windowHandle(
      new nsMainThreadPtrHolder<nsPIDOMWindowInner>(window));

    r = new NotificationClickWorkerRunnable(notification, windowHandle);
  } else if (!strcmp("alertfinished", aTopic)) {
    notification->UnpersistNotification();
    notification->mIsClosed = true;
    r = new NotificationEventWorkerRunnable(notification,
                                            NS_LITERAL_STRING("close"));
  } else if (!strcmp("alertshow", aTopic)) {
    r = new NotificationEventWorkerRunnable(notification,
                                            NS_LITERAL_STRING("show"));
  }

  MOZ_ASSERT(r);
  if (!r->Dispatch()) {
    NS_WARNING("Could not dispatch event to worker notification");
  }
  return NS_OK;
}

NS_IMETHODIMP
ServiceWorkerNotificationObserver::Observe(nsISupports* aSubject,
                                           const char* aTopic,
                                           const char16_t* aData)
{
  AssertIsOnMainThread();

  nsAutoCString originSuffix;
  nsresult rv = mPrincipal->GetOriginSuffix(originSuffix);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  nsCOMPtr<nsIServiceWorkerManager> swm =
    mozilla::services::GetServiceWorkerManager();
  if (NS_WARN_IF(!swm)) {
    return NS_ERROR_FAILURE;
  }

  if (!strcmp("alertclickcallback", aTopic)) {
    rv = swm->SendNotificationClickEvent(originSuffix,
                                         NS_ConvertUTF16toUTF8(mScope),
                                         mID,
                                         mTitle,
                                         mDir,
                                         mLang,
                                         mBody,
                                         mTag,
                                         mIcon,
                                         mData,
                                         mBehavior);
    Unused << NS_WARN_IF(NS_FAILED(rv));
    return NS_OK;
  }

  if (!strcmp("alertfinished", aTopic)) {
    nsString origin;
    nsresult rv = Notification::GetOrigin(mPrincipal, origin);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return rv;
    }

    // Remove closed or dismissed persistent notifications.
    nsCOMPtr<nsINotificationStorage> notificationStorage =
      do_GetService(NS_NOTIFICATION_STORAGE_CONTRACTID);
    if (notificationStorage) {
      notificationStorage->Delete(origin, mID);
    }

    rv = swm->SendNotificationCloseEvent(originSuffix,
                                         NS_ConvertUTF16toUTF8(mScope),
                                         mID,
                                         mTitle,
                                         mDir,
                                         mLang,
                                         mBody,
                                         mTag,
                                         mIcon,
                                         mData,
                                         mBehavior);
    Unused << NS_WARN_IF(NS_FAILED(rv));
    return NS_OK;
  }

  return NS_OK;
}

bool
Notification::IsInPrivateBrowsing()
{
  AssertIsOnMainThread();

  nsIDocument* doc = nullptr;

  if (mWorkerPrivate) {
    doc = mWorkerPrivate->GetDocument();
  } else if (GetOwner()) {
    doc = GetOwner()->GetExtantDoc();
  }

  if (doc) {
    nsCOMPtr<nsILoadContext> loadContext = doc->GetLoadContext();
    return loadContext && loadContext->UsePrivateBrowsing();
  }

  if (mWorkerPrivate) {
    // Not all workers may have a document, but with Bug 1107516 fixed, they
    // should all have a loadcontext.
    nsCOMPtr<nsILoadGroup> loadGroup = mWorkerPrivate->GetLoadGroup();
    nsCOMPtr<nsILoadContext> loadContext;
    NS_QueryNotificationCallbacks(nullptr, loadGroup, NS_GET_IID(nsILoadContext),
                                  getter_AddRefs(loadContext));
    return loadContext && loadContext->UsePrivateBrowsing();
  }

  //XXXnsm Should this default to true?
  return false;
}

void
Notification::ShowInternal()
{
  AssertIsOnMainThread();
  MOZ_ASSERT(mTempRef, "Notification should take ownership of itself before"
                       "calling ShowInternal!");
  // A notification can only have one observer and one call to ShowInternal.
  MOZ_ASSERT(!mObserver);

  // Transfer ownership to local scope so we can either release it at the end
  // of this function or transfer it to the observer.
  UniquePtr<NotificationRef> ownership;
  mozilla::Swap(ownership, mTempRef);
  MOZ_ASSERT(ownership->GetNotification() == this);

  nsresult rv = PersistNotification();
  if (NS_FAILED(rv)) {
    NS_WARNING("Could not persist Notification");
  }

  nsCOMPtr<nsIAlertsService> alertService =
    do_GetService(NS_ALERTSERVICE_CONTRACTID);

  ErrorResult result;
  NotificationPermission permission = NotificationPermission::Denied;
  if (mWorkerPrivate) {
    permission = GetPermissionInternal(mWorkerPrivate->GetPrincipal(), result);
  } else {
    permission = GetPermissionInternal(GetOwner(), result);
  }
  // We rely on GetPermissionInternal returning Denied on all failure codepaths.
  MOZ_ASSERT_IF(result.Failed(), permission == NotificationPermission::Denied);
  result.SuppressException();
  if (permission != NotificationPermission::Granted || !alertService) {
    if (mWorkerPrivate) {
      RefPtr<NotificationEventWorkerRunnable> r =
        new NotificationEventWorkerRunnable(this,
                                            NS_LITERAL_STRING("error"));
      if (!r->Dispatch()) {
        NS_WARNING("Could not dispatch event to worker notification");
      }
    } else {
      DispatchTrustedEvent(NS_LITERAL_STRING("error"));
    }
    return;
  }

  nsAutoString iconUrl;
  nsAutoString soundUrl;
  ResolveIconAndSoundURL(iconUrl, soundUrl);

  nsCOMPtr<nsIObserver> observer;
  if (mScope.IsEmpty()) {
    // Ownership passed to observer.
    if (mWorkerPrivate) {
      // Scope better be set on ServiceWorker initiated requests.
      MOZ_ASSERT(!mWorkerPrivate->IsServiceWorker());
      // Keep a pointer so that the feature can tell the observer not to release
      // the notification.
      mObserver = new WorkerNotificationObserver(Move(ownership));
      observer = mObserver;
    } else {
      observer = new MainThreadNotificationObserver(Move(ownership));
    }
  } else {
    // This observer does not care about the Notification. It will be released
    // at the end of this function.
    //
    // The observer is wholly owned by the NotificationObserver passed to the alert service.
    nsAutoString behavior;
    if (NS_WARN_IF(!mBehavior.ToJSON(behavior))) {
      behavior.Truncate();
    }
    observer = new ServiceWorkerNotificationObserver(mScope,
                                                     GetPrincipal(),
                                                     mID,
                                                     mTitle,
                                                     DirectionToString(mDir),
                                                     mLang,
                                                     mBody,
                                                     mTag,
                                                     iconUrl,
                                                     mDataAsBase64,
                                                     behavior);
  }
  MOZ_ASSERT(observer);
  nsCOMPtr<nsIObserver> alertObserver = new NotificationObserver(observer,
                                                                 GetPrincipal(),
                                                                 IsInPrivateBrowsing());


#ifdef MOZ_B2G
  nsCOMPtr<nsIAppNotificationService> appNotifier =
    do_GetService("@mozilla.org/system-alerts-service;1");
  if (appNotifier) {
    uint32_t appId = nsIScriptSecurityManager::UNKNOWN_APP_ID;
    if (mWorkerPrivate) {
      appId = mWorkerPrivate->GetPrincipal()->GetAppId();
    } else {
      nsCOMPtr<nsPIDOMWindowInner> window = GetOwner();
      if (window) {
        appId = window->GetDoc()->NodePrincipal()->GetAppId();
      }
    }

    if (appId != nsIScriptSecurityManager::UNKNOWN_APP_ID) {
      nsCOMPtr<nsIAppsService> appsService = do_GetService("@mozilla.org/AppsService;1");
      nsString manifestUrl = EmptyString();
      nsresult rv = appsService->GetManifestURLByLocalId(appId, manifestUrl);
      if (NS_SUCCEEDED(rv)) {
        mozilla::AutoSafeJSContext cx;
        JS::Rooted<JS::Value> val(cx);
        AppNotificationServiceOptions ops;
        ops.mTextClickable = true;
        ops.mManifestURL = manifestUrl;
        GetAlertName(ops.mId);
        ops.mDbId = mID;
        ops.mDir = DirectionToString(mDir);
        ops.mLang = mLang;
        ops.mTag = mTag;
        ops.mData = mDataAsBase64;
        ops.mMozbehavior = mBehavior;
        ops.mMozbehavior.mSoundFile = soundUrl;

        if (!ToJSValue(cx, ops, &val)) {
          NS_WARNING("Converting dict to object failed!");
          return;
        }

        appNotifier->ShowAppNotification(iconUrl, mTitle, mBody,
                                         alertObserver, val);
        return;
      }
    }
  }
#endif

  // In the case of IPC, the parent process uses the cookie to map to
  // nsIObserver. Thus the cookie must be unique to differentiate observers.
  nsString uniqueCookie = NS_LITERAL_STRING("notification:");
  uniqueCookie.AppendInt(sCount++);
  bool inPrivateBrowsing = IsInPrivateBrowsing();

  nsAutoString alertName;
  GetAlertName(alertName);
  nsCOMPtr<nsIAlertNotification> alert =
    do_CreateInstance(ALERT_NOTIFICATION_CONTRACTID);
  NS_ENSURE_TRUE_VOID(alert);
  rv = alert->Init(alertName, iconUrl, mTitle, mBody,
                   true,
                   uniqueCookie,
                   DirectionToString(mDir),
                   mLang,
                   mDataAsBase64,
                   GetPrincipal(),
                   inPrivateBrowsing);
  NS_ENSURE_SUCCESS_VOID(rv);
  alertService->ShowAlert(alert, alertObserver);
}

/* static */ bool
Notification::RequestPermissionEnabledForScope(JSContext* aCx, JSObject* /* unused */)
{
  // requestPermission() is not allowed on workers. The calling page should ask
  // for permission on the worker's behalf. This is to prevent 'which window
  // should show the browser pop-up'. See discussion:
  // http://lists.whatwg.org/pipermail/whatwg-whatwg.org/2013-October/041272.html
  return NS_IsMainThread();
}

already_AddRefed<Promise>
Notification::RequestPermission(const GlobalObject& aGlobal,
                                const Optional<OwningNonNull<NotificationPermissionCallback> >& aCallback,
                                ErrorResult& aRv)
{
  // Get principal from global to make permission request for notifications.
  nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(aGlobal.GetAsSupports());
  nsCOMPtr<nsIScriptObjectPrincipal> sop = do_QueryInterface(aGlobal.GetAsSupports());
  if (!sop) {
    aRv.Throw(NS_ERROR_UNEXPECTED);
    return nullptr;
  }
  nsCOMPtr<nsIPrincipal> principal = sop->GetPrincipal();

  nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(window);
  RefPtr<Promise> promise = Promise::Create(global, aRv);
  if (aRv.Failed()) {
    return nullptr;
  }
  NotificationPermissionCallback* permissionCallback = nullptr;
  if (aCallback.WasPassed()) {
    permissionCallback = &aCallback.Value();
  }
  nsCOMPtr<nsIRunnable> request =
    new NotificationPermissionRequest(principal, window, promise, permissionCallback);

  NS_DispatchToMainThread(request);
  return promise.forget();
}

// static
NotificationPermission
Notification::GetPermission(const GlobalObject& aGlobal, ErrorResult& aRv)
{
  nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
  return GetPermission(global, aRv);
}

// static
NotificationPermission
Notification::GetPermission(nsIGlobalObject* aGlobal, ErrorResult& aRv)
{
  if (NS_IsMainThread()) {
    return GetPermissionInternal(aGlobal, aRv);
  } else {
    WorkerPrivate* worker = GetCurrentThreadWorkerPrivate();
    MOZ_ASSERT(worker);
    RefPtr<GetPermissionRunnable> r =
      new GetPermissionRunnable(worker);
    r->Dispatch(aRv);
    if (aRv.Failed()) {
      return NotificationPermission::Denied;
    }

    return r->GetPermission();
  }
}

/* static */ NotificationPermission
Notification::GetPermissionInternal(nsISupports* aGlobal, ErrorResult& aRv)
{
  // Get principal from global to check permission for notifications.
  nsCOMPtr<nsIScriptObjectPrincipal> sop = do_QueryInterface(aGlobal);
  if (!sop) {
    aRv.Throw(NS_ERROR_UNEXPECTED);
    return NotificationPermission::Denied;
  }

  nsCOMPtr<nsIPrincipal> principal = sop->GetPrincipal();
  return GetPermissionInternal(principal, aRv);
}

/* static */ NotificationPermission
Notification::GetPermissionInternal(nsIPrincipal* aPrincipal,
                                    ErrorResult& aRv)
{
  AssertIsOnMainThread();
  MOZ_ASSERT(aPrincipal);

  if (nsContentUtils::IsSystemPrincipal(aPrincipal)) {
    return NotificationPermission::Granted;
  } else {
    // Allow files to show notifications by default.
    nsCOMPtr<nsIURI> uri;
    aPrincipal->GetURI(getter_AddRefs(uri));
    if (uri) {
      bool isFile;
      uri->SchemeIs("file", &isFile);
      if (isFile) {
        return NotificationPermission::Granted;
      }
    }
  }

  // We also allow notifications is they are pref'ed on.
  if (Preferences::GetBool("notification.prompt.testing", false)) {
    if (Preferences::GetBool("notification.prompt.testing.allow", true)) {
      return NotificationPermission::Granted;
    } else {
      return NotificationPermission::Denied;
    }
  }

  return TestPermission(aPrincipal);
}

/* static */ NotificationPermission
Notification::TestPermission(nsIPrincipal* aPrincipal)
{
  AssertIsOnMainThread();

  uint32_t permission = nsIPermissionManager::UNKNOWN_ACTION;

  nsCOMPtr<nsIPermissionManager> permissionManager =
    services::GetPermissionManager();
  if (!permissionManager) {
    return NotificationPermission::Default;
  }

  permissionManager->TestExactPermissionFromPrincipal(aPrincipal,
                                                      "desktop-notification",
                                                      &permission);

  // Convert the result to one of the enum types.
  switch (permission) {
  case nsIPermissionManager::ALLOW_ACTION:
    return NotificationPermission::Granted;
  case nsIPermissionManager::DENY_ACTION:
    return NotificationPermission::Denied;
  default:
    return NotificationPermission::Default;
  }
}

nsresult
Notification::ResolveIconAndSoundURL(nsString& iconUrl, nsString& soundUrl)
{
  AssertIsOnMainThread();
  nsresult rv = NS_OK;

  nsCOMPtr<nsIURI> baseUri;

  // XXXnsm If I understand correctly, the character encoding for resolving
  // URIs in new specs is dictated by the URL spec, which states that unless
  // the URL parser is passed an override encoding, the charset to be used is
  // UTF-8. The new Notification icon/sound specification just says to use the
  // Fetch API, where the Request constructor defers to URL parsing specifying
  // the API base URL and no override encoding. So we've to use UTF-8 on
  // workers, but for backwards compat keeping it document charset on main
  // thread.
  const char* charset = "UTF-8";

  if (mWorkerPrivate) {
    baseUri = mWorkerPrivate->GetBaseURI();
  } else {
    nsIDocument* doc = GetOwner() ? GetOwner()->GetExtantDoc() : nullptr;
    if (doc) {
      baseUri = doc->GetBaseURI();
      charset = doc->GetDocumentCharacterSet().get();
    } else {
      NS_WARNING("No document found for main thread notification!");
      return NS_ERROR_FAILURE;
    }
  }

  if (baseUri) {
    if (mIconUrl.Length() > 0) {
      nsCOMPtr<nsIURI> srcUri;
      rv = NS_NewURI(getter_AddRefs(srcUri), mIconUrl, charset, baseUri);
      if (NS_SUCCEEDED(rv)) {
        nsAutoCString src;
        srcUri->GetSpec(src);
        iconUrl = NS_ConvertUTF8toUTF16(src);
      }
    }
    if (mBehavior.mSoundFile.Length() > 0) {
      nsCOMPtr<nsIURI> srcUri;
      rv = NS_NewURI(getter_AddRefs(srcUri), mBehavior.mSoundFile, charset, baseUri);
      if (NS_SUCCEEDED(rv)) {
        nsAutoCString src;
        srcUri->GetSpec(src);
        soundUrl = NS_ConvertUTF8toUTF16(src);
      }
    }
  }

  return rv;
}

already_AddRefed<Promise>
Notification::Get(nsPIDOMWindowInner* aWindow,
                  const GetNotificationOptions& aFilter,
                  const nsAString& aScope,
                  ErrorResult& aRv)
{
  MOZ_ASSERT(aWindow);

  nsCOMPtr<nsIDocument> doc = aWindow->GetExtantDoc();
  if (!doc) {
    aRv.Throw(NS_ERROR_UNEXPECTED);
    return nullptr;
  }

  nsString origin;
  aRv = GetOrigin(doc->NodePrincipal(), origin);
  if (aRv.Failed()) {
    return nullptr;
  }

  nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aWindow);
  RefPtr<Promise> promise = Promise::Create(global, aRv);
  if (aRv.Failed()) {
    return nullptr;
  }

  nsCOMPtr<nsINotificationStorageCallback> callback =
    new NotificationStorageCallback(global, aScope, promise);

  RefPtr<NotificationGetRunnable> r =
    new NotificationGetRunnable(origin, aFilter.mTag, callback);

  aRv = NS_DispatchToMainThread(r);
  if (NS_WARN_IF(aRv.Failed())) {
    return nullptr;
  }

  return promise.forget();
}

already_AddRefed<Promise>
Notification::Get(const GlobalObject& aGlobal,
                  const GetNotificationOptions& aFilter,
                  ErrorResult& aRv)
{
  AssertIsOnMainThread();
  nsCOMPtr<nsIGlobalObject> global =
    do_QueryInterface(aGlobal.GetAsSupports());
  MOZ_ASSERT(global);
  nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(global);

  return Get(window, aFilter, EmptyString(), aRv);
}

class WorkerGetResultRunnable final : public NotificationWorkerRunnable
{
  RefPtr<PromiseWorkerProxy> mPromiseProxy;
  const nsTArray<NotificationStrings> mStrings;
public:
  WorkerGetResultRunnable(WorkerPrivate* aWorkerPrivate,
                          PromiseWorkerProxy* aPromiseProxy,
                          const nsTArray<NotificationStrings>&& aStrings)
    : NotificationWorkerRunnable(aWorkerPrivate)
    , mPromiseProxy(aPromiseProxy)
    , mStrings(Move(aStrings))
  {
  }

  void
  WorkerRunInternal(WorkerPrivate* aWorkerPrivate) override
  {
    RefPtr<Promise> workerPromise = mPromiseProxy->WorkerPromise();

    ErrorResult result;
    AutoTArray<RefPtr<Notification>, 5> notifications;
    for (uint32_t i = 0; i < mStrings.Length(); ++i) {
      RefPtr<Notification> n =
        Notification::ConstructFromFields(aWorkerPrivate->GlobalScope(),
                                          mStrings[i].mID,
                                          mStrings[i].mTitle,
                                          mStrings[i].mDir,
                                          mStrings[i].mLang,
                                          mStrings[i].mBody,
                                          mStrings[i].mTag,
                                          mStrings[i].mIcon,
                                          mStrings[i].mData,
                                          /* mStrings[i].mBehavior, not
                                           * supported */
                                          mStrings[i].mServiceWorkerRegistrationID,
                                          result);

      n->SetStoredState(true);
      Unused << NS_WARN_IF(result.Failed());
      if (!result.Failed()) {
        notifications.AppendElement(n.forget());
      }
    }

    workerPromise->MaybeResolve(notifications);
    mPromiseProxy->CleanUp();
  }
};

class WorkerGetCallback final : public ScopeCheckingGetCallback
{
  RefPtr<PromiseWorkerProxy> mPromiseProxy;
public:
  NS_DECL_ISUPPORTS

  WorkerGetCallback(PromiseWorkerProxy* aProxy, const nsAString& aScope)
    : ScopeCheckingGetCallback(aScope), mPromiseProxy(aProxy)
  {
    AssertIsOnMainThread();
    MOZ_ASSERT(aProxy);
  }

  NS_IMETHOD Done() final
  {
    AssertIsOnMainThread();
    MOZ_ASSERT(mPromiseProxy, "Was Done() called twice?");

    RefPtr<PromiseWorkerProxy> proxy = mPromiseProxy.forget();
    MutexAutoLock lock(proxy->Lock());
    if (proxy->CleanedUp()) {
      return NS_OK;
    }

    RefPtr<WorkerGetResultRunnable> r =
      new WorkerGetResultRunnable(proxy->GetWorkerPrivate(),
                                  proxy,
                                  Move(mStrings));

    r->Dispatch();
    return NS_OK;
  }

private:
  ~WorkerGetCallback()
  {}
};

NS_IMPL_ISUPPORTS(WorkerGetCallback, nsINotificationStorageCallback)

class WorkerGetRunnable final : public Runnable
{
  RefPtr<PromiseWorkerProxy> mPromiseProxy;
  const nsString mTag;
  const nsString mScope;
public:
  WorkerGetRunnable(PromiseWorkerProxy* aProxy,
                    const nsAString& aTag,
                    const nsAString& aScope)
    : mPromiseProxy(aProxy), mTag(aTag), mScope(aScope)
  {
    MOZ_ASSERT(mPromiseProxy);
  }

  NS_IMETHOD
  Run() override
  {
    AssertIsOnMainThread();
    nsCOMPtr<nsINotificationStorageCallback> callback =
      new WorkerGetCallback(mPromiseProxy, mScope);

    nsresult rv;
    nsCOMPtr<nsINotificationStorage> notificationStorage =
      do_GetService(NS_NOTIFICATION_STORAGE_CONTRACTID, &rv);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      callback->Done();
      return rv;
    }

    MutexAutoLock lock(mPromiseProxy->Lock());
    if (mPromiseProxy->CleanedUp()) {
      return NS_OK;
    }

    nsString origin;
    rv =
      Notification::GetOrigin(mPromiseProxy->GetWorkerPrivate()->GetPrincipal(),
                              origin);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      callback->Done();
      return rv;
    }

    rv = notificationStorage->Get(origin, mTag, callback);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      callback->Done();
      return rv;
    }

    return NS_OK;
  }
private:
  ~WorkerGetRunnable()
  {}
};

already_AddRefed<Promise>
Notification::WorkerGet(WorkerPrivate* aWorkerPrivate,
                        const GetNotificationOptions& aFilter,
                        const nsAString& aScope,
                        ErrorResult& aRv)
{
  MOZ_ASSERT(aWorkerPrivate);
  aWorkerPrivate->AssertIsOnWorkerThread();
  RefPtr<Promise> p = Promise::Create(aWorkerPrivate->GlobalScope(), aRv);
  if (NS_WARN_IF(aRv.Failed())) {
    return nullptr;
  }

  RefPtr<PromiseWorkerProxy> proxy =
    PromiseWorkerProxy::Create(aWorkerPrivate, p);
  if (!proxy) {
    aRv.Throw(NS_ERROR_DOM_ABORT_ERR);
    return nullptr;
  }

  RefPtr<WorkerGetRunnable> r =
    new WorkerGetRunnable(proxy, aFilter.mTag, aScope);
  // Since this is called from script via
  // ServiceWorkerRegistration::GetNotifications, we can assert dispatch.
  MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(r));
  return p.forget();
}

JSObject*
Notification::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
{
  return mozilla::dom::NotificationBinding::Wrap(aCx, this, aGivenProto);
}

void
Notification::Close()
{
  AssertIsOnTargetThread();
  auto ref = MakeUnique<NotificationRef>(this);
  if (!ref->Initialized()) {
    return;
  }

  nsCOMPtr<nsIRunnable> closeNotificationTask =
    new NotificationTask(Move(ref), NotificationTask::eClose);
  nsresult rv = NS_DispatchToMainThread(closeNotificationTask);

  if (NS_FAILED(rv)) {
    DispatchTrustedEvent(NS_LITERAL_STRING("error"));
    // If dispatch fails, NotificationTask will release the ref when it goes
    // out of scope at the end of this function.
  }
}

void
Notification::CloseInternal()
{
  AssertIsOnMainThread();
  // Transfer ownership (if any) to local scope so we can release it at the end
  // of this function. This is relevant when the call is from
  // NotificationTask::Run().
  UniquePtr<NotificationRef> ownership;
  mozilla::Swap(ownership, mTempRef);

  SetAlertName();
  UnpersistNotification();
  if (!mIsClosed) {
    nsCOMPtr<nsIAlertsService> alertService =
      do_GetService(NS_ALERTSERVICE_CONTRACTID);
    if (alertService) {
      nsAutoString alertName;
      GetAlertName(alertName);
      alertService->CloseAlert(alertName, GetPrincipal());
    }
  }
}

nsresult
Notification::GetOrigin(nsIPrincipal* aPrincipal, nsString& aOrigin)
{
  if (!aPrincipal) {
    return NS_ERROR_FAILURE;
  }

  uint16_t appStatus = aPrincipal->GetAppStatus();
  uint32_t appId = aPrincipal->GetAppId();

  nsresult rv;
  if (appStatus == nsIPrincipal::APP_STATUS_NOT_INSTALLED ||
      appId == nsIScriptSecurityManager::NO_APP_ID ||
      appId == nsIScriptSecurityManager::UNKNOWN_APP_ID) {
    rv = nsContentUtils::GetUTFOrigin(aPrincipal, aOrigin);
    NS_ENSURE_SUCCESS(rv, rv);
  } else {
    // If we are in "app code", use manifest URL as unique origin since
    // multiple apps can share the same origin but not same notifications.
    nsCOMPtr<nsIAppsService> appsService =
      do_GetService("@mozilla.org/AppsService;1", &rv);
    NS_ENSURE_SUCCESS(rv, rv);
    appsService->GetManifestURLByLocalId(appId, aOrigin);
  }

  return NS_OK;
}

void
Notification::GetData(JSContext* aCx,
                      JS::MutableHandle<JS::Value> aRetval)
{
  if (mData.isNull() && !mDataAsBase64.IsEmpty()) {
    nsresult rv;
    RefPtr<nsStructuredCloneContainer> container =
      new nsStructuredCloneContainer();
    rv = container->InitFromBase64(mDataAsBase64, JS_STRUCTURED_CLONE_VERSION);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      aRetval.setNull();
      return;
    }

    JS::Rooted<JS::Value> data(aCx);
    rv = container->DeserializeToJsval(aCx, &data);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      aRetval.setNull();
      return;
    }

    if (data.isGCThing()) {
      mozilla::HoldJSObjects(this);
    }
    mData = data;
  }
  if (mData.isNull()) {
    aRetval.setNull();
    return;
  }

  JS::ExposeValueToActiveJS(mData);
  aRetval.set(mData);
}

void
Notification::InitFromJSVal(JSContext* aCx, JS::Handle<JS::Value> aData,
                            ErrorResult& aRv)
{
  if (!mDataAsBase64.IsEmpty() || aData.isNull()) {
    return;
  }
  RefPtr<nsStructuredCloneContainer> dataObjectContainer =
    new nsStructuredCloneContainer();
  aRv = dataObjectContainer->InitFromJSVal(aData, aCx);
  if (NS_WARN_IF(aRv.Failed())) {
    return;
  }

  dataObjectContainer->GetDataAsBase64(mDataAsBase64);
}

void Notification::InitFromBase64(const nsAString& aData, ErrorResult& aRv)
{
  if (!mDataAsBase64.IsEmpty() || aData.IsEmpty()) {
    return;
  }

  // To and fro to ensure it is valid base64.
  RefPtr<nsStructuredCloneContainer> container =
    new nsStructuredCloneContainer();
  aRv = container->InitFromBase64(aData, JS_STRUCTURED_CLONE_VERSION);
  if (NS_WARN_IF(aRv.Failed())) {
    return;
  }

  container->GetDataAsBase64(mDataAsBase64);
}

bool
Notification::AddRefObject()
{
  AssertIsOnTargetThread();
  MOZ_ASSERT_IF(mWorkerPrivate && !mFeature, mTaskCount == 0);
  MOZ_ASSERT_IF(mWorkerPrivate && mFeature, mTaskCount > 0);
  if (mWorkerPrivate && !mFeature) {
    if (!RegisterFeature()) {
      return false;
    }
  }
  AddRef();
  ++mTaskCount;
  return true;
}

void
Notification::ReleaseObject()
{
  AssertIsOnTargetThread();
  MOZ_ASSERT(mTaskCount > 0);
  MOZ_ASSERT_IF(mWorkerPrivate, mFeature);

  --mTaskCount;
  if (mWorkerPrivate && mTaskCount == 0) {
    UnregisterFeature();
  }
  Release();
}

NotificationFeature::NotificationFeature(Notification* aNotification)
  : mNotification(aNotification)
{
  MOZ_ASSERT(mNotification->mWorkerPrivate);
  mNotification->mWorkerPrivate->AssertIsOnWorkerThread();
}

/*
 * Called from the worker, runs on main thread, blocks worker.
 *
 * We can freely access mNotification here because the feature supplied it and
 * the Notification owns the feature.
 */
class CloseNotificationRunnable final
  : public WorkerMainThreadRunnable
{
  Notification* mNotification;
  bool mHadObserver;

  public:
  explicit CloseNotificationRunnable(Notification* aNotification)
    : WorkerMainThreadRunnable(aNotification->mWorkerPrivate,
                               NS_LITERAL_CSTRING("Notification :: Close Notification"))
    , mNotification(aNotification)
    , mHadObserver(false)
  {}

  bool
  MainThreadRun() override
  {
    if (mNotification->mObserver) {
      // The Notify() take's responsibility of releasing the Notification.
      mNotification->mObserver->ForgetNotification();
      mNotification->mObserver = nullptr;
      mHadObserver = true;
    }
    mNotification->CloseInternal();
    return true;
  }

  bool
  HadObserver()
  {
    return mHadObserver;
  }
};

bool
NotificationFeature::Notify(Status aStatus)
{
  if (aStatus >= Canceling) {
    // CloseNotificationRunnable blocks the worker by pushing a sync event loop
    // on the stack. Meanwhile, WorkerControlRunnables dispatched to the worker
    // can still continue running. One of these is
    // ReleaseNotificationControlRunnable that releases the notification,
    // invalidating the notification and this feature. We hold this reference to
    // keep the notification valid until we are done with it.
    //
    // An example of when the control runnable could get dispatched to the
    // worker is if a Notification is created and the worker is immediately
    // closed, but there is no permission to show it so that the main thread
    // immediately drops the NotificationRef. In this case, this function blocks
    // on the main thread, but the main thread dispatches the control runnable,
    // invalidating mNotification.
    RefPtr<Notification> kungFuDeathGrip = mNotification;

    // Dispatched to main thread, blocks on closing the Notification.
    RefPtr<CloseNotificationRunnable> r =
      new CloseNotificationRunnable(mNotification);
    ErrorResult rv;
    r->Dispatch(rv);
    // XXXbz I'm told throwing and returning false from here is pointless (and
    // also that doing sync stuff from here is really weird), so I guess we just
    // suppress the exception on rv, if any.
    rv.SuppressException();

    // Only call ReleaseObject() to match the observer's NotificationRef
    // ownership (since CloseNotificationRunnable asked the observer to drop the
    // reference to the notification).
    if (r->HadObserver()) {
      mNotification->ReleaseObject();
    }

    // From this point we cannot touch properties of this feature because
    // ReleaseObject() may have led to the notification going away and the
    // notification owns this feature!
  }
  return true;
}

bool
Notification::RegisterFeature()
{
  MOZ_ASSERT(mWorkerPrivate);
  mWorkerPrivate->AssertIsOnWorkerThread();
  MOZ_ASSERT(!mFeature);
  mFeature = MakeUnique<NotificationFeature>(this);
  bool added = mWorkerPrivate->AddFeature(mFeature.get());
  if (!added) {
    mFeature = nullptr;
  }

  return added;
}

void
Notification::UnregisterFeature()
{
  MOZ_ASSERT(mWorkerPrivate);
  mWorkerPrivate->AssertIsOnWorkerThread();
  MOZ_ASSERT(mFeature);
  mWorkerPrivate->RemoveFeature(mFeature.get());
  mFeature = nullptr;
}

/*
 * Checks:
 * 1) Is aWorker allowed to show a notification for scope?
 * 2) Is aWorker an active worker?
 *
 * If it is not an active worker, Result() will be NS_ERROR_NOT_AVAILABLE.
 */
class CheckLoadRunnable final : public WorkerMainThreadRunnable
{
  nsresult mRv;
  nsCString mScope;

public:
  explicit CheckLoadRunnable(WorkerPrivate* aWorker, const nsACString& aScope)
    : WorkerMainThreadRunnable(aWorker,
                               NS_LITERAL_CSTRING("Notification :: Check Load"))
    , mRv(NS_ERROR_DOM_SECURITY_ERR)
    , mScope(aScope)
  { }

  bool
  MainThreadRun() override
  {
    nsIPrincipal* principal = mWorkerPrivate->GetPrincipal();
    mRv = CheckScope(principal, mScope);

    if (NS_FAILED(mRv)) {
      return true;
    }

    RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
    RefPtr<ServiceWorkerRegistrationInfo> registration =
      swm->GetRegistration(principal, mScope);

    // This is coming from a ServiceWorkerRegistrationWorkerThread.
    MOZ_ASSERT(registration);

    if (!registration->GetActive() ||
        registration->GetActive()->ID() != mWorkerPrivate->ServiceWorkerID()) {
      mRv = NS_ERROR_NOT_AVAILABLE;
    }

    return true;
  }

  nsresult
  Result()
  {
    return mRv;
  }

};

/* static */
already_AddRefed<Promise>
Notification::ShowPersistentNotification(JSContext* aCx,
                                         nsIGlobalObject *aGlobal,
                                         const nsAString& aScope,
                                         const nsAString& aTitle,
                                         const NotificationOptions& aOptions,
                                         ErrorResult& aRv)
{
  MOZ_ASSERT(aGlobal);

  // Validate scope.
  // XXXnsm: This may be slow due to blocking the worker and waiting on the main
  // thread. On calls from content, we can be sure the scope is valid since
  // ServiceWorkerRegistrations have their scope set correctly. Can this be made
  // debug only? The problem is that there would be different semantics in
  // debug and non-debug builds in such a case.
  if (NS_IsMainThread()) {
    nsCOMPtr<nsIScriptObjectPrincipal> sop = do_QueryInterface(aGlobal);
    if (NS_WARN_IF(!sop)) {
      aRv.Throw(NS_ERROR_UNEXPECTED);
      return nullptr;
    }

    nsIPrincipal* principal = sop->GetPrincipal();
    if (NS_WARN_IF(!principal)) {
      aRv.Throw(NS_ERROR_UNEXPECTED);
      return nullptr;
    }

    aRv = CheckScope(principal, NS_ConvertUTF16toUTF8(aScope));
    if (NS_WARN_IF(aRv.Failed())) {
      aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
      return nullptr;
    }
  } else {
    WorkerPrivate* worker = GetCurrentThreadWorkerPrivate();
    MOZ_ASSERT(worker);
    worker->AssertIsOnWorkerThread();
    RefPtr<CheckLoadRunnable> loadChecker =
      new CheckLoadRunnable(worker, NS_ConvertUTF16toUTF8(aScope));
    loadChecker->Dispatch(aRv);
    if (aRv.Failed()) {
      return nullptr;
    }

    if (NS_WARN_IF(NS_FAILED(loadChecker->Result()))) {
      if (loadChecker->Result() == NS_ERROR_NOT_AVAILABLE) {
        aRv.ThrowTypeError<MSG_NO_ACTIVE_WORKER>(aScope);
      } else {
        aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
      }
      return nullptr;
    }
  }


  RefPtr<Promise> p = Promise::Create(aGlobal, aRv);
  if (NS_WARN_IF(aRv.Failed())) {
    return nullptr;
  }

  // We check permission here rather than pass the Promise to NotificationTask
  // which leads to uglier code.
  NotificationPermission permission = GetPermission(aGlobal, aRv);

  // "If permission for notification’s origin is not "granted", reject promise with a TypeError exception, and terminate these substeps."
  if (NS_WARN_IF(aRv.Failed()) || permission == NotificationPermission::Denied) {
    ErrorResult result;
    result.ThrowTypeError<MSG_NOTIFICATION_PERMISSION_DENIED>();
    p->MaybeReject(result);
    return p.forget();
  }

  // "Otherwise, resolve promise with undefined."
  // The Notification may still not be shown due to other errors, but the spec
  // is not concerned with those.
  p->MaybeResolve(JS::UndefinedHandleValue);

  RefPtr<Notification> notification =
    CreateAndShow(aCx, aGlobal, aTitle, aOptions, aScope, aRv);
  if (NS_WARN_IF(aRv.Failed())) {
    return nullptr;
  }

  return p.forget();
}

/* static */ already_AddRefed<Notification>
Notification::CreateAndShow(JSContext* aCx,
                            nsIGlobalObject* aGlobal,
                            const nsAString& aTitle,
                            const NotificationOptions& aOptions,
                            const nsAString& aScope,
                            ErrorResult& aRv)
{
  MOZ_ASSERT(aGlobal);

  RefPtr<Notification> notification = CreateInternal(aGlobal, EmptyString(),
                                                     aTitle, aOptions);

  // Make a structured clone of the aOptions.mData object
  JS::Rooted<JS::Value> data(aCx, aOptions.mData);
  notification->InitFromJSVal(aCx, data, aRv);
  if (NS_WARN_IF(aRv.Failed())) {
    return nullptr;
  }

  notification->SetScope(aScope);

  auto ref = MakeUnique<NotificationRef>(notification);
  if (NS_WARN_IF(!ref->Initialized())) {
    aRv.Throw(NS_ERROR_DOM_ABORT_ERR);
    return nullptr;
  }

  // Queue a task to show the notification.
  nsCOMPtr<nsIRunnable> showNotificationTask =
    new NotificationTask(Move(ref), NotificationTask::eShow);
  nsresult rv = NS_DispatchToMainThread(showNotificationTask);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    notification->DispatchTrustedEvent(NS_LITERAL_STRING("error"));
  }

  return notification.forget();
}

/* static */ nsresult
Notification::RemovePermission(nsIPrincipal* aPrincipal)
{
  MOZ_ASSERT(XRE_IsParentProcess());
  nsCOMPtr<nsIPermissionManager> permissionManager =
    mozilla::services::GetPermissionManager();
  if (!permissionManager) {
    return NS_ERROR_FAILURE;
  }
  permissionManager->RemoveFromPrincipal(aPrincipal, "desktop-notification");
  return NS_OK;
}

/* static */ nsresult
Notification::OpenSettings(nsIPrincipal* aPrincipal)
{
  MOZ_ASSERT(XRE_IsParentProcess());
  nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
  if (!obs) {
    return NS_ERROR_FAILURE;
  }
  // Notify other observers so they can show settings UI.
  obs->NotifyObservers(aPrincipal, "notifications-open-settings", nullptr);
  return NS_OK;
}

NS_IMETHODIMP
Notification::Observe(nsISupports* aSubject, const char* aTopic,
                      const char16_t* aData)
{
  AssertIsOnMainThread();

  if (!strcmp(aTopic, DOM_WINDOW_DESTROYED_TOPIC) ||
      !strcmp(aTopic, DOM_WINDOW_FROZEN_TOPIC)) {

    nsCOMPtr<nsPIDOMWindowInner> window = GetOwner();
    if (SameCOMIdentity(aSubject, window)) {
      nsCOMPtr<nsIObserverService> obs =
        mozilla::services::GetObserverService();
      if (obs) {
        obs->RemoveObserver(this, DOM_WINDOW_DESTROYED_TOPIC);
        obs->RemoveObserver(this, DOM_WINDOW_FROZEN_TOPIC);
      }

      uint16_t appStatus = nsIPrincipal::APP_STATUS_NOT_INSTALLED;
      uint32_t appId = nsIScriptSecurityManager::UNKNOWN_APP_ID;

      nsCOMPtr<nsIDocument> doc = window ? window->GetExtantDoc() : nullptr;
      nsCOMPtr<nsIPrincipal> nodePrincipal = doc ? doc->NodePrincipal() :
                                             nullptr;
      if (nodePrincipal) {
        appStatus = nodePrincipal->GetAppStatus();
        appId = nodePrincipal->GetAppId();
      }

      if (appStatus == nsIPrincipal::APP_STATUS_NOT_INSTALLED ||
          appId == nsIScriptSecurityManager::NO_APP_ID ||
          appId == nsIScriptSecurityManager::UNKNOWN_APP_ID) {
        CloseInternal();
      }
    }
  }

  return NS_OK;
}

} // namespace dom
} // namespace mozilla

