/* -*- Mode: C++; tab-width: 2; 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 "nsMsgContentPolicy.h"
#include "nsIPermissionManager.h"
#include "nsIPrefService.h"
#include "nsIPrefBranch.h"
#include "nsIAbManager.h"
#include "nsIAbDirectory.h"
#include "nsIAbCard.h"
#include "nsIMsgWindow.h"
#include "nsIMimeMiscStatus.h"
#include "nsIMsgHdr.h"
#include "nsIEncryptedSMIMEURIsSrvc.h"
#include "nsNetUtil.h"
#include "nsIMsgComposeService.h"
#include "nsMsgCompCID.h"
#include "nsIDocShellTreeItem.h"
#include "nsIWebNavigation.h"
#include "nsContentPolicyUtils.h"
#include "nsIDOMHTMLImageElement.h"
#include "nsIFrameLoader.h"
#include "nsIWebProgress.h"
#include "nsMsgUtils.h"
#include "nsThreadUtils.h"
#include "mozilla/mailnews/MimeHeaderParser.h"
#include "nsINntpUrl.h"
#include "nsSandboxFlags.h"

static const char kBlockRemoteImages[] = "mailnews.message_display.disable_remote_image";
static const char kAllowPlugins[] = "mailnews.message_display.allow_plugins";
static const char kTrustedDomains[] =  "mail.trusteddomains";

using namespace mozilla::mailnews;

// Per message headder flags to keep track of whether the user is allowing remote
// content for a particular message.
// if you change or add more values to these constants, be sure to modify
// the corresponding definitions in mailWindowOverlay.js
#define kNoRemoteContentPolicy 0
#define kBlockRemoteContent 1
#define kAllowRemoteContent 2

NS_IMPL_ISUPPORTS(nsMsgContentPolicy,
                  nsIContentPolicy,
                  nsIWebProgressListener,
                  nsIMsgContentPolicy,
                  nsIObserver,
                  nsISupportsWeakReference)

nsMsgContentPolicy::nsMsgContentPolicy()
{
  mAllowPlugins = false;
  mBlockRemoteImages = true;
}

nsMsgContentPolicy::~nsMsgContentPolicy()
{
  // hey, we are going away...clean up after ourself....unregister our observer
  nsresult rv;
  nsCOMPtr<nsIPrefBranch> prefInternal = do_GetService(NS_PREFSERVICE_CONTRACTID, &rv);
  if (NS_SUCCEEDED(rv))
  {
    prefInternal->RemoveObserver(kBlockRemoteImages, this);
    prefInternal->RemoveObserver(kAllowPlugins, this);
  }
}

nsresult nsMsgContentPolicy::Init()
{
  nsresult rv;

  // register ourself as an observer on the mail preference to block remote images
  nsCOMPtr<nsIPrefBranch> prefInternal = do_GetService(NS_PREFSERVICE_CONTRACTID, &rv);
  NS_ENSURE_SUCCESS(rv, rv);

  prefInternal->AddObserver(kBlockRemoteImages, this, true);
  prefInternal->AddObserver(kAllowPlugins, this, true);

  prefInternal->GetBoolPref(kAllowPlugins, &mAllowPlugins);
  prefInternal->GetCharPref(kTrustedDomains, getter_Copies(mTrustedMailDomains));
  prefInternal->GetBoolPref(kBlockRemoteImages, &mBlockRemoteImages);

  // Grab a handle on the PermissionManager service for managing allowed remote
  // content senders.
  mPermissionManager = do_GetService(NS_PERMISSIONMANAGER_CONTRACTID, &rv);
  NS_ENSURE_SUCCESS(rv, rv);

  return NS_OK;
}

/**
 * @returns true if the sender referenced by aMsgHdr is explicitly allowed to
 *          load remote images according to the PermissionManager
 */
bool
nsMsgContentPolicy::ShouldAcceptRemoteContentForSender(nsIMsgDBHdr *aMsgHdr)
{
  if (!aMsgHdr)
    return false;

  // extract the e-mail address from the msg hdr
  nsCString author;
  nsresult rv = aMsgHdr->GetAuthor(getter_Copies(author));
  NS_ENSURE_SUCCESS(rv, false);

  nsCString emailAddress;
  ExtractEmail(EncodedHeader(author), emailAddress);
  if (emailAddress.IsEmpty())
    return false;

  nsCOMPtr<nsIIOService> ios = do_GetService("@mozilla.org/network/io-service;1", &rv);
  NS_ENSURE_SUCCESS(rv, false);
  nsCOMPtr<nsIURI> mailURI;
  emailAddress.Insert("chrome://messenger/content/email=", 0);
  rv = ios->NewURI(emailAddress, nullptr, nullptr, getter_AddRefs(mailURI));
  NS_ENSURE_SUCCESS(rv, false);

  // check with permission manager
  uint32_t permission = 0;
  rv = mPermissionManager->TestPermission(mailURI, "image", &permission);
  NS_ENSURE_SUCCESS(rv, false);

  // Only return true if the permission manager has an explicit allow
  return (permission == nsIPermissionManager::ALLOW_ACTION);
}

/**
 * Extract the host name from aContentLocation, and look it up in our list
 * of trusted domains.
 */
bool nsMsgContentPolicy::IsTrustedDomain(nsIURI * aContentLocation)
{
  bool trustedDomain = false;
  // get the host name of the server hosting the remote image
  nsAutoCString host;
  nsresult rv = aContentLocation->GetHost(host);

  if (NS_SUCCEEDED(rv) && !mTrustedMailDomains.IsEmpty())
    trustedDomain = MsgHostDomainIsTrusted(host, mTrustedMailDomains);

  return trustedDomain;
}

