uriloader/prefetch/nsOfflineCacheUpdate.cpp
author Glenn Watson <git@intuitionlibrary.com>
Sun, 13 Oct 2019 20:24:40 +0000
changeset 497380 1af5cb02462a3f01b83cdb48da9644da36ec7e4d
parent 495709 71d73642c1263b5d4ed11f3f1a221fd9e9990026
permissions -rw-r--r--
Bug 1587676 - Allocate picture caching tile surfaces later during the frame. r=nical,kvark Instead of allocating a tile surface from the texture cache during the tile post_update method, this is now deferred until the take_context method of picture. This will allow a simple CPU occlusion culling pass to run after all the tile cache dependency updates (when the visible tiles and rects are know), but before the surfaces are allocated. In this way, we will be able to skip allocating, rasterizing and compositing any tiles that are eliminated by the occlusion culling test. Differential Revision: https://phabricator.services.mozilla.com/D48795

/* -*- mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* 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 "nsOfflineCacheUpdate.h"

#include "nsCURILoader.h"
#include "nsIApplicationCacheContainer.h"
#include "nsIApplicationCacheChannel.h"
#include "nsIApplicationCacheService.h"
#include "nsICachingChannel.h"
#include "nsIContent.h"
#include "mozilla/dom/Element.h"
#include "mozilla/dom/OfflineResourceListBinding.h"
#include "nsIDocumentLoader.h"
#include "mozilla/dom/Document.h"
#include "nsIObserverService.h"
#include "nsIURL.h"
#include "nsIURIMutator.h"
#include "nsIWebProgress.h"
#include "nsICryptoHash.h"
#include "nsICacheEntry.h"
#include "nsIPermissionManager.h"
#include "nsIPrincipal.h"
#include "nsNetCID.h"
#include "nsNetUtil.h"
#include "nsServiceManagerUtils.h"
#include "nsStreamUtils.h"
#include "nsThreadUtils.h"
#include "nsProxyRelease.h"
#include "nsIConsoleService.h"
#include "mozilla/Logging.h"
#include "nsIAsyncVerifyRedirectCallback.h"
#include "mozilla/Preferences.h"
#include "mozilla/Attributes.h"
#include "nsContentUtils.h"
#include "nsIPrincipal.h"
#include "nsDiskCacheDeviceSQL.h"
#include "ReferrerInfo.h"

#include "nsXULAppAPI.h"

using namespace mozilla;

static const uint32_t kRescheduleLimit = 3;
// Max number of retries for every entry of pinned app.
static const uint32_t kPinnedEntryRetriesLimit = 3;
// Maximum number of parallel items loads
static const uint32_t kParallelLoadLimit = 15;

// Quota for offline apps when preloading
static const int32_t kCustomProfileQuota = 512000;

//
// To enable logging (see mozilla/Logging.h for full details):
//
//    set MOZ_LOG=nsOfflineCacheUpdate:5
//    set MOZ_LOG_FILE=offlineupdate.log
//
// this enables LogLevel::Debug level information and places all output in
// the file offlineupdate.log
//
extern LazyLogModule gOfflineCacheUpdateLog;

#undef LOG
#define LOG(args) \
  MOZ_LOG(gOfflineCacheUpdateLog, mozilla::LogLevel::Debug, args)

#undef LOG_ENABLED
#define LOG_ENABLED() \
  MOZ_LOG_TEST(gOfflineCacheUpdateLog, mozilla::LogLevel::Debug)

namespace {

nsresult DropReferenceFromURL(nsCOMPtr<nsIURI>& aURI) {
  // XXXdholbert If this SetRef fails, callers of this method probably
  // want to call aURI->CloneIgnoringRef() and use the result of that.
  nsCOMPtr<nsIURI> uri(aURI);
  return NS_GetURIWithoutRef(uri, getter_AddRefs(aURI));
}

void LogToConsole(const char* message,
                  nsOfflineCacheUpdateItem* item = nullptr) {
  nsCOMPtr<nsIConsoleService> consoleService =
      do_GetService(NS_CONSOLESERVICE_CONTRACTID);
  if (consoleService) {
    nsAutoString messageUTF16 = NS_ConvertUTF8toUTF16(message);
    if (item && item->mURI) {
      messageUTF16.AppendLiteral(", URL=");
      messageUTF16.Append(
          NS_ConvertUTF8toUTF16(item->mURI->GetSpecOrDefault()));
    }
    consoleService->LogStringMessage(messageUTF16.get());
  }
}

}  // namespace

//-----------------------------------------------------------------------------
// nsManifestCheck
//-----------------------------------------------------------------------------

class nsManifestCheck final : public nsIStreamListener,
                              public nsIChannelEventSink,
                              public nsIInterfaceRequestor {
 public:
  nsManifestCheck(nsOfflineCacheUpdate* aUpdate, nsIURI* aURI,
                  nsIURI* aReferrerURI, nsIPrincipal* aLoadingPrincipal)
      : mUpdate(aUpdate),
        mURI(aURI),
        mReferrerURI(aReferrerURI),
        mLoadingPrincipal(aLoadingPrincipal) {}

  NS_DECL_ISUPPORTS
  NS_DECL_NSIREQUESTOBSERVER
  NS_DECL_NSISTREAMLISTENER
  NS_DECL_NSICHANNELEVENTSINK
  NS_DECL_NSIINTERFACEREQUESTOR

  nsresult Begin();

 private:
  ~nsManifestCheck() {}

  static nsresult ReadManifest(nsIInputStream* aInputStream, void* aClosure,
                               const char* aFromSegment, uint32_t aOffset,
                               uint32_t aCount, uint32_t* aBytesConsumed);

  RefPtr<nsOfflineCacheUpdate> mUpdate;
  nsCOMPtr<nsIURI> mURI;
  nsCOMPtr<nsIURI> mReferrerURI;
  nsCOMPtr<nsIPrincipal> mLoadingPrincipal;
  nsCOMPtr<nsICryptoHash> mManifestHash;
  nsCOMPtr<nsIChannel> mChannel;
};

//-----------------------------------------------------------------------------
// nsManifestCheck::nsISupports
//-----------------------------------------------------------------------------
NS_IMPL_ISUPPORTS(nsManifestCheck, nsIRequestObserver, nsIStreamListener,
                  nsIChannelEventSink, nsIInterfaceRequestor)

//-----------------------------------------------------------------------------
// nsManifestCheck <public>
//-----------------------------------------------------------------------------

nsresult nsManifestCheck::Begin() {
  nsresult rv;
  mManifestHash = do_CreateInstance("@mozilla.org/security/hash;1", &rv);
  NS_ENSURE_SUCCESS(rv, rv);

  rv = mManifestHash->Init(nsICryptoHash::MD5);
  NS_ENSURE_SUCCESS(rv, rv);
  rv = NS_NewChannel(getter_AddRefs(mChannel), mURI, mLoadingPrincipal,
                     nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_DATA_IS_BLOCKED,
                     nsIContentPolicy::TYPE_OTHER,
                     nullptr,  // nsICookieSettings
                     nullptr,  // PerformanceStorage
                     nullptr,  // loadGroup
                     nullptr,  // aCallbacks
                     nsIRequest::LOAD_BYPASS_CACHE);

  NS_ENSURE_SUCCESS(rv, rv);

  // configure HTTP specific stuff
  nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(mChannel);
  if (httpChannel) {
    nsCOMPtr<nsIReferrerInfo> referrerInfo =
        new mozilla::dom::ReferrerInfo(mReferrerURI);
    rv = httpChannel->SetReferrerInfoWithoutClone(referrerInfo);
    MOZ_ASSERT(NS_SUCCEEDED(rv));
    rv = httpChannel->SetRequestHeader(NS_LITERAL_CSTRING("X-Moz"),
                                       NS_LITERAL_CSTRING("offline-resource"),
                                       false);
    MOZ_ASSERT(NS_SUCCEEDED(rv));
  }

  return mChannel->AsyncOpen(this);
}

//-----------------------------------------------------------------------------
// nsManifestCheck <public>
//-----------------------------------------------------------------------------

/* static */
nsresult nsManifestCheck::ReadManifest(nsIInputStream* aInputStream,
                                       void* aClosure, const char* aFromSegment,
                                       uint32_t aOffset, uint32_t aCount,
                                       uint32_t* aBytesConsumed) {
  nsManifestCheck* manifestCheck = static_cast<nsManifestCheck*>(aClosure);

  nsresult rv;
  *aBytesConsumed = aCount;

  rv = manifestCheck->mManifestHash->Update(
      reinterpret_cast<const uint8_t*>(aFromSegment), aCount);
  NS_ENSURE_SUCCESS(rv, rv);

  return NS_OK;
}

//-----------------------------------------------------------------------------
// nsManifestCheck::nsIStreamListener
//-----------------------------------------------------------------------------

NS_IMETHODIMP
nsManifestCheck::OnStartRequest(nsIRequest* aRequest) { return NS_OK; }

NS_IMETHODIMP
nsManifestCheck::OnDataAvailable(nsIRequest* aRequest, nsIInputStream* aStream,
                                 uint64_t aOffset, uint32_t aCount) {
  uint32_t bytesRead;
  aStream->ReadSegments(ReadManifest, this, aCount, &bytesRead);
  return NS_OK;
}

NS_IMETHODIMP
nsManifestCheck::OnStopRequest(nsIRequest* aRequest, nsresult aStatus) {
  nsAutoCString manifestHash;
  if (NS_SUCCEEDED(aStatus)) {
    mManifestHash->Finish(true, manifestHash);
  }

  mUpdate->ManifestCheckCompleted(aStatus, manifestHash);

  return NS_OK;
}

//-----------------------------------------------------------------------------
// nsManifestCheck::nsIInterfaceRequestor
//-----------------------------------------------------------------------------

NS_IMETHODIMP
nsManifestCheck::GetInterface(const nsIID& aIID, void** aResult) {
  if (aIID.Equals(NS_GET_IID(nsIChannelEventSink))) {
    NS_ADDREF_THIS();
    *aResult = static_cast<nsIChannelEventSink*>(this);
    return NS_OK;
  }

  return NS_ERROR_NO_INTERFACE;
}

//-----------------------------------------------------------------------------
// nsManifestCheck::nsIChannelEventSink
//-----------------------------------------------------------------------------

