/* 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 "PageThumbProtocolHandler.h"

#include "mozilla/ClearOnShutdown.h"
#include "mozilla/Components.h"
#include "mozilla/ipc/URIParams.h"
#include "mozilla/ipc/URIUtils.h"
#include "mozilla/net/NeckoChild.h"
#include "mozilla/net/NeckoParent.h"
#include "mozilla/RefPtr.h"
#include "mozilla/ResultExtensions.h"
#include "mozilla/Try.h"

#include "LoadInfo.h"
#include "nsContentUtils.h"
#include "nsServiceManagerUtils.h"
#include "nsIFile.h"
#include "nsIFileChannel.h"
#include "nsIFileStreams.h"
#include "nsIMIMEService.h"
#include "nsIURL.h"
#include "nsIChannel.h"
#include "nsIPageThumbsStorageService.h"
#include "nsIInputStreamPump.h"
#include "nsIStreamListener.h"
#include "nsIInputStream.h"
#include "nsNetUtil.h"
#include "nsURLHelper.h"
#include "prio.h"
#include "SimpleChannel.h"
#include "nsICancelable.h"

#ifdef MOZ_PLACES
#  include "nsIPlacesPreviewsHelperService.h"
#endif

#define PAGE_THUMB_HOST "thumbnails"
#define PLACES_PREVIEWS_HOST "places-previews"
#define PAGE_THUMB_SCHEME "moz-page-thumb"

namespace mozilla {
namespace net {

LazyLogModule gPageThumbProtocolLog("PageThumbProtocol");

#undef LOG
#define LOG(level, ...) \
  MOZ_LOG(gPageThumbProtocolLog, LogLevel::level, (__VA_ARGS__))

StaticRefPtr<PageThumbProtocolHandler> PageThumbProtocolHandler::sSingleton;

NS_IMPL_QUERY_INTERFACE(PageThumbProtocolHandler,
                        nsISubstitutingProtocolHandler, nsIProtocolHandler,
                        nsISupportsWeakReference)
NS_IMPL_ADDREF_INHERITED(PageThumbProtocolHandler, SubstitutingProtocolHandler)
NS_IMPL_RELEASE_INHERITED(PageThumbProtocolHandler, SubstitutingProtocolHandler)

already_AddRefed<PageThumbProtocolHandler>
PageThumbProtocolHandler::GetSingleton() {
  if (!sSingleton) {
    sSingleton = new PageThumbProtocolHandler();
    ClearOnShutdown(&sSingleton);
  }

  return do_AddRef(sSingleton);
}

// A moz-page-thumb URI is only loadable by chrome pages in the parent process,
// or privileged content running in the privileged about content process.
PageThumbProtocolHandler::PageThumbProtocolHandler()
    : SubstitutingProtocolHandler(PAGE_THUMB_SCHEME) {}

RefPtr<RemoteStreamPromise> PageThumbProtocolHandler::NewStream(
    nsIURI* aChildURI, nsILoadInfo* aLoadInfo, bool* aTerminateSender) {
  MOZ_ASSERT(!IsNeckoChild());
  MOZ_ASSERT(NS_IsMainThread());

  if (!aChildURI || !aTerminateSender) {
    return RemoteStreamPromise::CreateAndReject(NS_ERROR_INVALID_ARG, __func__);
  }

  *aTerminateSender = true;
  nsresult rv;

  // We should never receive a URI that isn't for a moz-page-thumb because
  // these requests ordinarily come from the child's PageThumbProtocolHandler.
  // Ensure this request is for a moz-page-thumb URI. A compromised child
  // process could send us any URI.
  bool isPageThumbScheme = false;
  if (NS_FAILED(aChildURI->SchemeIs(PAGE_THUMB_SCHEME, &isPageThumbScheme)) ||
      !isPageThumbScheme) {
    return RemoteStreamPromise::CreateAndReject(NS_ERROR_UNKNOWN_PROTOCOL,
                                                __func__);
  }

  if (!nsContentUtils::IsImageType(aLoadInfo->GetExternalContentPolicyType())) {
    return RemoteStreamPromise::CreateAndReject(NS_ERROR_CONTENT_BLOCKED,
                                                __func__);
  }

  // We should never receive a URI that does not have "thumbnails" as the host.
  nsAutoCString host;
  if (NS_FAILED(aChildURI->GetAsciiHost(host)) ||
      !(host.EqualsLiteral(PAGE_THUMB_HOST) ||
        host.EqualsLiteral(PLACES_PREVIEWS_HOST))) {
    return RemoteStreamPromise::CreateAndReject(NS_ERROR_UNEXPECTED, __func__);
  }

  // For errors after this point, we want to propagate the error to
  // the child, but we don't force the child process to be terminated.
  *aTerminateSender = false;

  // Make sure the child URI resolves to a file URI. We will then get a file
  // channel for the request. The resultant channel should be a file channel
  // because we only request remote streams for resource loads where the URI
  // resolves to a file.
  nsAutoCString resolvedSpec;
  rv = ResolveURI(aChildURI, resolvedSpec);
  if (NS_FAILED(rv)) {
    return RemoteStreamPromise::CreateAndReject(rv, __func__);
  }

  return mozilla::net::NeckoParent::CreateRemoteStreamForResolvedURI(
      aChildURI, resolvedSpec, ""_ns);
}

bool PageThumbProtocolHandler::ResolveSpecialCases(const nsACString& aHost,
                                                   const nsACString& aPath,
                                                   const nsACString& aPathname,
                                                   nsACString& aResult) {
  // This should match the scheme in PageThumbs.sys.mjs. We will only resolve
  // URIs for thumbnails generated by PageThumbs here.
  if (!aHost.EqualsLiteral(PAGE_THUMB_HOST) &&
      !aHost.EqualsLiteral(PLACES_PREVIEWS_HOST)) {
    // moz-page-thumb should always have a "thumbnails" host. We do not intend
    // to allow substitution rules to be created for moz-page-thumb.
    return false;
  }

  // Regardless of the outcome, the scheme will be resolved to file://.
  aResult.Assign("file://");

  if (IsNeckoChild()) {
    // We will resolve the URI in the parent if load is performed in the child
    // because the child does not have access to the profile directory path.
    // Technically we could retrieve the path from dom::ContentChild, but I
    // would prefer to obtain the path from PageThumbsStorageService (which
    // depends on OS.Path). Here, we resolve to the same URI, with the file://
    // scheme. This won't ever be accessed directly by the content process,
    // and is mainly used to keep the substitution protocol handler mechanism
    // happy.
    aResult.Append(aHost);
    aResult.Append(aPath);
  } else {
    // Resolve the URI in the parent to the thumbnail file URI since we will
    // attempt to open the channel to load the file after this.
    nsAutoString thumbnailUrl;
    nsresult rv = GetThumbnailPath(aPath, aHost, thumbnailUrl);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return false;
    }

    aResult.Append(NS_ConvertUTF16toUTF8(thumbnailUrl));
  }

  return true;
}

nsresult PageThumbProtocolHandler::SubstituteChannel(nsIURI* aURI,
                                                     nsILoadInfo* aLoadInfo,
                                                     nsIChannel** aRetVal) {
  if (!nsContentUtils::IsImageType(aLoadInfo->GetExternalContentPolicyType())) {
    return NS_ERROR_CONTENT_BLOCKED;
  }

  // Check if URI resolves to a file URI.
  nsAutoCString resolvedSpec;
  MOZ_TRY(ResolveURI(aURI, resolvedSpec));

  nsAutoCString scheme;
  MOZ_TRY(net_ExtractURLScheme(resolvedSpec, scheme));

  if (!scheme.EqualsLiteral("file")) {
    NS_WARNING("moz-page-thumb URIs should only resolve to file URIs.");
    return NS_ERROR_NO_INTERFACE;
  }

  // Load the URI remotely if accessed from a child.
  if (IsNeckoChild()) {
    MOZ_TRY(SubstituteRemoteChannel(aURI, aLoadInfo, aRetVal));
  }

  return NS_OK;
}

Result<Ok, nsresult> PageThumbProtocolHandler::SubstituteRemoteChannel(
    nsIURI* aURI, nsILoadInfo* aLoadInfo, nsIChannel** aRetVal) {
  MOZ_ASSERT(IsNeckoChild());
  MOZ_TRY(aURI ? NS_OK : NS_ERROR_INVALID_ARG);
  MOZ_TRY(aLoadInfo ? NS_OK : NS_ERROR_INVALID_ARG);

#ifdef DEBUG
  nsAutoCString resolvedSpec;
  MOZ_TRY(ResolveURI(aURI, resolvedSpec));

  nsAutoCString scheme;
  MOZ_TRY(net_ExtractURLScheme(resolvedSpec, scheme));

  MOZ_ASSERT(scheme.EqualsLiteral("file"));
#endif /* DEBUG */

  RefPtr<RemoteStreamGetter> streamGetter =
      new RemoteStreamGetter(aURI, aLoadInfo);

  NewSimpleChannel(aURI, aLoadInfo, streamGetter, aRetVal);
  return Ok();
}