NS_IMETHODIMP
nsMsgContentPolicy::ShouldLoad(uint32_t          aContentType,
                               nsIURI           *aContentLocation,
                               nsIURI           *aRequestingLocation,
                               nsISupports      *aRequestingContext,
                               const nsACString &aMimeGuess,
                               nsISupports      *aExtra,
                               nsIPrincipal     *aRequestPrincipal,
                               int16_t          *aDecision)
{
  nsresult rv = NS_OK;
  // The default decision at the start of the function is to accept the load.
  // Once we have checked the content type and the requesting location, then
  // we switch it to reject.
  //
  // Be very careful about returning error codes - if this method returns an
  // NS_ERROR_*, any decision made here will be ignored, and the document could
  // be accepted when we don't want it to be.
  //
  // In most cases if an error occurs, its something we didn't expect so we
  // should be rejecting the document anyway.
  *aDecision = nsIContentPolicy::ACCEPT;

  NS_ENSURE_ARG_POINTER(aContentLocation);

#ifdef DEBUG_MsgContentPolicy
  fprintf(stderr, "aContentType: %d\naContentLocation = %s\n",
          aContentType,
          aContentLocation->GetSpecOrDefault().get());
#endif

#ifndef MOZ_THUNDERBIRD
  // Go find out if we are dealing with mailnews. Anything else
  // isn't our concern and we accept content.
  nsCOMPtr<nsIDocShell> rootDocShell;
  rv = GetRootDocShellForContext(aRequestingContext,
                                 getter_AddRefs(rootDocShell));
  NS_ENSURE_SUCCESS(rv, rv);

  uint32_t appType;
  rv = rootDocShell->GetAppType(&appType);
  // We only want to deal with mailnews
  if (NS_FAILED(rv) || appType != nsIDocShell::APP_TYPE_MAIL)
    return NS_OK;
#endif

  switch(aContentType) {
    // Plugins (nsIContentPolicy::TYPE_OBJECT) are blocked on document load.
  case nsIContentPolicy::TYPE_DOCUMENT:
    // At this point, we have no intention of supporting a different JS
    // setting on a subdocument, so we don't worry about TYPE_SUBDOCUMENT here.

    // If the timing were right, we'd enable JavaScript on the docshell
    // for non mailnews URIs here.  However, at this point, the
    // old document may still be around, so we can't do any enabling just yet.
    // Instead, we apply the policy in nsIWebProgressListener::OnLocationChange.
    // For now, we explicitly disable JavaScript in order to be safe rather than
    // sorry, because OnLocationChange isn't guaranteed to necessarily be called
    // soon enough to disable it in time (though bz says it _should_ be called
    // soon enough "in all sane cases").
    rv = SetDisableItemsOnMailNewsUrlDocshells(aContentLocation,
                                               aRequestingContext);
    // if something went wrong during the tweaking, reject this content
    if (NS_FAILED(rv)) {
      NS_WARNING("Failed to set disable items on docShells");
      *aDecision = nsIContentPolicy::REJECT_TYPE;
      return NS_OK;
    }
    break;

  case nsIContentPolicy::TYPE_CSP_REPORT:
    // We cannot block CSP reports.
    *aDecision = nsIContentPolicy::ACCEPT;
    return NS_OK;
    break;

  default:
    break;
  }

  // NOTE: Not using NS_ENSURE_ARG_POINTER because this is a legitimate case
  // that can happen.  Also keep in mind that the default policy used for a
  // failure code is ACCEPT.
  if (!aRequestingLocation)
    return NS_ERROR_INVALID_POINTER;

#ifdef DEBUG_MsgContentPolicy
  fprintf(stderr, "aRequestingLocation = %s\n", aRequestingLocation->GetSpecOrDefault().get());
#endif

  // If the requesting location is safe, accept the content location request.
  if (IsSafeRequestingLocation(aRequestingLocation))
    return rv;

  // Now default to reject so early returns via NS_ENSURE_SUCCESS
  // cause content to be rejected.
  *aDecision = nsIContentPolicy::REJECT_REQUEST;

  // We want to establish the following:
  // \--------\  requester    |               |              |
  // content   \------------\ |               |              |
  // requested               \| mail message  | news message | http(s)/data etc.
  // -------------------------+---------------+--------------+------------------
  // mail message content     | load if same  | don't load   | don't load
  // mailbox, imap, JsAccount | message (1)   | (2)          | (3)
  // -------------------------+---------------+--------------+------------------
  // news message             | don't load (4)| load (5)     | load (6)
  // -------------------------+---------------+--------------+------------------
  // http(s)/data, etc.       | (default)     | (default)    | (default)
  // -------------------------+---------------+--------------+------------------
  nsCOMPtr<nsIMsgMessageUrl> contentURL(do_QueryInterface(aContentLocation));
  if (contentURL) {
    nsCOMPtr<nsINntpUrl> contentNntpURL(do_QueryInterface(aContentLocation));
    if (!contentNntpURL) {
      // Mail message (mailbox, imap or JsAccount) content requested, for example
      // a message part, like an image:
      // To load mail message content the requester must have the same
      // "normalised" principal. This is basically a "same origin" test, it
      // protects against cross-loading of mail message content from
      // other mail or news messages.
      nsCOMPtr<nsIMsgMessageUrl> requestURL(do_QueryInterface(aRequestingLocation));
      // If the request URL is not also a message URL, then we don't accept.
      if (requestURL) {
        nsCString contentPrincipalSpec, requestPrincipalSpec;
        nsresult rv1 = contentURL->GetPrincipalSpec(contentPrincipalSpec);
        nsresult rv2 = requestURL->GetPrincipalSpec(requestPrincipalSpec);
        if (NS_SUCCEEDED(rv1) && NS_SUCCEEDED(rv2) &&
            contentPrincipalSpec.Equals(requestPrincipalSpec))
          *aDecision = nsIContentPolicy::ACCEPT; // (1)
      }
      return NS_OK; // (2) and (3)
    }

    // News message content requested. Don't accept request coming
    // from a mail message since it would access the news server.
    nsCOMPtr<nsIMsgMessageUrl> requestURL(do_QueryInterface(aRequestingLocation));
    if (requestURL) {
      nsCOMPtr<nsINntpUrl> requestNntpURL(do_QueryInterface(aRequestingLocation));
      if (!requestNntpURL)
        return NS_OK; // (4)
    }
    *aDecision = nsIContentPolicy::ACCEPT; // (5) and (6)
    return NS_OK;
  }

  // If exposed protocol not covered by the test above or protocol that has been
  // specifically exposed by an add-on, or is a chrome url, then allow the load.
  if (IsExposedProtocol(aContentLocation))
  {
    *aDecision = nsIContentPolicy::ACCEPT;
    return NS_OK;
  }

  // never load unexposed protocols except for http, https and file.
  // Protocols like ftp are always blocked.
  if (ShouldBlockUnexposedProtocol(aContentLocation))
    return NS_OK;


  // Find out the URI that originally initiated the set of requests for this
  // context.
  nsCOMPtr<nsIURI> originatorLocation;
  if (!aRequestingContext && aRequestPrincipal)
  {
    // Can get the URI directly from the principal.
    rv = aRequestPrincipal->GetURI(getter_AddRefs(originatorLocation));
  }
  else
  {
    rv = GetOriginatingURIForContext(aRequestingContext,
                                     getter_AddRefs(originatorLocation));
  }
  NS_ENSURE_SUCCESS(rv, NS_OK);

#ifdef DEBUG_MsgContentPolicy
  fprintf(stderr, "originatorLocation = %s\n", originatorLocation->GetSpecOrDefault().get());
#endif

  // Don't load remote content for encrypted messages.
  nsCOMPtr<nsIEncryptedSMIMEURIsService> encryptedURIService =
    do_GetService("@mozilla.org/messenger-smime/smime-encrypted-uris-service;1", &rv);
  NS_ENSURE_SUCCESS(rv, rv);
  bool isEncrypted;
  rv = encryptedURIService->IsEncrypted(aRequestingLocation->GetSpecOrDefault(), &isEncrypted);
  NS_ENSURE_SUCCESS(rv, rv);
  if (isEncrypted)
  {
    *aDecision = nsIContentPolicy::REJECT_REQUEST;
    NotifyContentWasBlocked(originatorLocation, aContentLocation, false);
    return NS_OK;
  }

  // If we are allowing all remote content...
  if (!mBlockRemoteImages)
  {
    *aDecision = nsIContentPolicy::ACCEPT;
    return NS_OK;
  }

  // Extract the windowtype to handle compose windows separately from mail
  if (aRequestingContext)
  {
    nsCOMPtr<nsIMsgCompose> msgCompose =
      GetMsgComposeForContext(aRequestingContext);
    // Work out if we're in a compose window or not.
    if (msgCompose)
    {
      ComposeShouldLoad(msgCompose, aRequestingContext, aContentLocation,
                        aDecision);
      return NS_OK;
    }
  }

  // Allow content when using a remote page.
  bool isHttp;
  bool isHttps;
  rv = originatorLocation->SchemeIs("http", &isHttp);
  nsresult rv2 = originatorLocation->SchemeIs("https", &isHttps);
  if (NS_SUCCEEDED(rv) && NS_SUCCEEDED(rv2) && (isHttp || isHttps))
  {
    *aDecision = nsIContentPolicy::ACCEPT;
    return NS_OK;
  }

  uint32_t permission;
  mPermissionManager->TestPermission(aContentLocation, "image", &permission);
  switch (permission) {
    case nsIPermissionManager::UNKNOWN_ACTION:
    {
      // No exception was found for this location.
      break;
    }
    case nsIPermissionManager::ALLOW_ACTION:
    {
      *aDecision = nsIContentPolicy::ACCEPT;
      return NS_OK;
    }
    case nsIPermissionManager::DENY_ACTION:
    {
      *aDecision = nsIContentPolicy::REJECT_REQUEST;
      return NS_OK;
    }
  }

  // The default decision is still to reject.
  ShouldAcceptContentForPotentialMsg(originatorLocation, aContentLocation,
                                     aDecision);
  return NS_OK;
}