NS_IMETHODIMP
nsManifestCheck::AsyncOnChannelRedirect(
    nsIChannel* aOldChannel, nsIChannel* aNewChannel, uint32_t aFlags,
    nsIAsyncVerifyRedirectCallback* callback) {
  // Redirects should cause the load (and therefore the update) to fail.
  if (aFlags & nsIChannelEventSink::REDIRECT_INTERNAL) {
    callback->OnRedirectVerifyCallback(NS_OK);
    return NS_OK;
  }

  LogToConsole("Manifest check failed because its response is a redirect");

  aOldChannel->Cancel(NS_ERROR_ABORT);
  return NS_ERROR_ABORT;
}

//-----------------------------------------------------------------------------
// nsOfflineCacheUpdateItem::nsISupports
//-----------------------------------------------------------------------------

NS_IMPL_ISUPPORTS(nsOfflineCacheUpdateItem, nsIRequestObserver,
                  nsIStreamListener, nsIRunnable, nsIInterfaceRequestor,
                  nsIChannelEventSink)

//-----------------------------------------------------------------------------
// nsOfflineCacheUpdateItem <public>
//-----------------------------------------------------------------------------

nsOfflineCacheUpdateItem::nsOfflineCacheUpdateItem(
    nsIURI* aURI, nsIURI* aReferrerURI, nsIPrincipal* aLoadingPrincipal,
    nsIApplicationCache* aApplicationCache,
    nsIApplicationCache* aPreviousApplicationCache, uint32_t type,
    uint32_t loadFlags)
    : mURI(aURI),
      mReferrerURI(aReferrerURI),
      mLoadingPrincipal(aLoadingPrincipal),
      mApplicationCache(aApplicationCache),
      mPreviousApplicationCache(aPreviousApplicationCache),
      mItemType(type),
      mLoadFlags(loadFlags),
      mChannel(nullptr),
      mState(LoadStatus::UNINITIALIZED),
      mBytesRead(0) {}

nsOfflineCacheUpdateItem::~nsOfflineCacheUpdateItem() {}

nsresult nsOfflineCacheUpdateItem::OpenChannel(nsOfflineCacheUpdate* aUpdate) {
  if (LOG_ENABLED()) {
    LOG(("%p: Opening channel for %s", this, mURI->GetSpecOrDefault().get()));
  }

  if (mUpdate) {
    // Holding a reference to the update means this item is already
    // in progress (has a channel, or is just in between OnStopRequest()
    // and its Run() call.  We must never open channel on this item again.
    LOG(("  %p is already running! ignoring", this));
    return NS_ERROR_ALREADY_OPENED;
  }

  nsresult rv = nsOfflineCacheUpdate::GetCacheKey(mURI, mCacheKey);
  NS_ENSURE_SUCCESS(rv, rv);

  uint32_t flags =
      nsIRequest::LOAD_BACKGROUND | nsICachingChannel::LOAD_ONLY_IF_MODIFIED;

  if (mApplicationCache == mPreviousApplicationCache) {
    // Same app cache to read from and to write to is used during
    // an only-update-check procedure.  Here we protect the existing
    // cache from being modified.
    flags |= nsIRequest::INHIBIT_CACHING;
  }

  flags |= mLoadFlags;

  rv = NS_NewChannel(getter_AddRefs(mChannel), mURI, mLoadingPrincipal,
                     nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
                     nsIContentPolicy::TYPE_OTHER,
                     nullptr,  // nsICookieSettings
                     nullptr,  // PerformanceStorage
                     nullptr,  // aLoadGroup
                     this,     // aCallbacks
                     flags);

  NS_ENSURE_SUCCESS(rv, rv);

  nsCOMPtr<nsIApplicationCacheChannel> appCacheChannel =
      do_QueryInterface(mChannel, &rv);

  // Support for nsIApplicationCacheChannel is required.
  NS_ENSURE_SUCCESS(rv, rv);

  // Use the existing application cache as the cache to check.
  rv = appCacheChannel->SetApplicationCache(mPreviousApplicationCache);
  NS_ENSURE_SUCCESS(rv, rv);

  // Set the new application cache as the target for write.
  rv = appCacheChannel->SetApplicationCacheForWrite(mApplicationCache);
  NS_ENSURE_SUCCESS(rv, rv);

  // configure HTTP specific stuff
  nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(mChannel);
  if (httpChannel) {
    nsCOMPtr<nsIReferrerInfo> referrerInfo =
        new mozilla::dom::ReferrerInfo(mReferrerURI);
    rv = httpChannel->SetReferrerInfoWithoutClone(referrerInfo);
    MOZ_ASSERT(NS_SUCCEEDED(rv));
    rv = httpChannel->SetRequestHeader(NS_LITERAL_CSTRING("X-Moz"),
                                       NS_LITERAL_CSTRING("offline-resource"),
                                       false);
    MOZ_ASSERT(NS_SUCCEEDED(rv));
  }

  rv = mChannel->AsyncOpen(this);
  NS_ENSURE_SUCCESS(rv, rv);

  mUpdate = aUpdate;

  mState = LoadStatus::REQUESTED;

  return NS_OK;
}

nsresult nsOfflineCacheUpdateItem::Cancel() {
  if (mChannel) {
    mChannel->Cancel(NS_ERROR_ABORT);
    mChannel = nullptr;
  }

  mState = LoadStatus::UNINITIALIZED;

  return NS_OK;
}

//-----------------------------------------------------------------------------
// nsOfflineCacheUpdateItem::nsIStreamListener
//-----------------------------------------------------------------------------

NS_IMETHODIMP
nsOfflineCacheUpdateItem::OnStartRequest(nsIRequest* aRequest) {
  mState = LoadStatus::RECEIVING;

  return NS_OK;
}

NS_IMETHODIMP
nsOfflineCacheUpdateItem::OnDataAvailable(nsIRequest* aRequest,
                                          nsIInputStream* aStream,
                                          uint64_t aOffset, uint32_t aCount) {
  uint32_t bytesRead = 0;
  aStream->ReadSegments(NS_DiscardSegment, nullptr, aCount, &bytesRead);
  mBytesRead += bytesRead;
  LOG(("loaded %u bytes into offline cache [offset=%" PRIu64 "]\n", bytesRead,
       aOffset));

  mUpdate->OnByteProgress(bytesRead);

  return NS_OK;
}

NS_IMETHODIMP
nsOfflineCacheUpdateItem::OnStopRequest(nsIRequest* aRequest,
                                        nsresult aStatus) {
  if (LOG_ENABLED()) {
    LOG(("%p: Done fetching offline item %s [status=%" PRIx32 "]\n", this,
         mURI->GetSpecOrDefault().get(), static_cast<uint32_t>(aStatus)));
  }

  if (mBytesRead == 0 && aStatus == NS_OK) {
    // we didn't need to read (because LOAD_ONLY_IF_MODIFIED was
    // specified), but the object should report loadedSize as if it
    // did.
    mChannel->GetContentLength(&mBytesRead);
    mUpdate->OnByteProgress(mBytesRead);
  }

  if (NS_FAILED(aStatus)) {
    nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(mChannel);
    if (httpChannel) {
      bool isNoStore;
      if (NS_SUCCEEDED(httpChannel->IsNoStoreResponse(&isNoStore)) &&
          isNoStore) {
        LogToConsole(
            "Offline cache manifest item has Cache-control: no-store header",
            this);
      }
    }
  }

  // We need to notify the update that the load is complete, but we
  // want to give the channel a chance to close the cache entries.
  NS_DispatchToCurrentThread(this);

  return NS_OK;
}

//-----------------------------------------------------------------------------
// nsOfflineCacheUpdateItem::nsIRunnable
//-----------------------------------------------------------------------------
NS_IMETHODIMP
nsOfflineCacheUpdateItem::Run() {
  // Set mState to LOADED here rather than in OnStopRequest to prevent
  // race condition when checking state of all mItems in ProcessNextURI().
  // If state would have been set in OnStopRequest we could mistakenly
  // take this item as already finished and finish the update process too
  // early when ProcessNextURI() would get called between OnStopRequest()
  // and Run() of this item.  Finish() would then have been called twice.
  mState = LoadStatus::LOADED;

  RefPtr<nsOfflineCacheUpdate> update;
  update.swap(mUpdate);
  update->LoadCompleted(this);

  return NS_OK;
}

//-----------------------------------------------------------------------------
// nsOfflineCacheUpdateItem::nsIInterfaceRequestor
//-----------------------------------------------------------------------------

NS_IMETHODIMP
nsOfflineCacheUpdateItem::GetInterface(const nsIID& aIID, void** aResult) {
  if (aIID.Equals(NS_GET_IID(nsIChannelEventSink))) {
    NS_ADDREF_THIS();
    *aResult = static_cast<nsIChannelEventSink*>(this);
    return NS_OK;
  }

  return NS_ERROR_NO_INTERFACE;
}

//-----------------------------------------------------------------------------
// nsOfflineCacheUpdateItem::nsIChannelEventSink
//-----------------------------------------------------------------------------

NS_IMETHODIMP
nsOfflineCacheUpdateItem::AsyncOnChannelRedirect(
    nsIChannel* aOldChannel, nsIChannel* aNewChannel, uint32_t aFlags,
    nsIAsyncVerifyRedirectCallback* cb) {
  if (!(aFlags & nsIChannelEventSink::REDIRECT_INTERNAL)) {
    // Don't allow redirect in case of non-internal redirect and cancel
    // the channel to clean the cache entry.
    LogToConsole("Offline cache manifest failed because an item redirects",
                 this);

    aOldChannel->Cancel(NS_ERROR_ABORT);
    return NS_ERROR_ABORT;
  }

  nsCOMPtr<nsIURI> newURI;
  nsresult rv = aNewChannel->GetURI(getter_AddRefs(newURI));
  if (NS_FAILED(rv)) return rv;

  nsCOMPtr<nsIApplicationCacheChannel> appCacheChannel =
      do_QueryInterface(aNewChannel);
  if (appCacheChannel) {
    rv = appCacheChannel->SetApplicationCacheForWrite(mApplicationCache);
    NS_ENSURE_SUCCESS(rv, rv);
  }

  nsAutoCString oldScheme;
  mURI->GetScheme(oldScheme);

  if (!newURI->SchemeIs(oldScheme.get())) {
    LOG(("rejected: redirected to a different scheme\n"));
    return NS_ERROR_ABORT;
  }

  // HTTP request headers are not automatically forwarded to the new channel.
  nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aNewChannel);
  NS_ENSURE_STATE(httpChannel);

  rv = httpChannel->SetRequestHeader(NS_LITERAL_CSTRING("X-Moz"),
                                     NS_LITERAL_CSTRING("offline-resource"),
                                     false);
  MOZ_ASSERT(NS_SUCCEEDED(rv));

  mChannel = aNewChannel;

  cb->OnRedirectVerifyCallback(NS_OK);
  return NS_OK;
}

