/* -*- 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 "CookieStoreSubscriptionService.h"
#include "json/json.h"
#include "mozilla/ClearOnShutdown.h"
#include "mozilla/dom/PCookieStore.h"
#include "mozilla/dom/ServiceWorkerManager.h"
#include "mozilla/dom/ServiceWorkerRegistrar.h"
#include "mozilla/net/Cookie.h"
#include "mozilla/net/CookieCommons.h"
#include "nsAppDirectoryServiceDefs.h"
#include "nsICookieNotification.h"

using namespace mozilla::dom;
using namespace mozilla::net;
using mozilla::ipc::PrincipalInfo;

static mozilla::StaticRefPtr<CookieStoreSubscriptionService> gService;

NS_IMPL_ISUPPORTS(CookieStoreSubscriptionService, nsIObserver)

// static
void CookieStoreSubscriptionService::ServiceWorkerLoaded(
    const ServiceWorkerRegistrationData& aData, const nsACString& aValue) {
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(XRE_IsParentProcess());

  CookieStoreSubscriptionService* service =
      CookieStoreSubscriptionService::Instance();
  service->Load(aData, aValue);
}

// static
void CookieStoreSubscriptionService::ServiceWorkerUpdated(
    const ServiceWorkerRegistrationData& aData) {
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(XRE_IsParentProcess());

  // This is a no-op
}

// static
void CookieStoreSubscriptionService::ServiceWorkerUnregistered(
    const ServiceWorkerRegistrationData& aData) {
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(XRE_IsParentProcess());

  CookieStoreSubscriptionService* service =
      CookieStoreSubscriptionService::Instance();
  service->Unregister(aData);
}
// static
void CookieStoreSubscriptionService::ServiceWorkerUnregistered(
    nsIPrincipal* aPrincipal, const nsACString& aScopeURL) {
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(XRE_IsParentProcess());

  PrincipalInfo principalInfo;
  nsresult rv = PrincipalToPrincipalInfo(aPrincipal, &principalInfo);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return;
  }

  ServiceWorkerRegistrationData tmp;
  tmp.principal() = principalInfo;
  tmp.scope() = aScopeURL;

  CookieStoreSubscriptionService* service =
      CookieStoreSubscriptionService::Instance();
  service->Unregister(tmp);
}

// static
CookieStoreSubscriptionService* CookieStoreSubscriptionService::Instance() {
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(XRE_IsParentProcess());

  if (!gService && !PastShutdownPhase(ShutdownPhase::XPCOMShutdownFinal)) {
    gService = new CookieStoreSubscriptionService();
    gService->Initialize();
    ClearOnShutdown(&gService, ShutdownPhase::XPCOMShutdownFinal);
  }

  return gService;
}

CookieStoreSubscriptionService::CookieStoreSubscriptionService() {
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(XRE_IsParentProcess());
}

void CookieStoreSubscriptionService::Initialize() {
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(XRE_IsParentProcess());

  nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
  if (obs) {
    DebugOnly<nsresult> rv =
        obs->AddObserver(gService, "private-cookie-changed", false);
    MOZ_ASSERT(NS_SUCCEEDED(rv));

    rv = obs->AddObserver(gService, "cookie-changed", false);
    MOZ_ASSERT(NS_SUCCEEDED(rv));
  }
}

namespace {

bool Equivalent(const ServiceWorkerRegistrationData& aLeft,
                const ServiceWorkerRegistrationData& aRight) {
  MOZ_ASSERT(aLeft.principal().type() == PrincipalInfo::TContentPrincipalInfo);
  MOZ_ASSERT(aRight.principal().type() == PrincipalInfo::TContentPrincipalInfo);

  const auto& leftPrincipal = aLeft.principal().get_ContentPrincipalInfo();
  const auto& rightPrincipal = aRight.principal().get_ContentPrincipalInfo();

  // Only compare the attributes, not the spec part of the principal.
  // The scope comparison above already covers the origin and codebase
  // principals include the full path in their spec which is not what
  // we want here.
  return aLeft.scope() == aRight.scope() &&
         leftPrincipal.attrs() == rightPrincipal.attrs();
}

}  // anonymous namespace

void CookieStoreSubscriptionService::GetSubscriptions(
    const PrincipalInfo& aPrincipalInfo, const nsACString& aScope,
    nsTArray<CookieSubscription>& aSubscriptions) {
  MOZ_ASSERT(NS_IsMainThread());

  ServiceWorkerRegistrationData tmp;
  tmp.principal() = aPrincipalInfo;
  tmp.scope() = aScope;

  for (const RegistrationData& data : mData) {
    if (Equivalent(tmp, data.mRegistration)) {
      aSubscriptions.AppendElements(data.mSubscriptions);
      break;
    }
  }
}

void CookieStoreSubscriptionService::Subscribe(
    const PrincipalInfo& aPrincipalInfo, const nsACString& aScope,
    const nsTArray<CookieSubscription>& aSubscriptions) {
  MOZ_ASSERT(NS_IsMainThread());

  ServiceWorkerRegistrationData tmp;
  tmp.principal() = aPrincipalInfo;
  tmp.scope() = aScope;

  RegistrationData* registrationData = nullptr;

  for (RegistrationData& data : mData) {
    if (Equivalent(tmp, data.mRegistration)) {
      registrationData = &data;
      break;
    }
  }

  if (!registrationData) {
    registrationData = mData.AppendElement();
    registrationData->mRegistration = tmp;
  }

  bool toStore = false;

  for (const CookieSubscription& subscription : aSubscriptions) {
    bool found = false;
    for (const CookieSubscription& existingSubscription :
         registrationData->mSubscriptions) {
      if (existingSubscription.name() == subscription.name() &&
          existingSubscription.url() == subscription.url()) {
        // Nothing to do.
        found = true;
        break;
      }
    }

    if (!found) {
      registrationData->mSubscriptions.AppendElement(subscription);
      toStore = true;
    }
  }

  if (toStore) {
    SerializeAndSave(*registrationData);
  }
}

void CookieStoreSubscriptionService::Unsubscribe(
    const PrincipalInfo& aPrincipalInfo, const nsACString& aScope,
    const nsTArray<CookieSubscription>& aSubscriptions) {
  MOZ_ASSERT(NS_IsMainThread());

  ServiceWorkerRegistrationData tmp;
  tmp.principal() = aPrincipalInfo;
  tmp.scope() = aScope;

  RegistrationData* registrationData = nullptr;
  uint32_t registrationDataId = 0;

  for (; registrationDataId < mData.Length(); ++registrationDataId) {
    RegistrationData& data = mData[registrationDataId];
    if (Equivalent(tmp, data.mRegistration)) {
      registrationData = &data;
      break;
    }
  }

  if (!registrationData) {
    return;
  }

  bool toStore = false;

  for (const CookieSubscription& subscription : aSubscriptions) {
    for (uint32_t i = 0; i < registrationData->mSubscriptions.Length(); ++i) {
      const CookieSubscription& existingSubscription =
          registrationData->mSubscriptions[i];
      if (existingSubscription.name() == subscription.name() &&
          existingSubscription.url() == subscription.url()) {
        registrationData->mSubscriptions.RemoveElementAt(i);
        toStore = true;
        break;
      }
    }
  }

  if (toStore) {
    SerializeAndSave(*registrationData);

    if (registrationData->mSubscriptions.IsEmpty()) {
      mData.RemoveElementAt(registrationDataId);
    }
  }
}

NS_IMETHODIMP
CookieStoreSubscriptionService::Observe(nsISupports* aSubject,
                                        const char* aTopic,
                                        const char16_t* aData) {
  MOZ_ASSERT(NS_IsMainThread());

  nsCOMPtr<nsICookieNotification> notification = do_QueryInterface(aSubject);
  NS_ENSURE_TRUE(notification, NS_ERROR_FAILURE);

  auto action = notification->GetAction();
  if (action != nsICookieNotification::COOKIE_DELETED &&
      action != nsICookieNotification::COOKIE_ADDED &&
      action != nsICookieNotification::COOKIE_CHANGED) {
    // Other actions are user specific ones (ALL_COOKIES_CLEARED or
    // COOKIES_BATCH_DELETED) and we don't want to expose them here.
    return NS_OK;
  }

  nsAutoCString baseDomain;
  nsresult rv = notification->GetBaseDomain(baseDomain);
  if (NS_WARN_IF(NS_FAILED(rv)) || baseDomain.IsEmpty()) {
    return rv;
  }

  nsCOMPtr<nsICookie> cookie;
  rv = notification->GetCookie(getter_AddRefs(cookie));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  bool isHttpOnly;
  rv = cookie->GetIsHttpOnly(&isHttpOnly);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  if (isHttpOnly) {
    return NS_OK;
  }

  nsAutoCString nameUtf8;
  rv = cookie->GetName(nameUtf8);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  NS_ConvertUTF8toUTF16 name(nameUtf8);

  bool deleteEvent = action == nsICookieNotification::COOKIE_DELETED;

  nsAutoString value;
  if (!deleteEvent) {
    nsAutoCString valueUtf8;
    rv = cookie->GetValue(valueUtf8);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return rv;
    }

    CopyUTF8toUTF16(valueUtf8, value);
  }

  RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
  if (!swm) {
    return NS_ERROR_FAILURE;
  }

  for (const RegistrationData& data : mData) {
    MOZ_ASSERT(data.mRegistration.principal().type() ==
               PrincipalInfo::TContentPrincipalInfo);

    const auto& principalInfo =
        data.mRegistration.principal().get_ContentPrincipalInfo();

    if (principalInfo.baseDomain() != baseDomain) {
      continue;
    }

    if (cookie->OriginAttributesNative() != principalInfo.attrs()) {
      continue;
    }

    for (const CookieSubscription& subscription : data.mSubscriptions) {
      if (subscription.name().isSome() && subscription.name().value() != name) {
        continue;
      }

      nsCOMPtr<nsIURI> uri;
      rv = NS_NewURI(getter_AddRefs(uri), data.mRegistration.scope());
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return rv;
      }

      nsAutoCString filePath;
      rv = uri->GetFilePath(filePath);
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return rv;
      }

      if (!CookieCommons::PathMatches(cookie->AsCookie().Path(), filePath)) {
        continue;
      }

      rv = swm->SendCookieChangeEvent(principalInfo.attrs(),
                                      data.mRegistration.scope(),
                                      cookie->AsCookie().ToIPC(), deleteEvent);
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return rv;
      }

      break;
    }
  }

  return NS_OK;
}

void CookieStoreSubscriptionService::Load(
    const ServiceWorkerRegistrationData& aData, const nsACString& aValue) {
  MOZ_ASSERT(NS_IsMainThread());

  for (RegistrationData& data : mData) {
    if (Equivalent(aData, data.mRegistration)) {
      ParseAndAddSubscription(data, aValue);
      return;
    }
  }

  RegistrationData* data = mData.AppendElement();
  data->mRegistration = aData;
  ParseAndAddSubscription(*data, aValue);
}

void CookieStoreSubscriptionService::Unregister(
    const ServiceWorkerRegistrationData& aData) {
  MOZ_ASSERT(NS_IsMainThread());

  for (uint32_t i = 0; i < mData.Length(); ++i) {
    if (Equivalent(aData, mData[i].mRegistration)) {
      mData.RemoveElementAt(i);
      return;
    }
  }
}

void CookieStoreSubscriptionService::ParseAndAddSubscription(
    RegistrationData& aData, const nsACString& aValue) {
  MOZ_ASSERT(NS_IsMainThread());

  Json::Value value;
  Json::Reader jsonReader;

  MOZ_ASSERT(jsonReader.parse(aValue.BeginReading(), value, false));
  MOZ_ASSERT(value.isObject());

  for (Json::ValueConstIterator iter = value.begin(); iter != value.end();
       ++iter) {
    CookieSubscription* subscription = aData.mSubscriptions.AppendElement();

    for (Json::Value::const_iterator itr = iter->begin(); itr != iter->end();
         itr++) {
      MOZ_ASSERT(iter.key().isString());
      MOZ_ASSERT(iter->isString());
      if (itr.key().asString().compare("name") == 0) {
        subscription->name() =
            Some(NS_ConvertUTF8toUTF16(iter->asString().c_str()));
      } else if (itr.key().asString().compare("url") == 0) {
        subscription->url() = NS_ConvertUTF8toUTF16(iter->asString().c_str());
      }
    }
  }
}

void CookieStoreSubscriptionService::SerializeAndSave(
    const RegistrationData& aData) {
  MOZ_ASSERT(NS_IsMainThread());

  RefPtr<ServiceWorkerRegistrar> swr = ServiceWorkerRegistrar::Get();
  MOZ_ASSERT(swr);

  if (aData.mSubscriptions.IsEmpty()) {
    swr->UnstoreServiceWorkerExpandoOnMainThread(
        aData.mRegistration.principal(), aData.mRegistration.scope(),
        nsCString("cookie-store"));
    return;
  }

  Json::Value root;
  Json::StreamWriterBuilder builder;
  builder["indentation"] = "";
  const std::unique_ptr<Json::StreamWriter> writer(builder.newStreamWriter());

  for (uint32_t i = 0; i < aData.mSubscriptions.Length(); ++i) {
    Json::Value entry;

    if (aData.mSubscriptions[i].name().isSome()) {
      entry["name"] =
          NS_ConvertUTF16toUTF8(aData.mSubscriptions[i].name().value()).get();
    }

    entry["url"] = NS_ConvertUTF16toUTF8(aData.mSubscriptions[i].url()).get();
    root[i] = entry;
  }

  std::string document = Json::writeString(builder, root);

  swr->StoreServiceWorkerExpandoOnMainThread(
      aData.mRegistration.principal(), aData.mRegistration.scope(),
      nsCString("cookie-store"), nsCString(document));
}