/**
 * Determines if the requesting location is a safe one, i.e. its under the
 * app/user's control - so file, about, chrome etc.
 */
bool
nsMsgContentPolicy::IsSafeRequestingLocation(nsIURI *aRequestingLocation)
{
  if (!aRequestingLocation)
    return false;

  // If aRequestingLocation is one of chrome, resource, file or view-source,
  // allow aContentLocation to load.
  bool isChrome;
  bool isRes;
  bool isFile;
  bool isViewSource;

  nsresult rv = aRequestingLocation->SchemeIs("chrome", &isChrome);
  NS_ENSURE_SUCCESS(rv, false);
  rv = aRequestingLocation->SchemeIs("resource", &isRes);
  NS_ENSURE_SUCCESS(rv, false);
  rv = aRequestingLocation->SchemeIs("file", &isFile);
  NS_ENSURE_SUCCESS(rv, false);
  rv = aRequestingLocation->SchemeIs("view-source", &isViewSource);
  NS_ENSURE_SUCCESS(rv, false);

  if (isChrome || isRes || isFile || isViewSource)
    return true;

  // Only allow about: to load anything if the requesting location is not the
  // special about:blank one.
  bool isAbout;
  rv = aRequestingLocation->SchemeIs("about", &isAbout);
  NS_ENSURE_SUCCESS(rv, false);

  if (!isAbout)
    return false;

  nsCString fullSpec;
  rv = aRequestingLocation->GetSpec(fullSpec);
  NS_ENSURE_SUCCESS(rv, false);

  return !fullSpec.EqualsLiteral("about:blank");
}