nsresult nsOfflineCacheUpdateItem::GetRequestSucceeded(bool* succeeded) {
  *succeeded = false;

  if (!mChannel) return NS_OK;

  nsresult rv;
  nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(mChannel, &rv);
  NS_ENSURE_SUCCESS(rv, rv);

  bool reqSucceeded;
  rv = httpChannel->GetRequestSucceeded(&reqSucceeded);
  if (NS_ERROR_NOT_AVAILABLE == rv) return NS_OK;
  NS_ENSURE_SUCCESS(rv, rv);

  if (!reqSucceeded) {
    LOG(("Request failed"));
    return NS_OK;
  }

  nsresult channelStatus;
  rv = httpChannel->GetStatus(&channelStatus);
  NS_ENSURE_SUCCESS(rv, rv);

  if (NS_FAILED(channelStatus)) {
    LOG(("Channel status=0x%08" PRIx32, static_cast<uint32_t>(channelStatus)));
    return NS_OK;
  }

  *succeeded = true;
  return NS_OK;
}

bool nsOfflineCacheUpdateItem::IsScheduled() {
  return mState == LoadStatus::UNINITIALIZED;
}

bool nsOfflineCacheUpdateItem::IsInProgress() {
  return mState == LoadStatus::REQUESTED || mState == LoadStatus::RECEIVING;
}

bool nsOfflineCacheUpdateItem::IsCompleted() {
  return mState == LoadStatus::LOADED;
}

nsresult nsOfflineCacheUpdateItem::GetStatus(uint16_t* aStatus) {
  if (!mChannel) {
    *aStatus = 0;
    return NS_OK;
  }

  nsresult rv;
  nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(mChannel, &rv);
  NS_ENSURE_SUCCESS(rv, rv);

  uint32_t httpStatus;
  rv = httpChannel->GetResponseStatus(&httpStatus);
  if (rv == NS_ERROR_NOT_AVAILABLE) {
    *aStatus = 0;
    return NS_OK;
  }

  NS_ENSURE_SUCCESS(rv, rv);
  *aStatus = uint16_t(httpStatus);
  return NS_OK;
}

//-----------------------------------------------------------------------------
// nsOfflineManifestItem
//-----------------------------------------------------------------------------

//-----------------------------------------------------------------------------
// nsOfflineManifestItem <public>
//-----------------------------------------------------------------------------

nsOfflineManifestItem::nsOfflineManifestItem(
    nsIURI* aURI, nsIURI* aReferrerURI, nsIPrincipal* aLoadingPrincipal,
    nsIApplicationCache* aApplicationCache,
    nsIApplicationCache* aPreviousApplicationCache)
    : nsOfflineCacheUpdateItem(aURI, aReferrerURI, aLoadingPrincipal,
                               aApplicationCache, aPreviousApplicationCache,
                               nsIApplicationCache::ITEM_MANIFEST, 0),
      mParserState(PARSE_INIT),
      mNeedsUpdate(true),
      mStrictFileOriginPolicy(false),
      mManifestHashInitialized(false) {
  ReadStrictFileOriginPolicyPref();
}

nsOfflineManifestItem::~nsOfflineManifestItem() {}

//-----------------------------------------------------------------------------
// nsOfflineManifestItem <private>
//-----------------------------------------------------------------------------

/* static */
nsresult nsOfflineManifestItem::ReadManifest(nsIInputStream* aInputStream,
                                             void* aClosure,
                                             const char* aFromSegment,
                                             uint32_t aOffset, uint32_t aCount,
                                             uint32_t* aBytesConsumed) {
  nsOfflineManifestItem* manifest =
      static_cast<nsOfflineManifestItem*>(aClosure);

  nsresult rv;

  *aBytesConsumed = aCount;

  if (manifest->mParserState == PARSE_ERROR) {
    // parse already failed, ignore this
    return NS_OK;
  }

  if (!manifest->mManifestHashInitialized) {
    // Avoid re-creation of crypto hash when it fails from some reason the first
    // time
    manifest->mManifestHashInitialized = true;

    manifest->mManifestHash =
        do_CreateInstance("@mozilla.org/security/hash;1", &rv);
    if (NS_SUCCEEDED(rv)) {
      rv = manifest->mManifestHash->Init(nsICryptoHash::MD5);
      if (NS_FAILED(rv)) {
        manifest->mManifestHash = nullptr;
        LOG(
            ("Could not initialize manifest hash for byte-to-byte check, "
             "rv=%08" PRIx32,
             static_cast<uint32_t>(rv)));
      }
    }
  }

  if (manifest->mManifestHash) {
    rv = manifest->mManifestHash->Update(
        reinterpret_cast<const uint8_t*>(aFromSegment), aCount);
    if (NS_FAILED(rv)) {
      manifest->mManifestHash = nullptr;
      LOG(("Could not update manifest hash, rv=%08" PRIx32,
           static_cast<uint32_t>(rv)));
    }
  }

  manifest->mReadBuf.Append(aFromSegment, aCount);

  nsCString::const_iterator begin, iter, end;
  manifest->mReadBuf.BeginReading(begin);
  manifest->mReadBuf.EndReading(end);

  for (iter = begin; iter != end; iter++) {
    if (*iter == '\r' || *iter == '\n') {
      rv = manifest->HandleManifestLine(begin, iter);

      if (NS_FAILED(rv)) {
        LOG(("HandleManifestLine failed with 0x%08" PRIx32,
             static_cast<uint32_t>(rv)));
        *aBytesConsumed = 0;  // Avoid assertion failure in stream tee
        return NS_ERROR_ABORT;
      }

      begin = iter;
      begin++;
    }
  }

  // any leftovers are saved for next time
  manifest->mReadBuf = Substring(begin, end);

  return NS_OK;
}

nsresult nsOfflineManifestItem::AddNamespace(uint32_t namespaceType,
                                             const nsCString& namespaceSpec,
                                             const nsCString& data)

{
  nsresult rv;
  if (!mNamespaces) {
    mNamespaces = do_CreateInstance(NS_ARRAY_CONTRACTID, &rv);
    NS_ENSURE_SUCCESS(rv, rv);
  }

  nsCOMPtr<nsIApplicationCacheNamespace> ns = new nsApplicationCacheNamespace();

  rv = ns->Init(namespaceType, namespaceSpec, data);
  NS_ENSURE_SUCCESS(rv, rv);

  rv = mNamespaces->AppendElement(ns);
  NS_ENSURE_SUCCESS(rv, rv);

  return NS_OK;
}

static nsresult GetURIDirectory(nsIURI* uri, nsACString& directory) {
  nsresult rv;

  nsCOMPtr<nsIURL> url(do_QueryInterface(uri, &rv));
  NS_ENSURE_SUCCESS(rv, rv);

  rv = url->GetDirectory(directory);
  NS_ENSURE_SUCCESS(rv, rv);

  return NS_OK;
}

static nsresult CheckFileContainedInPath(nsIURI* file,
                                         nsACString const& masterDirectory) {
  nsresult rv;

  nsAutoCString directory;
  rv = GetURIDirectory(file, directory);
  NS_ENSURE_SUCCESS(rv, rv);

  bool contains = StringBeginsWith(directory, masterDirectory);
  if (!contains) {
    return NS_ERROR_DOM_BAD_URI;
  }

  return NS_OK;
}