nsresult PageThumbProtocolHandler::GetThumbnailPath(const nsACString& aPath,
                                                    const nsACString& aHost,
                                                    nsString& aThumbnailPath) {
  MOZ_ASSERT(!IsNeckoChild());

  // Ensures that the provided path has a query string. We will start parsing
  // from there.
  int32_t queryIndex = aPath.FindChar('?');
  if (queryIndex <= 0) {
    return NS_ERROR_MALFORMED_URI;
  }

  // Extract URL from query string.
  nsAutoCString url;
  bool found =
      URLParams::Extract(Substring(aPath, queryIndex + 1), "url"_ns, url);
  if (!found || url.IsVoid()) {
    return NS_ERROR_NOT_AVAILABLE;
  }

  nsresult rv;
  if (aHost.EqualsLiteral(PAGE_THUMB_HOST)) {
    nsCOMPtr<nsIPageThumbsStorageService> pageThumbsStorage;
    pageThumbsStorage = mozilla::components::PageThumbsStorage::Service(&rv);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return rv;
    }
    // Use PageThumbsStorageService to get the local file path of the screenshot
    // for the given URL.
    rv = pageThumbsStorage->GetFilePathForURL(NS_ConvertUTF8toUTF16(url),
                                              aThumbnailPath);
#ifdef MOZ_PLACES
  } else if (aHost.EqualsLiteral(PLACES_PREVIEWS_HOST)) {
    nsCOMPtr<nsIPlacesPreviewsHelperService> helper;
    helper = mozilla::components::PlacesPreviewsHelper::Service(&rv);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return rv;
    }
    rv = helper->GetFilePathForURL(NS_ConvertUTF8toUTF16(url), aThumbnailPath);
#endif
  } else {
    MOZ_ASSERT_UNREACHABLE("Unknown thumbnail host");
    return NS_ERROR_UNEXPECTED;
  }
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }
  return NS_OK;
}

// static
void PageThumbProtocolHandler::NewSimpleChannel(
    nsIURI* aURI, nsILoadInfo* aLoadinfo, RemoteStreamGetter* aStreamGetter,
    nsIChannel** aRetVal) {
  nsCOMPtr<nsIChannel> channel = NS_NewSimpleChannel(
      aURI, aLoadinfo, aStreamGetter,
      [](nsIStreamListener* listener, nsIChannel* simpleChannel,
         RemoteStreamGetter* getter) -> RequestOrReason {
        return getter->GetAsync(listener, simpleChannel,
                                &NeckoChild::SendGetPageThumbStream);
      });

  channel.swap(*aRetVal);
}

#undef LOG

}  // namespace net
}  // namespace mozilla