/**
 * Determines if the content location is a scheme that we're willing to expose
 * for unlimited loading of content.
 */
bool
nsMsgContentPolicy::IsExposedProtocol(nsIURI *aContentLocation)
{
  nsAutoCString contentScheme;
  nsresult rv = aContentLocation->GetScheme(contentScheme);
  NS_ENSURE_SUCCESS(rv, false);

  // Check some exposed protocols. Not all protocols in the list of
  // network.protocol-handler.expose.* prefs in all-thunderbird.js are
  // admitted purely based on their scheme.
  // news, snews, nntp, imap and mailbox are checked before the call
  // to this function by matching content location and requesting location.
  if (MsgLowerCaseEqualsLiteral(contentScheme, "mailto") ||
      MsgLowerCaseEqualsLiteral(contentScheme, "addbook") ||
      MsgLowerCaseEqualsLiteral(contentScheme, "about"))
    return true;

  // check if customized exposed scheme
  if (mCustomExposedProtocols.Contains(contentScheme))
    return true;

  bool isData;
  bool isChrome;
  bool isRes;
  rv = aContentLocation->SchemeIs("chrome", &isChrome);
  NS_ENSURE_SUCCESS(rv, false);
  rv = aContentLocation->SchemeIs("resource", &isRes);
  NS_ENSURE_SUCCESS(rv, false);
  rv = aContentLocation->SchemeIs("data", &isData);
  NS_ENSURE_SUCCESS(rv, false);

  return isChrome || isRes || isData;
}

/**
 * We block most unexposed protocols - apart from http(s) and file.
 */
bool
nsMsgContentPolicy::ShouldBlockUnexposedProtocol(nsIURI *aContentLocation)
{
  bool isHttp;
  bool isHttps;
  bool isFile;
  // Error condition - we must return true so that we block.
  nsresult rv = aContentLocation->SchemeIs("http", &isHttp);
  NS_ENSURE_SUCCESS(rv, true);
  rv = aContentLocation->SchemeIs("https", &isHttps);
  NS_ENSURE_SUCCESS(rv, true);
  rv = aContentLocation->SchemeIs("file", &isFile);
  NS_ENSURE_SUCCESS(rv, true);

  return !isHttp && !isHttps && !isFile;
}

/**
 * The default for this function will be to reject the content request.
 * When determining if to allow the request for a given msg hdr, the function
 * will go through the list of remote content blocking criteria:
 *
 * #1 Allow if there is a db header for a manual override.
 * #2 Allow if the message is in an RSS folder.
 * #3 Allow if the domain for the remote image in our white list.
 * #4 Allow if the author has been specifically white listed.
 */
int16_t
nsMsgContentPolicy::ShouldAcceptRemoteContentForMsgHdr(nsIMsgDBHdr *aMsgHdr,
                                                       nsIURI *aRequestingLocation,
                                                       nsIURI *aContentLocation)
{
  if (!aMsgHdr)
    return static_cast<int16_t>(nsIContentPolicy::REJECT_REQUEST);

  // Case #1, check the db hdr for the remote content policy on this particular
  // message.
  uint32_t remoteContentPolicy = kNoRemoteContentPolicy;
  aMsgHdr->GetUint32Property("remoteContentPolicy", &remoteContentPolicy);

  // Case #2, check if the message is in an RSS folder
  bool isRSS = false;
  IsRSSArticle(aRequestingLocation, &isRSS);

  // Case #3, the domain for the remote image is in our white list
  bool trustedDomain = IsTrustedDomain(aContentLocation);

  // Case 4 means looking up items in the permissions database. So if
  // either of the two previous items means we load the data, just do it.
  if (isRSS || remoteContentPolicy == kAllowRemoteContent || trustedDomain)
    return nsIContentPolicy::ACCEPT;

  // Case #4, author is in our white list..
  bool allowForSender = ShouldAcceptRemoteContentForSender(aMsgHdr);

  int16_t result = allowForSender ?
    static_cast<int16_t>(nsIContentPolicy::ACCEPT) :
    static_cast<int16_t>(nsIContentPolicy::REJECT_REQUEST);

  // kNoRemoteContentPolicy means we have never set a value on the message
  if (result == nsIContentPolicy::REJECT_REQUEST && !remoteContentPolicy)
    aMsgHdr->SetUint32Property("remoteContentPolicy", kBlockRemoteContent);

  return result;
}