nsresult nsOfflineManifestItem::HandleManifestLine(
    const nsCString::const_iterator& aBegin,
    const nsCString::const_iterator& aEnd) {
  nsCString::const_iterator begin = aBegin;
  nsCString::const_iterator end = aEnd;

  // all lines ignore trailing spaces and tabs
  nsCString::const_iterator last = end;
  --last;
  while (end != begin && (*last == ' ' || *last == '\t')) {
    --end;
    --last;
  }

  if (mParserState == PARSE_INIT) {
    // Allow a UTF-8 BOM
    if (begin != end && static_cast<unsigned char>(*begin) == 0xef) {
      if (++begin == end || static_cast<unsigned char>(*begin) != 0xbb ||
          ++begin == end || static_cast<unsigned char>(*begin) != 0xbf) {
        mParserState = PARSE_ERROR;
        LogToConsole("Offline cache manifest BOM error", this);
        return NS_OK;
      }
      ++begin;
    }

    const nsACString& magic = Substring(begin, end);

    if (!magic.EqualsLiteral("CACHE MANIFEST")) {
      mParserState = PARSE_ERROR;
      LogToConsole("Offline cache manifest magic incorrect", this);
      return NS_OK;
    }

    mParserState = PARSE_CACHE_ENTRIES;
    return NS_OK;
  }

  // lines other than the first ignore leading spaces and tabs
  while (begin != end && (*begin == ' ' || *begin == '\t')) begin++;

  // ignore blank lines and comments
  if (begin == end || *begin == '#') return NS_OK;

  const nsACString& line = Substring(begin, end);

  if (line.EqualsLiteral("CACHE:")) {
    mParserState = PARSE_CACHE_ENTRIES;
    return NS_OK;
  }

  if (line.EqualsLiteral("FALLBACK:")) {
    mParserState = PARSE_FALLBACK_ENTRIES;
    return NS_OK;
  }

  if (line.EqualsLiteral("NETWORK:")) {
    mParserState = PARSE_BYPASS_ENTRIES;
    return NS_OK;
  }

  // Every other section type we don't know must be silently ignored.
  nsCString::const_iterator lastChar = end;
  if (*(--lastChar) == ':') {
    mParserState = PARSE_UNKNOWN_SECTION;
    return NS_OK;
  }

  nsresult rv;

  switch (mParserState) {
    case PARSE_INIT:
    case PARSE_ERROR: {
      // this should have been dealt with earlier
      return NS_ERROR_FAILURE;
    }

    case PARSE_UNKNOWN_SECTION: {
      // just jump over
      return NS_OK;
    }

    case PARSE_CACHE_ENTRIES: {
      nsCOMPtr<nsIURI> uri;
      rv = NS_NewURI(getter_AddRefs(uri), line, nullptr, mURI);
      if (NS_FAILED(rv)) break;
      if (NS_FAILED(DropReferenceFromURL(uri))) break;

      nsAutoCString scheme;
      uri->GetScheme(scheme);

      // Manifest URIs must have the same scheme as the manifest.
      if (!mURI->SchemeIs(scheme.get())) {
        break;
      }

      mExplicitURIs.AppendObject(uri);

      if (!NS_SecurityCompareURIs(mURI, uri, mStrictFileOriginPolicy)) {
        mAnonymousURIs.AppendObject(uri);
      }

      break;
    }

    case PARSE_FALLBACK_ENTRIES: {
      int32_t separator = line.FindChar(' ');
      if (separator == kNotFound) {
        separator = line.FindChar('\t');
        if (separator == kNotFound) break;
      }

      nsCString namespaceSpec(Substring(line, 0, separator));
      nsCString fallbackSpec(Substring(line, separator + 1));
      namespaceSpec.CompressWhitespace();
      fallbackSpec.CompressWhitespace();

      nsCOMPtr<nsIURI> namespaceURI;
      rv =
          NS_NewURI(getter_AddRefs(namespaceURI), namespaceSpec, nullptr, mURI);
      if (NS_FAILED(rv)) break;
      if (NS_FAILED(DropReferenceFromURL(namespaceURI))) break;
      rv = namespaceURI->GetAsciiSpec(namespaceSpec);
      if (NS_FAILED(rv)) break;

      nsCOMPtr<nsIURI> fallbackURI;
      rv = NS_NewURI(getter_AddRefs(fallbackURI), fallbackSpec, nullptr, mURI);
      if (NS_FAILED(rv)) break;
      if (NS_FAILED(DropReferenceFromURL(fallbackURI))) break;
      rv = fallbackURI->GetAsciiSpec(fallbackSpec);
      if (NS_FAILED(rv)) break;

      // The following set of checks is preventing a website under
      // a subdirectory to add fallback pages for the whole origin
      // (or a parent directory) to prevent fallback attacks.
      nsAutoCString manifestDirectory;
      rv = GetURIDirectory(mURI, manifestDirectory);
      if (NS_FAILED(rv)) {
        break;
      }

      rv = CheckFileContainedInPath(namespaceURI, manifestDirectory);
      if (NS_FAILED(rv)) {
        break;
      }

      rv = CheckFileContainedInPath(fallbackURI, manifestDirectory);
      if (NS_FAILED(rv)) {
        break;
      }

      // Manifest and namespace must be same origin
      if (!NS_SecurityCompareURIs(mURI, namespaceURI, mStrictFileOriginPolicy))
        break;

      // Fallback and namespace must be same origin
      if (!NS_SecurityCompareURIs(namespaceURI, fallbackURI,
                                  mStrictFileOriginPolicy))
        break;

      mFallbackURIs.AppendObject(fallbackURI);

      AddNamespace(nsIApplicationCacheNamespace::NAMESPACE_FALLBACK,
                   namespaceSpec, fallbackSpec);
      break;
    }

    case PARSE_BYPASS_ENTRIES: {
      if (line[0] == '*' &&
          (line.Length() == 1 || line[1] == ' ' || line[1] == '\t')) {
        // '*' indicates to make the online whitelist wildcard flag open,
        // i.e. do allow load of resources not present in the offline cache
        // or not conforming any namespace.
        // We achive that simply by adding an 'empty' - i.e. universal
        // namespace of BYPASS type into the cache.
        AddNamespace(nsIApplicationCacheNamespace::NAMESPACE_BYPASS,
                     EmptyCString(), EmptyCString());
        break;
      }

      nsCOMPtr<nsIURI> bypassURI;
      rv = NS_NewURI(getter_AddRefs(bypassURI), line, nullptr, mURI);
      if (NS_FAILED(rv)) break;

      nsAutoCString scheme;
      bypassURI->GetScheme(scheme);
      if (!mURI->SchemeIs(scheme.get())) {
        break;
      }
      if (NS_FAILED(DropReferenceFromURL(bypassURI))) break;
      nsCString spec;
      if (NS_FAILED(bypassURI->GetAsciiSpec(spec))) break;

      AddNamespace(nsIApplicationCacheNamespace::NAMESPACE_BYPASS, spec,
                   EmptyCString());
      break;
    }
  }

  return NS_OK;
}

nsresult nsOfflineManifestItem::GetOldManifestContentHash(
    nsIRequest* aRequest) {
  nsresult rv;

  nsCOMPtr<nsICachingChannel> cachingChannel = do_QueryInterface(aRequest, &rv);
  NS_ENSURE_SUCCESS(rv, rv);

  // load the main cache token that is actually the old offline cache token and
  // read previous manifest content hash value
  nsCOMPtr<nsISupports> cacheToken;
  cachingChannel->GetCacheToken(getter_AddRefs(cacheToken));
  if (cacheToken) {
    nsCOMPtr<nsICacheEntry> cacheDescriptor(do_QueryInterface(cacheToken, &rv));
    NS_ENSURE_SUCCESS(rv, rv);

    rv = cacheDescriptor->GetMetaDataElement(
        "offline-manifest-hash", getter_Copies(mOldManifestHashValue));
    if (NS_FAILED(rv)) mOldManifestHashValue.Truncate();
  }

  return NS_OK;
}

nsresult nsOfflineManifestItem::CheckNewManifestContentHash(
    nsIRequest* aRequest) {
  nsresult rv;

  if (!mManifestHash) {
    // Nothing to compare against...
    return NS_OK;
  }

  nsCString newManifestHashValue;
  rv = mManifestHash->Finish(true, mManifestHashValue);
  mManifestHash = nullptr;

  if (NS_FAILED(rv)) {
    LOG(("Could not finish manifest hash, rv=%08" PRIx32,
         static_cast<uint32_t>(rv)));
    // This is not critical error
    return NS_OK;
  }

  if (!ParseSucceeded()) {
    // Parsing failed, the hash is not valid
    return NS_OK;
  }

  if (mOldManifestHashValue == mManifestHashValue) {
    LOG(
        ("Update not needed, downloaded manifest content is byte-for-byte "
         "identical"));
    mNeedsUpdate = false;
  }

  // Store the manifest content hash value to the new
  // offline cache token
  nsCOMPtr<nsICachingChannel> cachingChannel = do_QueryInterface(aRequest, &rv);
  NS_ENSURE_SUCCESS(rv, rv);

  nsCOMPtr<nsISupports> cacheToken;
  cachingChannel->GetOfflineCacheToken(getter_AddRefs(cacheToken));
  if (cacheToken) {
    nsCOMPtr<nsICacheEntry> cacheDescriptor(do_QueryInterface(cacheToken, &rv));
    NS_ENSURE_SUCCESS(rv, rv);

    rv = cacheDescriptor->SetMetaDataElement("offline-manifest-hash",
                                             mManifestHashValue.get());
    NS_ENSURE_SUCCESS(rv, rv);
  }

  return NS_OK;
}

void nsOfflineManifestItem::ReadStrictFileOriginPolicyPref() {
  mStrictFileOriginPolicy =
      Preferences::GetBool("security.fileuri.strict_origin_policy", true);
}

NS_IMETHODIMP
nsOfflineManifestItem::OnStartRequest(nsIRequest* aRequest) {
  nsresult rv;

  nsCOMPtr<nsIHttpChannel> channel = do_QueryInterface(aRequest, &rv);
  NS_ENSURE_SUCCESS(rv, rv);

  bool succeeded;
  rv = channel->GetRequestSucceeded(&succeeded);
  NS_ENSURE_SUCCESS(rv, rv);

  if (!succeeded) {
    LOG(("HTTP request failed"));
    LogToConsole("Offline cache manifest HTTP request failed", this);
    mParserState = PARSE_ERROR;
    return NS_ERROR_ABORT;
  }

  rv = GetOldManifestContentHash(aRequest);
  NS_ENSURE_SUCCESS(rv, rv);

  return nsOfflineCacheUpdateItem::OnStartRequest(aRequest);
}

NS_IMETHODIMP
nsOfflineManifestItem::OnDataAvailable(nsIRequest* aRequest,
                                       nsIInputStream* aStream,
                                       uint64_t aOffset, uint32_t aCount) {
  uint32_t bytesRead = 0;
  aStream->ReadSegments(ReadManifest, this, aCount, &bytesRead);
  mBytesRead += bytesRead;

  if (mParserState == PARSE_ERROR) {
    LOG(("OnDataAvailable is canceling the request due a parse error\n"));
    return NS_ERROR_ABORT;
  }

  LOG(("loaded %u bytes into offline cache [offset=%" PRIu64 "]\n", bytesRead,
       aOffset));

  // All the parent method does is read and discard, don't bother
  // chaining up.

  return NS_OK;
}

NS_IMETHODIMP
nsOfflineManifestItem::OnStopRequest(nsIRequest* aRequest, nsresult aStatus) {
  if (mBytesRead == 0) {
    // We didn't need to read (because LOAD_ONLY_IF_MODIFIED was
    // specified).
    mNeedsUpdate = false;
  } else {
    // Handle any leftover manifest data.
    nsCString::const_iterator begin, end;
    mReadBuf.BeginReading(begin);
    mReadBuf.EndReading(end);
    nsresult rv = HandleManifestLine(begin, end);
    NS_ENSURE_SUCCESS(rv, rv);

    rv = CheckNewManifestContentHash(aRequest);
    NS_ENSURE_SUCCESS(rv, rv);
  }

  return nsOfflineCacheUpdateItem::OnStopRequest(aRequest, aStatus);
}

//-----------------------------------------------------------------------------
// nsOfflineCacheUpdate::nsISupports
//-----------------------------------------------------------------------------

NS_IMPL_ISUPPORTS(nsOfflineCacheUpdate, nsIOfflineCacheUpdateObserver,
                  nsIOfflineCacheUpdate, nsIRunnable)

//-----------------------------------------------------------------------------
// nsOfflineCacheUpdate <public>
//-----------------------------------------------------------------------------

nsOfflineCacheUpdate::nsOfflineCacheUpdate()
    : mState(STATE_UNINITIALIZED),
      mAddedItems(false),
      mPartialUpdate(false),
      mOnlyCheckUpdate(false),
      mSucceeded(true),
      mObsolete(false),
      mItemsInProgress(0),
      mRescheduleCount(0),
      mPinnedEntryRetriesCount(0),
      mPinned(false),
      mByteProgress(0) {}

nsOfflineCacheUpdate::~nsOfflineCacheUpdate() {
  LOG(("nsOfflineCacheUpdate::~nsOfflineCacheUpdate [%p]", this));
}

/* static */
nsresult nsOfflineCacheUpdate::GetCacheKey(nsIURI* aURI, nsACString& aKey) {
  aKey.Truncate();

  nsCOMPtr<nsIURI> newURI;
  nsresult rv = NS_GetURIWithoutRef(aURI, getter_AddRefs(newURI));
  NS_ENSURE_SUCCESS(rv, rv);

  rv = newURI->GetAsciiSpec(aKey);
  NS_ENSURE_SUCCESS(rv, rv);

  return NS_OK;
}

nsresult nsOfflineCacheUpdate::InitInternal(nsIURI* aManifestURI,
                                            nsIPrincipal* aLoadingPrincipal) {
  nsresult rv;

  // Only http and https applications are supported.
  if (!aManifestURI->SchemeIs("http") && !aManifestURI->SchemeIs("https")) {
    return NS_ERROR_ABORT;
  }

  mManifestURI = aManifestURI;
  mLoadingPrincipal = aLoadingPrincipal;

  rv = mManifestURI->GetAsciiHost(mUpdateDomain);
  NS_ENSURE_SUCCESS(rv, rv);

  mPartialUpdate = false;

  return NS_OK;
}

nsresult nsOfflineCacheUpdate::Init(nsIURI* aManifestURI, nsIURI* aDocumentURI,
                                    nsIPrincipal* aLoadingPrincipal,
                                    dom::Document* aDocument,
                                    nsIFile* aCustomProfileDir) {
  nsresult rv;

  // Make sure the service has been initialized
  nsOfflineCacheUpdateService* service =
      nsOfflineCacheUpdateService::EnsureService();
  if (!service) return NS_ERROR_FAILURE;

  LOG(("nsOfflineCacheUpdate::Init [%p]", this));

  rv = InitInternal(aManifestURI, aLoadingPrincipal);
  NS_ENSURE_SUCCESS(rv, rv);

  nsCOMPtr<nsIApplicationCacheService> cacheService =
      do_GetService(NS_APPLICATIONCACHESERVICE_CONTRACTID, &rv);
  NS_ENSURE_SUCCESS(rv, rv);

  nsAutoCString originSuffix;
  rv = aLoadingPrincipal->GetOriginSuffix(originSuffix);
  NS_ENSURE_SUCCESS(rv, rv);

  mDocumentURI = aDocumentURI;

  if (aCustomProfileDir) {
    rv = cacheService->BuildGroupIDForSuffix(aManifestURI, originSuffix,
                                             mGroupID);
    NS_ENSURE_SUCCESS(rv, rv);

    // Create only a new offline application cache in the custom profile
    // This is a preload of a new cache.

    // XXX Custom updates don't support "updating" of an existing cache
    // in the custom profile at the moment.  This support can be, though,
    // simply added as well when needed.
    mPreviousApplicationCache = nullptr;

    rv = cacheService->CreateCustomApplicationCache(
        mGroupID, aCustomProfileDir, kCustomProfileQuota,
        getter_AddRefs(mApplicationCache));
    NS_ENSURE_SUCCESS(rv, rv);

    mCustomProfileDir = aCustomProfileDir;
  } else {
    rv = cacheService->BuildGroupIDForSuffix(aManifestURI, originSuffix,
                                             mGroupID);
    NS_ENSURE_SUCCESS(rv, rv);

    rv = cacheService->GetActiveCache(
        mGroupID, getter_AddRefs(mPreviousApplicationCache));
    NS_ENSURE_SUCCESS(rv, rv);

    rv = cacheService->CreateApplicationCache(
        mGroupID, getter_AddRefs(mApplicationCache));
    NS_ENSURE_SUCCESS(rv, rv);
  }

  rv = nsOfflineCacheUpdateService::OfflineAppPinnedForURI(aDocumentURI,
                                                           &mPinned);
  NS_ENSURE_SUCCESS(rv, rv);

  mState = STATE_INITIALIZED;
  return NS_OK;
}

nsresult nsOfflineCacheUpdate::InitForUpdateCheck(
    nsIURI* aManifestURI, nsIPrincipal* aLoadingPrincipal,
    nsIObserver* aObserver) {
  nsresult rv;

  // Make sure the service has been initialized
  nsOfflineCacheUpdateService* service =
      nsOfflineCacheUpdateService::EnsureService();
  if (!service) return NS_ERROR_FAILURE;

  LOG(("nsOfflineCacheUpdate::InitForUpdateCheck [%p]", this));

  rv = InitInternal(aManifestURI, aLoadingPrincipal);
  NS_ENSURE_SUCCESS(rv, rv);

  nsCOMPtr<nsIApplicationCacheService> cacheService =
      do_GetService(NS_APPLICATIONCACHESERVICE_CONTRACTID, &rv);
  NS_ENSURE_SUCCESS(rv, rv);

  nsAutoCString originSuffix;
  rv = aLoadingPrincipal->GetOriginSuffix(originSuffix);
  NS_ENSURE_SUCCESS(rv, rv);

  rv =
      cacheService->BuildGroupIDForSuffix(aManifestURI, originSuffix, mGroupID);
  NS_ENSURE_SUCCESS(rv, rv);

  rv = cacheService->GetActiveCache(mGroupID,
                                    getter_AddRefs(mPreviousApplicationCache));
  NS_ENSURE_SUCCESS(rv, rv);

  // To load the manifest properly using current app cache to satisfy and
  // also to compare the cached content hash value we have to set 'some'
  // app cache to write to on the channel.  Otherwise the cached version will
  // be used and no actual network request will be made.  We use the same
  // app cache here.  OpenChannel prevents caching in this case using
  // INHIBIT_CACHING load flag.
  mApplicationCache = mPreviousApplicationCache;

  rv = nsOfflineCacheUpdateService::OfflineAppPinnedForURI(aManifestURI,
                                                           &mPinned);
  NS_ENSURE_SUCCESS(rv, rv);

  mUpdateAvailableObserver = aObserver;
  mOnlyCheckUpdate = true;

  mState = STATE_INITIALIZED;
  return NS_OK;
}

nsresult nsOfflineCacheUpdate::InitPartial(nsIURI* aManifestURI,
                                           const nsACString& clientID,
                                           nsIURI* aDocumentURI,
                                           nsIPrincipal* aLoadingPrincipal) {
  nsresult rv;

  // Make sure the service has been initialized
  nsOfflineCacheUpdateService* service =
      nsOfflineCacheUpdateService::EnsureService();
  if (!service) return NS_ERROR_FAILURE;

  LOG(("nsOfflineCacheUpdate::InitPartial [%p]", this));

  mPartialUpdate = true;
  mDocumentURI = aDocumentURI;
  mLoadingPrincipal = aLoadingPrincipal;

  mManifestURI = aManifestURI;
  rv = mManifestURI->GetAsciiHost(mUpdateDomain);
  NS_ENSURE_SUCCESS(rv, rv);

  nsCOMPtr<nsIApplicationCacheService> cacheService =
      do_GetService(NS_APPLICATIONCACHESERVICE_CONTRACTID, &rv);
  NS_ENSURE_SUCCESS(rv, rv);

  rv = cacheService->GetApplicationCache(clientID,
                                         getter_AddRefs(mApplicationCache));
  NS_ENSURE_SUCCESS(rv, rv);

  if (!mApplicationCache) {
    nsAutoCString manifestSpec;
    rv = GetCacheKey(mManifestURI, manifestSpec);
    NS_ENSURE_SUCCESS(rv, rv);

    rv = cacheService->CreateApplicationCache(
        manifestSpec, getter_AddRefs(mApplicationCache));
    NS_ENSURE_SUCCESS(rv, rv);
  }

  rv = mApplicationCache->GetManifestURI(getter_AddRefs(mManifestURI));
  NS_ENSURE_SUCCESS(rv, rv);

  nsAutoCString groupID;
  rv = mApplicationCache->GetGroupID(groupID);
  NS_ENSURE_SUCCESS(rv, rv);

  rv = nsOfflineCacheUpdateService::OfflineAppPinnedForURI(aDocumentURI,
                                                           &mPinned);
  NS_ENSURE_SUCCESS(rv, rv);

  mState = STATE_INITIALIZED;
  return NS_OK;
}

nsresult nsOfflineCacheUpdate::HandleManifest(bool* aDoUpdate) {
  // Be pessimistic
  *aDoUpdate = false;

  bool succeeded;
  nsresult rv = mManifestItem->GetRequestSucceeded(&succeeded);
  NS_ENSURE_SUCCESS(rv, rv);

  if (!succeeded || !mManifestItem->ParseSucceeded()) {
    return NS_ERROR_FAILURE;
  }

  if (!mManifestItem->NeedsUpdate()) {
    return NS_OK;
  }

  // Add items requested by the manifest.
  const nsCOMArray<nsIURI>& manifestURIs = mManifestItem->GetExplicitURIs();
  for (int32_t i = 0; i < manifestURIs.Count(); i++) {
    rv = AddURI(manifestURIs[i], nsIApplicationCache::ITEM_EXPLICIT);
    NS_ENSURE_SUCCESS(rv, rv);
  }

  const nsCOMArray<nsIURI>& anonURIs = mManifestItem->GetAnonymousURIs();
  for (int32_t i = 0; i < anonURIs.Count(); i++) {
    rv = AddURI(anonURIs[i], nsIApplicationCache::ITEM_EXPLICIT,
                nsIRequest::LOAD_ANONYMOUS);
    NS_ENSURE_SUCCESS(rv, rv);
  }

  const nsCOMArray<nsIURI>& fallbackURIs = mManifestItem->GetFallbackURIs();
  for (int32_t i = 0; i < fallbackURIs.Count(); i++) {
    rv = AddURI(fallbackURIs[i], nsIApplicationCache::ITEM_FALLBACK);
    NS_ENSURE_SUCCESS(rv, rv);
  }

  // The document that requested the manifest is implicitly included
  // as part of that manifest update.
  rv = AddURI(mDocumentURI, nsIApplicationCache::ITEM_IMPLICIT);
  NS_ENSURE_SUCCESS(rv, rv);

  // Add items previously cached implicitly
  rv = AddExistingItems(nsIApplicationCache::ITEM_IMPLICIT);
  NS_ENSURE_SUCCESS(rv, rv);

  // Add items requested by the script API
  rv = AddExistingItems(nsIApplicationCache::ITEM_DYNAMIC);
  NS_ENSURE_SUCCESS(rv, rv);

  // Add opportunistically cached items conforming current opportunistic
  // namespace list
  rv = AddExistingItems(nsIApplicationCache::ITEM_OPPORTUNISTIC,
                        &mManifestItem->GetOpportunisticNamespaces());
  NS_ENSURE_SUCCESS(rv, rv);

  *aDoUpdate = true;

  return NS_OK;
}

bool nsOfflineCacheUpdate::CheckUpdateAvailability() {
  nsresult rv;

  bool succeeded;
  rv = mManifestItem->GetRequestSucceeded(&succeeded);
  NS_ENSURE_SUCCESS(rv, false);

  if (!succeeded || !mManifestItem->ParseSucceeded()) {
    return false;
  }

  if (!mPinned) {
    uint16_t status;
    rv = mManifestItem->GetStatus(&status);
    NS_ENSURE_SUCCESS(rv, false);

    // Treat these as there would be an update available,
    // since this is indication of demand to remove this
    // offline cache.
    if (status == 404 || status == 410) {
      return true;
    }
  }

  return mManifestItem->NeedsUpdate();
}