class RemoteContentNotifierEvent : public mozilla::Runnable
{
public:
  RemoteContentNotifierEvent(nsIMsgWindow *aMsgWindow, nsIMsgDBHdr *aMsgHdr,
                             nsIURI *aContentURI, bool aCanOverride = true)
    : mMsgWindow(aMsgWindow), mMsgHdr(aMsgHdr), mContentURI(aContentURI),
    mCanOverride(aCanOverride)
  {}

  NS_IMETHOD Run()
  {
    if (mMsgWindow)
    {
      nsCOMPtr<nsIMsgHeaderSink> msgHdrSink;
      (void)mMsgWindow->GetMsgHeaderSink(getter_AddRefs(msgHdrSink));
      if (msgHdrSink)
        msgHdrSink->OnMsgHasRemoteContent(mMsgHdr, mContentURI, mCanOverride);
    }
    return NS_OK;
  }

private:
  nsCOMPtr<nsIMsgWindow> mMsgWindow;
  nsCOMPtr<nsIMsgDBHdr> mMsgHdr;
  nsCOMPtr<nsIURI> mContentURI;
  bool mCanOverride;
};

/**
 * This function is used to show a blocked remote content notification.
 */
void
nsMsgContentPolicy::NotifyContentWasBlocked(nsIURI *aOriginatorLocation,
                                            nsIURI *aContentLocation,
                                            bool aCanOverride)
{
  // Is it a mailnews url?
  nsresult rv;
  nsCOMPtr<nsIMsgMessageUrl> msgUrl(do_QueryInterface(aOriginatorLocation,
                                                      &rv));
  if (NS_FAILED(rv))
  {
    return;
  }

  nsCString resourceURI;
  rv = msgUrl->GetUri(getter_Copies(resourceURI));
  NS_ENSURE_SUCCESS_VOID(rv);

  nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl(do_QueryInterface(aOriginatorLocation, &rv));
  NS_ENSURE_SUCCESS_VOID(rv);

  nsCOMPtr<nsIMsgDBHdr> msgHdr;
  rv = GetMsgDBHdrFromURI(resourceURI.get(), getter_AddRefs(msgHdr));
  if (NS_FAILED(rv))
  {
    // Maybe we can get a dummy header.
    nsCOMPtr<nsIMsgWindow> msgWindow;
    rv = mailnewsUrl->GetMsgWindow(getter_AddRefs(msgWindow));
    if (msgWindow)
    {
      nsCOMPtr<nsIMsgHeaderSink> msgHdrSink;
      rv = msgWindow->GetMsgHeaderSink(getter_AddRefs(msgHdrSink));
      if (msgHdrSink)
        rv = msgHdrSink->GetDummyMsgHeader(getter_AddRefs(msgHdr));
    }
  }

  nsCOMPtr<nsIMsgWindow> msgWindow;
  (void)mailnewsUrl->GetMsgWindow(getter_AddRefs(msgWindow));
  if (msgWindow)
  {
    nsCOMPtr<nsIRunnable> event =
      new RemoteContentNotifierEvent(msgWindow, msgHdr, aContentLocation, aCanOverride);
    // Post this as an event because it can cause dom mutations, and we
    // get called at a bad time to be causing dom mutations.
    if (event)
      NS_DispatchToCurrentThread(event);
  }
}

/**
 * This function is used to determine if we allow content for a remote message.
 * If we reject loading remote content, then we'll inform the message window
 * that this message has remote content (and hence we are not loading it).
 *
 * See ShouldAcceptRemoteContentForMsgHdr for the actual decisions that
 * determine if we are going to allow remote content.
 */
void
nsMsgContentPolicy::ShouldAcceptContentForPotentialMsg(nsIURI *aOriginatorLocation,
                                                       nsIURI *aContentLocation,
                                                       int16_t *aDecision)
{
  NS_PRECONDITION(*aDecision == nsIContentPolicy::REJECT_REQUEST,
                  "AllowContentForPotentialMessage expects default decision to be reject!");

  // Is it a mailnews url?
  nsresult rv;
  nsCOMPtr<nsIMsgMessageUrl> msgUrl(do_QueryInterface(aOriginatorLocation,
                                                      &rv));
  if (NS_FAILED(rv))
  {
    // It isn't a mailnews url - so we accept the load here, and let other
    // content policies make the decision if we should be loading it or not.
    *aDecision = nsIContentPolicy::ACCEPT;
    return;
  }

  nsCString resourceURI;
  rv = msgUrl->GetUri(getter_Copies(resourceURI));
  NS_ENSURE_SUCCESS_VOID(rv);

  nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl(do_QueryInterface(aOriginatorLocation, &rv));
  NS_ENSURE_SUCCESS_VOID(rv);

  nsCOMPtr<nsIMsgDBHdr> msgHdr;
  rv = GetMsgDBHdrFromURI(resourceURI.get(), getter_AddRefs(msgHdr));
  if (NS_FAILED(rv))
  {
    // Maybe we can get a dummy header.
    nsCOMPtr<nsIMsgWindow> msgWindow;
    rv = mailnewsUrl->GetMsgWindow(getter_AddRefs(msgWindow));
    if (msgWindow)
    {
      nsCOMPtr<nsIMsgHeaderSink> msgHdrSink;
      rv = msgWindow->GetMsgHeaderSink(getter_AddRefs(msgHdrSink));
      if (msgHdrSink)
        rv = msgHdrSink->GetDummyMsgHeader(getter_AddRefs(msgHdr));
    }
  }

  // Get a decision on whether or not to allow remote content for this message
  // header.
  *aDecision = ShouldAcceptRemoteContentForMsgHdr(msgHdr, aOriginatorLocation,
                                                  aContentLocation);

  // If we're not allowing the remote content, tell the nsIMsgWindow loading
  // this url that this is the case, so that the UI knows to show the remote
  // content header bar, so the user can override if they wish.
  if (*aDecision == nsIContentPolicy::REJECT_REQUEST)
  {
    nsCOMPtr<nsIMsgWindow> msgWindow;
    (void)mailnewsUrl->GetMsgWindow(getter_AddRefs(msgWindow));
    if (msgWindow)
    {
      nsCOMPtr<nsIRunnable> event =
        new RemoteContentNotifierEvent(msgWindow, msgHdr, aContentLocation);
      // Post this as an event because it can cause dom mutations, and we
      // get called at a bad time to be causing dom mutations.
      if (event)
        NS_DispatchToCurrentThread(event);
    }
  }
}