void nsOfflineCacheUpdate::LoadCompleted(nsOfflineCacheUpdateItem* aItem) {
  nsresult rv;

  LOG(("nsOfflineCacheUpdate::LoadCompleted [%p]", this));

  if (mState == STATE_FINISHED) {
    LOG(("  after completion, ignoring"));
    return;
  }

  // Keep the object alive through a Finish() call.
  nsCOMPtr<nsIOfflineCacheUpdate> kungFuDeathGrip(this);

  if (mState == STATE_CANCELLED) {
    NotifyState(nsIOfflineCacheUpdateObserver::STATE_ERROR);
    Finish();
    return;
  }

  if (mState == STATE_CHECKING) {
    // Manifest load finished.

    if (mOnlyCheckUpdate) {
      Finish();
      NotifyUpdateAvailability(CheckUpdateAvailability());
      return;
    }

    NS_ASSERTION(mManifestItem, "Must have a manifest item in STATE_CHECKING.");
    NS_ASSERTION(mManifestItem == aItem,
                 "Unexpected aItem in nsOfflineCacheUpdate::LoadCompleted");

    // A 404 or 410 is interpreted as an intentional removal of
    // the manifest file, rather than a transient server error.
    // Obsolete this cache group if one of these is returned.
    uint16_t status;
    rv = mManifestItem->GetStatus(&status);
    if (status == 404 || status == 410) {
      LogToConsole("Offline cache manifest removed, cache cleared",
                   mManifestItem);
      mSucceeded = false;
      if (mPreviousApplicationCache) {
        if (mPinned) {
          // Do not obsolete a pinned application.
          NotifyState(nsIOfflineCacheUpdateObserver::STATE_NOUPDATE);
        } else {
          NotifyState(nsIOfflineCacheUpdateObserver::STATE_OBSOLETE);
          mObsolete = true;
        }
      } else {
        NotifyState(nsIOfflineCacheUpdateObserver::STATE_ERROR);
        mObsolete = true;
      }
      Finish();
      return;
    }

    bool doUpdate;
    if (NS_FAILED(HandleManifest(&doUpdate))) {
      mSucceeded = false;
      NotifyState(nsIOfflineCacheUpdateObserver::STATE_ERROR);
      Finish();
      return;
    }

    if (!doUpdate) {
      LogToConsole("Offline cache doesn't need to update", mManifestItem);

      mSucceeded = false;

      AssociateDocuments(mPreviousApplicationCache);

      ScheduleImplicit();

      // If we didn't need an implicit update, we can
      // send noupdate and end the update now.
      if (!mImplicitUpdate) {
        NotifyState(nsIOfflineCacheUpdateObserver::STATE_NOUPDATE);
        Finish();
      }
      return;
    }

    rv = mApplicationCache->MarkEntry(mManifestItem->mCacheKey,
                                      mManifestItem->mItemType);
    if (NS_FAILED(rv)) {
      mSucceeded = false;
      NotifyState(nsIOfflineCacheUpdateObserver::STATE_ERROR);
      Finish();
      return;
    }

    mState = STATE_DOWNLOADING;
    NotifyState(nsIOfflineCacheUpdateObserver::STATE_DOWNLOADING);

    // Start fetching resources.
    ProcessNextURI();

    return;
  }

  // Normal load finished.
  if (mItemsInProgress)  // Just to be safe here!
    --mItemsInProgress;

  bool succeeded;
  rv = aItem->GetRequestSucceeded(&succeeded);

  if (mPinned && NS_SUCCEEDED(rv) && succeeded) {
    uint32_t dummy_cache_type;
    rv = mApplicationCache->GetTypes(aItem->mCacheKey, &dummy_cache_type);
    bool item_doomed = NS_FAILED(rv);  // can not find it? -> doomed

    if (item_doomed && mPinnedEntryRetriesCount < kPinnedEntryRetriesLimit &&
        (aItem->mItemType & (nsIApplicationCache::ITEM_EXPLICIT |
                             nsIApplicationCache::ITEM_FALLBACK))) {
      rv = EvictOneNonPinned();
      if (NS_FAILED(rv)) {
        mSucceeded = false;
        NotifyState(nsIOfflineCacheUpdateObserver::STATE_ERROR);
        Finish();
        return;
      }

      // This reverts the item state to UNINITIALIZED that makes it to
      // be scheduled for download again.
      rv = aItem->Cancel();
      if (NS_FAILED(rv)) {
        mSucceeded = false;
        NotifyState(nsIOfflineCacheUpdateObserver::STATE_ERROR);
        Finish();
        return;
      }

      mPinnedEntryRetriesCount++;

      LogToConsole("An unpinned offline cache deleted");

      // Retry this item.
      ProcessNextURI();
      return;
    }
  }

  // According to parallelism this may imply more pinned retries count,
  // but that is not critical, since at one moment the algorithm will
  // stop anyway.  Also, this code may soon be completely removed
  // after we have a separate storage for pinned apps.
  mPinnedEntryRetriesCount = 0;

  // Check for failures.  3XX, 4XX and 5XX errors on items explicitly
  // listed in the manifest will cause the update to fail.
  if (NS_FAILED(rv) || !succeeded) {
    if (aItem->mItemType & (nsIApplicationCache::ITEM_EXPLICIT |
                            nsIApplicationCache::ITEM_FALLBACK)) {
      LogToConsole("Offline cache manifest item failed to load", aItem);
      mSucceeded = false;
    }
  } else {
    rv = mApplicationCache->MarkEntry(aItem->mCacheKey, aItem->mItemType);
    if (NS_FAILED(rv)) {
      mSucceeded = false;
    }
  }

  if (!mSucceeded) {
    NotifyState(nsIOfflineCacheUpdateObserver::STATE_ERROR);
    Finish();
    return;
  }

  NotifyState(nsIOfflineCacheUpdateObserver::STATE_ITEMCOMPLETED);

  ProcessNextURI();
}

void nsOfflineCacheUpdate::ManifestCheckCompleted(
    nsresult aStatus, const nsCString& aManifestHash) {
  // Keep the object alive through a Finish() call.
  nsCOMPtr<nsIOfflineCacheUpdate> kungFuDeathGrip(this);

  if (NS_SUCCEEDED(aStatus)) {
    nsAutoCString firstManifestHash;
    mManifestItem->GetManifestHash(firstManifestHash);
    if (aManifestHash != firstManifestHash) {
      LOG(("Manifest has changed during cache items download [%p]", this));
      LogToConsole("Offline cache manifest changed during update",
                   mManifestItem);
      aStatus = NS_ERROR_FAILURE;
    }
  }

  if (NS_FAILED(aStatus)) {
    mSucceeded = false;
    NotifyState(nsIOfflineCacheUpdateObserver::STATE_ERROR);
  }

  if (NS_FAILED(aStatus) && mRescheduleCount < kRescheduleLimit) {
    // Do the final stuff but prevent notification of STATE_FINISHED.
    // That would disconnect listeners that are responsible for document
    // association after a successful update. Forwarding notifications
    // from a new update through this dead update to them is absolutely
    // correct.
    FinishNoNotify();

    RefPtr<nsOfflineCacheUpdate> newUpdate = new nsOfflineCacheUpdate();
    // Leave aDocument argument null. Only glues and children keep
    // document instances.
    newUpdate->Init(mManifestURI, mDocumentURI, mLoadingPrincipal, nullptr,
                    mCustomProfileDir);

    // In a rare case the manifest will not be modified on the next refetch
    // transfer all master document URIs to the new update to ensure that
    // all documents refering it will be properly cached.
    for (int32_t i = 0; i < mDocumentURIs.Count(); i++) {
      newUpdate->StickDocument(mDocumentURIs[i]);
    }

    newUpdate->mRescheduleCount = mRescheduleCount + 1;
    newUpdate->AddObserver(this, false);
    newUpdate->Schedule();
  } else {
    LogToConsole("Offline cache update done", mManifestItem);
    Finish();
  }
}

nsresult nsOfflineCacheUpdate::Begin() {
  LOG(("nsOfflineCacheUpdate::Begin [%p]", this));

  // Keep the object alive through a ProcessNextURI()/Finish() call.
  nsCOMPtr<nsIOfflineCacheUpdate> kungFuDeathGrip(this);

  mItemsInProgress = 0;

  if (mState == STATE_CANCELLED) {
    nsresult rv = NS_DispatchToMainThread(
        NewRunnableMethod("nsOfflineCacheUpdate::AsyncFinishWithError", this,
                          &nsOfflineCacheUpdate::AsyncFinishWithError));
    NS_ENSURE_SUCCESS(rv, rv);

    return NS_OK;
  }

  if (mPartialUpdate) {
    mState = STATE_DOWNLOADING;
    NotifyState(nsIOfflineCacheUpdateObserver::STATE_DOWNLOADING);
    ProcessNextURI();
    return NS_OK;
  }

  // Start checking the manifest.
  mManifestItem =
      new nsOfflineManifestItem(mManifestURI, mDocumentURI, mLoadingPrincipal,
                                mApplicationCache, mPreviousApplicationCache);
  if (!mManifestItem) {
    return NS_ERROR_OUT_OF_MEMORY;
  }

  mState = STATE_CHECKING;
  mByteProgress = 0;
  NotifyState(nsIOfflineCacheUpdateObserver::STATE_CHECKING);

  nsresult rv = mManifestItem->OpenChannel(this);
  if (NS_FAILED(rv)) {
    LoadCompleted(mManifestItem);
  }

  return NS_OK;
}

//-----------------------------------------------------------------------------
// nsOfflineCacheUpdate <private>
//-----------------------------------------------------------------------------

nsresult nsOfflineCacheUpdate::AddExistingItems(
    uint32_t aType, nsTArray<nsCString>* namespaceFilter) {
  if (!mPreviousApplicationCache) {
    return NS_OK;
  }

  if (namespaceFilter && namespaceFilter->Length() == 0) {
    // Don't bother to walk entries when there are no namespaces
    // defined.
    return NS_OK;
  }

  nsTArray<nsCString> keys;
  nsresult rv = mPreviousApplicationCache->GatherEntries(aType, keys);
  NS_ENSURE_SUCCESS(rv, rv);

  for (auto& key : keys) {
    if (namespaceFilter) {
      bool found = false;
      for (uint32_t j = 0; j < namespaceFilter->Length() && !found; j++) {
        found = StringBeginsWith(key, namespaceFilter->ElementAt(j));
      }

      if (!found) continue;
    }

    nsCOMPtr<nsIURI> uri;
    if (NS_SUCCEEDED(NS_NewURI(getter_AddRefs(uri), key))) {
      rv = AddURI(uri, aType);
      NS_ENSURE_SUCCESS(rv, rv);
    }
  }

  return NS_OK;
}

nsresult nsOfflineCacheUpdate::ProcessNextURI() {
  // Keep the object alive through a Finish() call.
  nsCOMPtr<nsIOfflineCacheUpdate> kungFuDeathGrip(this);

  LOG(("nsOfflineCacheUpdate::ProcessNextURI [%p, inprogress=%d, numItems=%zu]",
       this, mItemsInProgress, mItems.Length()));

  if (mState != STATE_DOWNLOADING) {
    LOG(("  should only be called from the DOWNLOADING state, ignoring"));
    return NS_ERROR_UNEXPECTED;
  }

  nsOfflineCacheUpdateItem* runItem = nullptr;
  uint32_t completedItems = 0;
  for (uint32_t i = 0; i < mItems.Length(); ++i) {
    nsOfflineCacheUpdateItem* item = mItems[i];

    if (item->IsScheduled()) {
      runItem = item;
      break;
    }

    if (item->IsCompleted()) ++completedItems;
  }

  if (completedItems == mItems.Length()) {
    LOG(("nsOfflineCacheUpdate::ProcessNextURI [%p]: all items loaded", this));

    if (mPartialUpdate) {
      return Finish();
    } else {
      // Verify that the manifest wasn't changed during the
      // update, to prevent capturing a cache while the server
      // is being updated.  The check will call
      // ManifestCheckCompleted() when it's done.
      RefPtr<nsManifestCheck> manifestCheck = new nsManifestCheck(
          this, mManifestURI, mDocumentURI, mLoadingPrincipal);
      if (NS_FAILED(manifestCheck->Begin())) {
        mSucceeded = false;
        NotifyState(nsIOfflineCacheUpdateObserver::STATE_ERROR);
        return Finish();
      }

      return NS_OK;
    }
  }

  if (!runItem) {
    LOG(
        ("nsOfflineCacheUpdate::ProcessNextURI [%p]:"
         " No more items to include in parallel load",
         this));
    return NS_OK;
  }

  if (LOG_ENABLED()) {
    LOG(("%p: Opening channel for %s", this,
         runItem->mURI->GetSpecOrDefault().get()));
  }

  ++mItemsInProgress;
  NotifyState(nsIOfflineCacheUpdateObserver::STATE_ITEMSTARTED);

  nsresult rv = runItem->OpenChannel(this);
  if (NS_FAILED(rv)) {
    LoadCompleted(runItem);
    return rv;
  }

  if (mItemsInProgress >= kParallelLoadLimit) {
    LOG(
        ("nsOfflineCacheUpdate::ProcessNextURI [%p]:"
         " At parallel load limit",
         this));
    return NS_OK;
  }

  // This calls this method again via a post triggering
  // a parallel item load
  return NS_DispatchToCurrentThread(this);
}

void nsOfflineCacheUpdate::GatherObservers(
    nsCOMArray<nsIOfflineCacheUpdateObserver>& aObservers) {
  for (int32_t i = 0; i < mWeakObservers.Count(); i++) {
    nsCOMPtr<nsIOfflineCacheUpdateObserver> observer =
        do_QueryReferent(mWeakObservers[i]);
    if (observer)
      aObservers.AppendObject(observer);
    else
      mWeakObservers.RemoveObjectAt(i--);
  }

  for (int32_t i = 0; i < mObservers.Count(); i++) {
    aObservers.AppendObject(mObservers[i]);
  }
}

void nsOfflineCacheUpdate::NotifyState(uint32_t state) {
  LOG(("nsOfflineCacheUpdate::NotifyState [%p, %d]", this, state));

  if (state == STATE_ERROR) {
    LogToConsole("Offline cache update error", mManifestItem);
  }

  nsCOMArray<nsIOfflineCacheUpdateObserver> observers;
  GatherObservers(observers);

  for (int32_t i = 0; i < observers.Count(); i++) {
    observers[i]->UpdateStateChanged(this, state);
  }
}

void nsOfflineCacheUpdate::NotifyUpdateAvailability(bool updateAvailable) {
  if (!mUpdateAvailableObserver) return;

  LOG(("nsOfflineCacheUpdate::NotifyUpdateAvailability [this=%p, avail=%d]",
       this, updateAvailable));

  const char* topic = updateAvailable ? "offline-cache-update-available"
                                      : "offline-cache-update-unavailable";

  nsCOMPtr<nsIObserver> observer;
  observer.swap(mUpdateAvailableObserver);
  observer->Observe(mManifestURI, topic, nullptr);
}

void nsOfflineCacheUpdate::AssociateDocuments(nsIApplicationCache* cache) {
  if (!cache) {
    LOG(
        ("nsOfflineCacheUpdate::AssociateDocuments bypassed"
         ", no cache provided [this=%p]",
         this));
    return;
  }

  nsCOMArray<nsIOfflineCacheUpdateObserver> observers;
  GatherObservers(observers);

  for (int32_t i = 0; i < observers.Count(); i++) {
    observers[i]->ApplicationCacheAvailable(cache);
  }
}

void nsOfflineCacheUpdate::StickDocument(nsIURI* aDocumentURI) {
  if (!aDocumentURI) return;

  mDocumentURIs.AppendObject(aDocumentURI);
}

void nsOfflineCacheUpdate::SetOwner(nsOfflineCacheUpdateOwner* aOwner) {
  NS_ASSERTION(!mOwner, "Tried to set cache update owner twice.");
  mOwner = aOwner;
}

bool nsOfflineCacheUpdate::IsForGroupID(const nsACString& groupID) {
  return mGroupID == groupID;
}

bool nsOfflineCacheUpdate::IsForProfile(nsIFile* aCustomProfileDir) {
  if (!mCustomProfileDir && !aCustomProfileDir) return true;
  if (!mCustomProfileDir || !aCustomProfileDir) return false;

  bool equals;
  nsresult rv = mCustomProfileDir->Equals(aCustomProfileDir, &equals);

  return NS_SUCCEEDED(rv) && equals;
}

nsresult nsOfflineCacheUpdate::UpdateFinished(nsOfflineCacheUpdate* aUpdate) {
  // Keep the object alive through a Finish() call.
  nsCOMPtr<nsIOfflineCacheUpdate> kungFuDeathGrip(this);

  mImplicitUpdate = nullptr;

  NotifyState(nsIOfflineCacheUpdateObserver::STATE_NOUPDATE);
  Finish();

  return NS_OK;
}

void nsOfflineCacheUpdate::OnByteProgress(uint64_t byteIncrement) {
  mByteProgress += byteIncrement;
  NotifyState(nsIOfflineCacheUpdateObserver::STATE_ITEMPROGRESS);
}

nsresult nsOfflineCacheUpdate::ScheduleImplicit() {
  if (mDocumentURIs.Count() == 0) return NS_OK;

  nsresult rv;

  RefPtr<nsOfflineCacheUpdate> update = new nsOfflineCacheUpdate();
  NS_ENSURE_TRUE(update, NS_ERROR_OUT_OF_MEMORY);

  nsAutoCString clientID;
  if (mPreviousApplicationCache) {
    rv = mPreviousApplicationCache->GetClientID(clientID);
    NS_ENSURE_SUCCESS(rv, rv);
  } else if (mApplicationCache) {
    rv = mApplicationCache->GetClientID(clientID);
    NS_ENSURE_SUCCESS(rv, rv);
  } else {
    NS_ERROR("Offline cache update not having set mApplicationCache?");
  }

  rv = update->InitPartial(mManifestURI, clientID, mDocumentURI,
                           mLoadingPrincipal);
  NS_ENSURE_SUCCESS(rv, rv);

  for (int32_t i = 0; i < mDocumentURIs.Count(); i++) {
    rv = update->AddURI(mDocumentURIs[i], nsIApplicationCache::ITEM_IMPLICIT);
    NS_ENSURE_SUCCESS(rv, rv);
  }

  update->SetOwner(this);
  rv = update->Begin();
  NS_ENSURE_SUCCESS(rv, rv);

  mImplicitUpdate = update;

  return NS_OK;
}

nsresult nsOfflineCacheUpdate::FinishNoNotify() {
  LOG(("nsOfflineCacheUpdate::Finish [%p]", this));

  mState = STATE_FINISHED;

  if (!mPartialUpdate && !mOnlyCheckUpdate) {
    if (mSucceeded) {
      nsIArray* namespaces = mManifestItem->GetNamespaces();
      nsresult rv = mApplicationCache->AddNamespaces(namespaces);
      if (NS_FAILED(rv)) {
        NotifyState(nsIOfflineCacheUpdateObserver::STATE_ERROR);
        mSucceeded = false;
      }

      rv = mApplicationCache->Activate();
      if (NS_FAILED(rv)) {
        NotifyState(nsIOfflineCacheUpdateObserver::STATE_ERROR);
        mSucceeded = false;
      }

      AssociateDocuments(mApplicationCache);
    }

    if (mObsolete) {
      nsCOMPtr<nsIApplicationCacheService> appCacheService =
          do_GetService(NS_APPLICATIONCACHESERVICE_CONTRACTID);
      if (appCacheService) {
        nsAutoCString groupID;
        mApplicationCache->GetGroupID(groupID);
        appCacheService->DeactivateGroup(groupID);
      }
    }

    if (!mSucceeded) {
      // Update was not merged, mark all the loads as failures
      for (uint32_t i = 0; i < mItems.Length(); i++) {
        mItems[i]->Cancel();
      }

      mApplicationCache->Discard();
    }
  }

  nsresult rv = NS_OK;

  if (mOwner) {
    rv = mOwner->UpdateFinished(this);
    // mozilla::WeakPtr is missing some key features, like setting it to
    // null explicitly.
    mOwner = mozilla::WeakPtr<nsOfflineCacheUpdateOwner>();
  }

  return rv;
}

nsresult nsOfflineCacheUpdate::Finish() {
  nsresult rv = FinishNoNotify();

  NotifyState(nsIOfflineCacheUpdateObserver::STATE_FINISHED);

  return rv;
}

void nsOfflineCacheUpdate::AsyncFinishWithError() {
  NotifyState(nsOfflineCacheUpdate::STATE_ERROR);
  Finish();
}