/**
 * Content policy logic for compose windows
 *
 */
void nsMsgContentPolicy::ComposeShouldLoad(nsIMsgCompose *aMsgCompose,
                                           nsISupports *aRequestingContext,
                                           nsIURI *aContentLocation,
                                           int16_t *aDecision)
{
  NS_PRECONDITION(*aDecision == nsIContentPolicy::REJECT_REQUEST,
                  "ComposeShouldLoad expects default decision to be reject!");

  nsCString originalMsgURI;
  nsresult rv = aMsgCompose->GetOriginalMsgURI(getter_Copies(originalMsgURI));
  NS_ENSURE_SUCCESS_VOID(rv);

  MSG_ComposeType composeType;
  rv = aMsgCompose->GetType(&composeType);
  NS_ENSURE_SUCCESS_VOID(rv);

  // Only allow remote content for new mail compositions or mailto
  // Block remote content for all other types (drafts, templates, forwards, replies, etc)
  // unless there is an associated msgHdr which allows the load, or unless the image is being
  // added by the user and not the quoted message content...
  if (composeType == nsIMsgCompType::New ||
      composeType == nsIMsgCompType::MailToUrl)
    *aDecision = nsIContentPolicy::ACCEPT;
  else if (!originalMsgURI.IsEmpty())
  {
    nsCOMPtr<nsIMsgDBHdr> msgHdr;
    rv = GetMsgDBHdrFromURI(originalMsgURI.get(), getter_AddRefs(msgHdr));
    NS_ENSURE_SUCCESS_VOID(rv);
    *aDecision = ShouldAcceptRemoteContentForMsgHdr(msgHdr, nullptr,
                                                    aContentLocation);

    // Special case image elements. When replying to a message, we want to allow
    // the user to add remote images to the message. But we don't want remote
    // images that are a part of the quoted content to load. Hence we block them
    // while the reply is created (insertingQuotedContent==true), but allow them
    // later when the user inserts them.
    if (*aDecision == nsIContentPolicy::REJECT_REQUEST)
    {
      bool insertingQuotedContent = true;
      aMsgCompose->GetInsertingQuotedContent(&insertingQuotedContent);
      nsCOMPtr<nsIDOMHTMLImageElement> imageElement(do_QueryInterface(aRequestingContext));
      if (imageElement)
      {
        if (!insertingQuotedContent)
        {
          *aDecision = nsIContentPolicy::ACCEPT;
          return;
        }

        // Test whitelist.
        uint32_t permission;
        mPermissionManager->TestPermission(aContentLocation, "image", &permission);
        if (permission == nsIPermissionManager::ALLOW_ACTION)
          *aDecision = nsIContentPolicy::ACCEPT;
      }
    }
  }
}

already_AddRefed<nsIMsgCompose> nsMsgContentPolicy::GetMsgComposeForContext(nsISupports *aRequestingContext)
{
  nsresult rv;

  nsIDocShell *shell = NS_CP_GetDocShellFromContext(aRequestingContext);
  nsCOMPtr<nsIDocShellTreeItem> docShellTreeItem(do_QueryInterface(shell, &rv));
  NS_ENSURE_SUCCESS(rv, nullptr);

  nsCOMPtr<nsIDocShellTreeItem> rootItem;
  rv = docShellTreeItem->GetSameTypeRootTreeItem(getter_AddRefs(rootItem));
  NS_ENSURE_SUCCESS(rv, nullptr);

  nsCOMPtr<nsIDocShell> docShell(do_QueryInterface(rootItem, &rv));
  NS_ENSURE_SUCCESS(rv, nullptr);

  nsCOMPtr<nsIMsgComposeService> composeService(do_GetService(NS_MSGCOMPOSESERVICE_CONTRACTID, &rv));
  NS_ENSURE_SUCCESS(rv, nullptr);

  nsCOMPtr<nsIMsgCompose> msgCompose;
  // Don't bother checking rv, as GetMsgComposeForDocShell returns NS_ERROR_FAILURE
  // for not found.
  composeService->GetMsgComposeForDocShell(docShell,
                                           getter_AddRefs(msgCompose));
  return msgCompose.forget();
}

nsresult nsMsgContentPolicy::SetDisableItemsOnMailNewsUrlDocshells(
  nsIURI *aContentLocation, nsISupports *aRequestingContext)
{
  // XXX if this class changes so that this method can be called from
  // ShouldProcess, and if it's possible for this to be null when called from
  // ShouldLoad, but not in the corresponding ShouldProcess call,
  // we need to re-think the assumptions underlying this code.

  // If there's no docshell to get to, there's nowhere for the JavaScript to
  // run, so we're already safe and don't need to disable anything.
  if (!aRequestingContext) {
    return NS_OK;
  }

  nsresult rv;
  bool isAllowedContent = !ShouldBlockUnexposedProtocol(aContentLocation);
  nsCOMPtr<nsIMsgMessageUrl> msgUrl = do_QueryInterface(aContentLocation, &rv);
  if (NS_FAILED(rv) && !isAllowedContent) {
    // If it's not a mailnews url or allowed content url (http[s]|file) then
    // bail; otherwise set whether js and plugins are allowed.
    return NS_OK;
  }

  // since NS_CP_GetDocShellFromContext returns the containing docshell rather
  // than the contained one we need, we can't use that here, so...
  nsCOMPtr<nsIFrameLoaderOwner> flOwner = do_QueryInterface(aRequestingContext,
                                                            &rv);
  NS_ENSURE_SUCCESS(rv, rv);

  nsCOMPtr<nsIFrameLoader> frameLoader;
  rv = flOwner->GetFrameLoaderXPCOM(getter_AddRefs(frameLoader));
  NS_ENSURE_SUCCESS(rv, rv);
  NS_ENSURE_TRUE(frameLoader, NS_ERROR_INVALID_POINTER);

  nsCOMPtr<nsIDocShell> docShell;
  rv = frameLoader->GetDocShell(getter_AddRefs(docShell));
  NS_ENSURE_SUCCESS(rv, rv);

  nsCOMPtr<nsIDocShellTreeItem> docshellTreeItem(do_QueryInterface(docShell, &rv));
  NS_ENSURE_SUCCESS(rv, rv);

  // what sort of docshell is this?
  int32_t itemType;
  rv = docshellTreeItem->GetItemType(&itemType);
  NS_ENSURE_SUCCESS(rv, rv);

  // we're only worried about policy settings in content docshells
  if (itemType != nsIDocShellTreeItem::typeContent) {
    return NS_OK;
  }

  if (!isAllowedContent) {
    // Disable JavaScript on message URLs.
    rv = docShell->SetAllowJavascript(false);
    NS_ENSURE_SUCCESS(rv, rv);
    rv = docShell->SetAllowContentRetargetingOnChildren(false);
    NS_ENSURE_SUCCESS(rv, rv);
    rv = docShell->SetAllowPlugins(mAllowPlugins);
    NS_ENSURE_SUCCESS(rv, rv);

    uint32_t sandboxFlags;
    rv = docShell->GetSandboxFlags(&sandboxFlags);
    sandboxFlags |= SANDBOXED_FORMS;
    NS_ENSURE_SUCCESS(rv, rv);
    rv = docShell->SetSandboxFlags(sandboxFlags);
    NS_ENSURE_SUCCESS(rv, rv);
  }
  else {
    // JavaScript and plugins are allowed on non-message URLs.
    rv = docShell->SetAllowJavascript(true);
    NS_ENSURE_SUCCESS(rv, rv);
    rv = docShell->SetAllowContentRetargetingOnChildren(true);
    NS_ENSURE_SUCCESS(rv, rv);
    rv = docShell->SetAllowPlugins(true);
    NS_ENSURE_SUCCESS(rv, rv);
  }

  return NS_OK;
}

/**
 * Gets the root docshell from a requesting context.
 */
nsresult
nsMsgContentPolicy::GetRootDocShellForContext(nsISupports *aRequestingContext,
                                              nsIDocShell **aDocShell)
{
  NS_ENSURE_ARG_POINTER(aRequestingContext);
  nsresult rv;

  nsIDocShell *shell = NS_CP_GetDocShellFromContext(aRequestingContext);
  nsCOMPtr<nsIDocShellTreeItem> docshellTreeItem(do_QueryInterface(shell, &rv));
  NS_ENSURE_SUCCESS(rv, rv);

  nsCOMPtr<nsIDocShellTreeItem> rootItem;
  rv = docshellTreeItem->GetRootTreeItem(getter_AddRefs(rootItem));
  NS_ENSURE_SUCCESS(rv, rv);

  return CallQueryInterface(rootItem, aDocShell);
}

/**
 * Gets the originating URI that started off a set of requests, accounting
 * for multiple iframes.
 *
 * Navigates up the docshell tree from aRequestingContext and finds the
 * highest parent with the same type docshell as aRequestingContext, then
 * returns the URI associated with that docshell.
 */
nsresult
nsMsgContentPolicy::GetOriginatingURIForContext(nsISupports *aRequestingContext,
                                                nsIURI **aURI)
{
  NS_ENSURE_ARG_POINTER(aRequestingContext);
  nsresult rv;

  nsIDocShell *shell = NS_CP_GetDocShellFromContext(aRequestingContext);
  nsCOMPtr<nsIDocShellTreeItem> docshellTreeItem(do_QueryInterface(shell, &rv));
  NS_ENSURE_SUCCESS(rv, rv);

  nsCOMPtr<nsIDocShellTreeItem> rootItem;
  rv = docshellTreeItem->GetSameTypeRootTreeItem(getter_AddRefs(rootItem));
  NS_ENSURE_SUCCESS(rv, rv);

  nsCOMPtr<nsIWebNavigation> webNavigation(do_QueryInterface(rootItem, &rv));
  NS_ENSURE_SUCCESS(rv, rv);

  return webNavigation->GetCurrentURI(aURI);
}