static nsresult EvictOneOfCacheGroups(nsIApplicationCacheService* cacheService,
                                      const nsTArray<nsCString>& groups) {
  nsresult rv;

  for (auto& group : groups) {
    nsCOMPtr<nsIURI> uri;
    rv = NS_NewURI(getter_AddRefs(uri), group);
    NS_ENSURE_SUCCESS(rv, rv);

    nsCOMPtr<nsIApplicationCache> cache;
    rv = cacheService->GetActiveCache(group, getter_AddRefs(cache));
    // Maybe someone in another thread or process have deleted it.
    if (NS_FAILED(rv) || !cache) continue;

    bool pinned;
    rv = nsOfflineCacheUpdateService::OfflineAppPinnedForURI(uri, &pinned);
    NS_ENSURE_SUCCESS(rv, rv);

    if (!pinned) {
      rv = cache->Discard();
      return NS_OK;
    }
  }

  return NS_ERROR_FILE_NOT_FOUND;
}

nsresult nsOfflineCacheUpdate::EvictOneNonPinned() {
  nsresult rv;

  nsCOMPtr<nsIApplicationCacheService> cacheService =
      do_GetService(NS_APPLICATIONCACHESERVICE_CONTRACTID, &rv);
  NS_ENSURE_SUCCESS(rv, rv);

  nsTArray<nsCString> groups;
  rv = cacheService->GetGroupsTimeOrdered(groups);
  NS_ENSURE_SUCCESS(rv, rv);

  return EvictOneOfCacheGroups(cacheService, groups);
}

//-----------------------------------------------------------------------------
// nsOfflineCacheUpdate::nsIOfflineCacheUpdate
//-----------------------------------------------------------------------------

NS_IMETHODIMP
nsOfflineCacheUpdate::GetUpdateDomain(nsACString& aUpdateDomain) {
  NS_ENSURE_TRUE(mState >= STATE_INITIALIZED, NS_ERROR_NOT_INITIALIZED);

  aUpdateDomain = mUpdateDomain;
  return NS_OK;
}

NS_IMETHODIMP
nsOfflineCacheUpdate::GetStatus(uint16_t* aStatus) {
  switch (mState) {
    case STATE_CHECKING:
      *aStatus = dom::OfflineResourceList_Binding::CHECKING;
      return NS_OK;
    case STATE_DOWNLOADING:
      *aStatus = dom::OfflineResourceList_Binding::DOWNLOADING;
      return NS_OK;
    default:
      *aStatus = dom::OfflineResourceList_Binding::IDLE;
      return NS_OK;
  }

  return NS_ERROR_FAILURE;
}

NS_IMETHODIMP
nsOfflineCacheUpdate::GetPartial(bool* aPartial) {
  *aPartial = mPartialUpdate || mOnlyCheckUpdate;
  return NS_OK;
}

NS_IMETHODIMP
nsOfflineCacheUpdate::GetManifestURI(nsIURI** aManifestURI) {
  NS_ENSURE_TRUE(mState >= STATE_INITIALIZED, NS_ERROR_NOT_INITIALIZED);

  NS_IF_ADDREF(*aManifestURI = mManifestURI);
  return NS_OK;
}

NS_IMETHODIMP
nsOfflineCacheUpdate::GetLoadingPrincipal(nsIPrincipal** aLoadingPrincipal) {
  NS_ENSURE_TRUE(mState >= STATE_INITIALIZED, NS_ERROR_NOT_INITIALIZED);

  NS_IF_ADDREF(*aLoadingPrincipal = mLoadingPrincipal);
  return NS_OK;
}

NS_IMETHODIMP
nsOfflineCacheUpdate::GetSucceeded(bool* aSucceeded) {
  NS_ENSURE_TRUE(mState == STATE_FINISHED, NS_ERROR_NOT_AVAILABLE);

  *aSucceeded = mSucceeded;

  return NS_OK;
}

NS_IMETHODIMP
nsOfflineCacheUpdate::GetIsUpgrade(bool* aIsUpgrade) {
  NS_ENSURE_TRUE(mState >= STATE_INITIALIZED, NS_ERROR_NOT_INITIALIZED);

  *aIsUpgrade = (mPreviousApplicationCache != nullptr);

  return NS_OK;
}

nsresult nsOfflineCacheUpdate::AddURI(nsIURI* aURI, uint32_t aType,
                                      uint32_t aLoadFlags) {
  NS_ENSURE_TRUE(mState >= STATE_INITIALIZED, NS_ERROR_NOT_INITIALIZED);

  if (mState >= STATE_DOWNLOADING) return NS_ERROR_NOT_AVAILABLE;

  // Resource URIs must have the same scheme as the manifest.
  nsAutoCString scheme;
  aURI->GetScheme(scheme);

  if (!mManifestURI->SchemeIs(scheme.get())) {
    return NS_ERROR_FAILURE;
  }

  // Don't fetch the same URI twice.
  for (uint32_t i = 0; i < mItems.Length(); i++) {
    bool equals;
    if (NS_SUCCEEDED(mItems[i]->mURI->Equals(aURI, &equals)) && equals &&
        mItems[i]->mLoadFlags == aLoadFlags) {
      // retain both types.
      mItems[i]->mItemType |= aType;
      return NS_OK;
    }
  }

  RefPtr<nsOfflineCacheUpdateItem> item = new nsOfflineCacheUpdateItem(
      aURI, mDocumentURI, mLoadingPrincipal, mApplicationCache,
      mPreviousApplicationCache, aType, aLoadFlags);
  if (!item) return NS_ERROR_OUT_OF_MEMORY;

  mItems.AppendElement(item);
  mAddedItems = true;

  return NS_OK;
}

NS_IMETHODIMP
nsOfflineCacheUpdate::AddDynamicURI(nsIURI* aURI) {
  if (GeckoProcessType_Default != XRE_GetProcessType())
    return NS_ERROR_NOT_IMPLEMENTED;

  // If this is a partial update and the resource is already in the
  // cache, we should only mark the entry, not fetch it again.
  if (mPartialUpdate) {
    nsAutoCString key;
    GetCacheKey(aURI, key);

    uint32_t types;
    nsresult rv = mApplicationCache->GetTypes(key, &types);
    if (NS_SUCCEEDED(rv)) {
      if (!(types & nsIApplicationCache::ITEM_DYNAMIC)) {
        mApplicationCache->MarkEntry(key, nsIApplicationCache::ITEM_DYNAMIC);
      }
      return NS_OK;
    }
  }

  return AddURI(aURI, nsIApplicationCache::ITEM_DYNAMIC);
}

NS_IMETHODIMP
nsOfflineCacheUpdate::Cancel() {
  LOG(("nsOfflineCacheUpdate::Cancel [%p]", this));

  if ((mState == STATE_FINISHED) || (mState == STATE_CANCELLED)) {
    return NS_ERROR_NOT_AVAILABLE;
  }

  mState = STATE_CANCELLED;
  mSucceeded = false;

  // Cancel all running downloads
  for (uint32_t i = 0; i < mItems.Length(); ++i) {
    nsOfflineCacheUpdateItem* item = mItems[i];

    if (item->IsInProgress()) item->Cancel();
  }

  return NS_OK;
}

NS_IMETHODIMP
nsOfflineCacheUpdate::AddObserver(nsIOfflineCacheUpdateObserver* aObserver,
                                  bool aHoldWeak) {
  LOG(("nsOfflineCacheUpdate::AddObserver [%p] to update [%p]", aObserver,
       this));

  NS_ENSURE_TRUE(mState >= STATE_INITIALIZED, NS_ERROR_NOT_INITIALIZED);

  if (aHoldWeak) {
    nsWeakPtr weakRef = do_GetWeakReference(aObserver);
    mWeakObservers.AppendObject(weakRef);
  } else {
    mObservers.AppendObject(aObserver);
  }

  return NS_OK;
}

NS_IMETHODIMP
nsOfflineCacheUpdate::RemoveObserver(nsIOfflineCacheUpdateObserver* aObserver) {
  LOG(("nsOfflineCacheUpdate::RemoveObserver [%p] from update [%p]", aObserver,
       this));

  NS_ENSURE_TRUE(mState >= STATE_INITIALIZED, NS_ERROR_NOT_INITIALIZED);

  for (int32_t i = 0; i < mWeakObservers.Count(); i++) {
    nsCOMPtr<nsIOfflineCacheUpdateObserver> observer =
        do_QueryReferent(mWeakObservers[i]);
    if (observer == aObserver) {
      mWeakObservers.RemoveObjectAt(i);
      return NS_OK;
    }
  }

  for (int32_t i = 0; i < mObservers.Count(); i++) {
    if (mObservers[i] == aObserver) {
      mObservers.RemoveObjectAt(i);
      return NS_OK;
    }
  }

  return NS_OK;
}

NS_IMETHODIMP
nsOfflineCacheUpdate::GetByteProgress(uint64_t* _result) {
  NS_ENSURE_ARG(_result);

  *_result = mByteProgress;
  return NS_OK;
}

NS_IMETHODIMP
nsOfflineCacheUpdate::Schedule() {
  LOG(("nsOfflineCacheUpdate::Schedule [%p]", this));

  nsOfflineCacheUpdateService* service =
      nsOfflineCacheUpdateService::EnsureService();

  if (!service) {
    return NS_ERROR_FAILURE;
  }

  return service->ScheduleUpdate(this);
}

NS_IMETHODIMP
nsOfflineCacheUpdate::UpdateStateChanged(nsIOfflineCacheUpdate* aUpdate,
                                         uint32_t aState) {
  if (aState == nsIOfflineCacheUpdateObserver::STATE_FINISHED) {
    // Take the mSucceeded flag from the underlying update, we will be
    // queried for it soon. mSucceeded of this update is false (manifest
    // check failed) but the subsequent re-fetch update might succeed
    bool succeeded;
    aUpdate->GetSucceeded(&succeeded);
    mSucceeded = succeeded;
  }

  NotifyState(aState);
  if (aState == nsIOfflineCacheUpdateObserver::STATE_FINISHED)
    aUpdate->RemoveObserver(this);

  return NS_OK;
}

NS_IMETHODIMP
nsOfflineCacheUpdate::ApplicationCacheAvailable(
    nsIApplicationCache* applicationCache) {
  AssociateDocuments(applicationCache);
  return NS_OK;
}

//-----------------------------------------------------------------------------
// nsOfflineCacheUpdate::nsIRunable
//-----------------------------------------------------------------------------

NS_IMETHODIMP
nsOfflineCacheUpdate::Run() {
  ProcessNextURI();
  return NS_OK;
}