NS_IMETHODIMP
nsMsgContentPolicy::ShouldProcess(uint32_t          aContentType,
                                  nsIURI           *aContentLocation,
                                  nsIURI           *aRequestingLocation,
                                  nsISupports      *aRequestingContext,
                                  const nsACString &aMimeGuess,
                                  nsISupports      *aExtra,
                                  nsIPrincipal     *aRequestPrincipal,
                                  int16_t          *aDecision)
{
  // XXX Returning ACCEPT is presumably only a reasonable thing to do if we
  // think that ShouldLoad is going to catch all possible cases (i.e. that
  // everything we use to make decisions is going to be available at
  // ShouldLoad time, and not only become available in time for ShouldProcess).
  // Do we think that's actually the case?
  *aDecision = nsIContentPolicy::ACCEPT;
  return NS_OK;
}

NS_IMETHODIMP nsMsgContentPolicy::Observe(nsISupports *aSubject, const char *aTopic, const char16_t *aData)
{
  if (!strcmp(NS_PREFBRANCH_PREFCHANGE_TOPIC_ID, aTopic))
  {
    NS_LossyConvertUTF16toASCII pref(aData);

    nsresult rv;

    nsCOMPtr<nsIPrefBranch> prefBranchInt = do_QueryInterface(aSubject, &rv);
    NS_ENSURE_SUCCESS(rv, rv);

    if (pref.Equals(kBlockRemoteImages))
      prefBranchInt->GetBoolPref(kBlockRemoteImages, &mBlockRemoteImages);
    if (pref.Equals(kAllowPlugins))
      prefBranchInt->GetBoolPref(kAllowPlugins, &mAllowPlugins);
  }

  return NS_OK;
}

/**
 * We implement the nsIWebProgressListener interface in order to enforce
 * settings at onLocationChange time.
 */
NS_IMETHODIMP
nsMsgContentPolicy::OnStateChange(nsIWebProgress *aWebProgress,
                                  nsIRequest *aRequest, uint32_t aStateFlags,
                                  nsresult aStatus)
{
  return NS_OK;
}

NS_IMETHODIMP
nsMsgContentPolicy::OnProgressChange(nsIWebProgress *aWebProgress,
                                     nsIRequest *aRequest,
                                     int32_t aCurSelfProgress,
                                     int32_t aMaxSelfProgress,
                                     int32_t aCurTotalProgress,
                                     int32_t aMaxTotalProgress)
{
  return NS_OK;
}

NS_IMETHODIMP
nsMsgContentPolicy::OnLocationChange(nsIWebProgress *aWebProgress,
                                     nsIRequest *aRequest, nsIURI *aLocation,
                                     uint32_t aFlags)
{
  nsresult rv;

  // If anything goes wrong and/or there's no docshell associated with this
  // request, just give up.  The behavior ends up being "don't consider
  // re-enabling JS on the docshell", which is the safe thing to do (and if
  // the problem was that there's no docshell, that means that there was
  // nowhere for any JavaScript to run, so we're already safe

  nsCOMPtr<nsIDocShell> docShell = do_QueryInterface(aWebProgress, &rv);
  if (NS_FAILED(rv)) {
    return NS_OK;
  }

#ifdef DEBUG
  nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest, &rv);
  if (NS_SUCCEEDED(rv)) {
    nsCOMPtr<nsIDocShell> docShell2;
    NS_QueryNotificationCallbacks(channel, docShell2);
    NS_ASSERTION(docShell == docShell2, "aWebProgress and channel callbacks"
                                        " do not point to the same docshell");
  }
#endif

  nsCOMPtr<nsIMsgMessageUrl> messageUrl = do_QueryInterface(aLocation, &rv);

  if (NS_SUCCEEDED(rv)) {
    // Disable javascript on message URLs.
    rv = docShell->SetAllowJavascript(false);
    NS_ASSERTION(NS_SUCCEEDED(rv),
                 "Failed to set javascript disabled on docShell");
    // Also disable plugins if the preference requires it.
    rv = docShell->SetAllowPlugins(mAllowPlugins);
    NS_ASSERTION(NS_SUCCEEDED(rv),
                 "Failed to set plugins disabled on docShell");
  }
  else {
    // Disable javascript and plugins are allowed on non-message URLs.
    rv = docShell->SetAllowJavascript(true);
    NS_ASSERTION(NS_SUCCEEDED(rv),
                 "Failed to set javascript allowed on docShell");
    rv = docShell->SetAllowPlugins(true);
    NS_ASSERTION(NS_SUCCEEDED(rv),
                 "Failed to set plugins allowed on docShell");
  }

  return NS_OK;
}

NS_IMETHODIMP
nsMsgContentPolicy::OnStatusChange(nsIWebProgress *aWebProgress,
                                   nsIRequest *aRequest, nsresult aStatus,
                                   const char16_t *aMessage)
{
  return NS_OK;
}

NS_IMETHODIMP
nsMsgContentPolicy::OnSecurityChange(nsIWebProgress *aWebProgress,
                                     nsIRequest *aRequest, uint32_t aState)
{
  return NS_OK;
}

/**
 * Implementation of nsIMsgContentPolicy
 *
 */
NS_IMETHODIMP
nsMsgContentPolicy::AddExposedProtocol(const nsACString &aScheme)
{
  if (mCustomExposedProtocols.Contains(nsCString(aScheme)))
    return NS_OK;

  mCustomExposedProtocols.AppendElement(aScheme);

  return NS_OK;
}

NS_IMETHODIMP
nsMsgContentPolicy::RemoveExposedProtocol(const nsACString &aScheme)
{
  mCustomExposedProtocols.RemoveElement(nsCString(aScheme));

  return NS_OK;
}