/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "HTMLFormSubmission.h"
#include "HTMLFormElement.h"
#include "HTMLFormSubmissionConstants.h"
#include "nsCOMPtr.h"
#include "nsComponentManagerUtils.h"
#include "nsGkAtoms.h"
#include "nsIFormControl.h"
#include "nsError.h"
#include "nsGenericHTMLElement.h"
#include "nsAttrValueInlines.h"
#include "nsDirectoryServiceDefs.h"
#include "nsStringStream.h"
#include "nsIURI.h"
#include "nsIURIMutator.h"
#include "nsIURL.h"
#include "nsNetUtil.h"
#include "nsLinebreakConverter.h"
#include "nsEscape.h"
#include "nsUnicharUtils.h"
#include "nsIMultiplexInputStream.h"
#include "nsIMIMEInputStream.h"
#include "nsIScriptError.h"
#include "nsCExternalHandlerService.h"
#include "nsContentUtils.h"

#include "mozilla/dom/Document.h"
#include "mozilla/dom/AncestorIterator.h"
#include "mozilla/dom/Directory.h"
#include "mozilla/dom/File.h"
#include "mozilla/StaticPrefs_dom.h"
#include "mozilla/RandomNum.h"

#include <tuple>

namespace mozilla::dom {

namespace {

void SendJSWarning(Document* aDocument, const char* aWarningName,
                   const nsTArray<nsString>& aWarningArgs) {
  nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, "HTML"_ns,
                                  aDocument, nsContentUtils::eFORMS_PROPERTIES,
                                  aWarningName, aWarningArgs);
}

void RetrieveFileName(Blob* aBlob, nsAString& aFilename) {
  if (!aBlob) {
    return;
  }

  RefPtr<File> file = aBlob->ToFile();
  if (file) {
    file->GetName(aFilename);
  }
}

void RetrieveDirectoryName(Directory* aDirectory, nsAString& aDirname) {
  MOZ_ASSERT(aDirectory);

  ErrorResult rv;
  aDirectory->GetName(aDirname, rv);
  if (NS_WARN_IF(rv.Failed())) {
    rv.SuppressException();
    aDirname.Truncate();
  }
}

// --------------------------------------------------------------------------

class FSURLEncoded : public EncodingFormSubmission {
 public:
  /**
   * @param aEncoding the character encoding of the form
   * @param aMethod the method of the submit (either NS_FORM_METHOD_GET or
   *        NS_FORM_METHOD_POST).
   */
  FSURLEncoded(nsIURI* aActionURL, const nsAString& aTarget,
               NotNull<const Encoding*> aEncoding, int32_t aMethod,
               Document* aDocument, Element* aSubmitter)
      : EncodingFormSubmission(aActionURL, aTarget, aEncoding, aSubmitter),
        mMethod(aMethod),
        mDocument(aDocument),
        mWarnedFileControl(false) {}

  virtual nsresult AddNameValuePair(const nsAString& aName,
                                    const nsAString& aValue) override;

  virtual nsresult AddNameBlobPair(const nsAString& aName,
                                   Blob* aBlob) override;

  virtual nsresult AddNameDirectoryPair(const nsAString& aName,
                                        Directory* aDirectory) override;

  virtual nsresult GetEncodedSubmission(nsIURI* aURI,
                                        nsIInputStream** aPostDataStream,
                                        nsCOMPtr<nsIURI>& aOutURI) override;

 protected:
  /**
   * URL encode a Unicode string by encoding it to bytes, converting linebreaks
   * properly, and then escaping many bytes as %xx.
   *
   * @param aStr the string to encode
   * @param aEncoded the encoded string [OUT]
   * @throws NS_ERROR_OUT_OF_MEMORY if we run out of memory
   */
  nsresult URLEncode(const nsAString& aStr, nsACString& aEncoded);

 private:
  /**
   * The method of the submit (either NS_FORM_METHOD_GET or
   * NS_FORM_METHOD_POST).
   */
  int32_t mMethod;

  /** The query string so far (the part after the ?) */
  nsCString mQueryString;

  /** The document whose URI to use when reporting errors */
  nsCOMPtr<Document> mDocument;

  /** Whether or not we have warned about a file control not being submitted */
  bool mWarnedFileControl;
};

nsresult FSURLEncoded::AddNameValuePair(const nsAString& aName,
                                        const nsAString& aValue) {
  // Encode value
  nsCString convValue;
  nsresult rv = URLEncode(aValue, convValue);
  NS_ENSURE_SUCCESS(rv, rv);

  // Encode name
  nsAutoCString convName;
  rv = URLEncode(aName, convName);
  NS_ENSURE_SUCCESS(rv, rv);

  // Append data to string
  if (mQueryString.IsEmpty()) {
    mQueryString += convName + "="_ns + convValue;
  } else {
    mQueryString += "&"_ns + convName + "="_ns + convValue;
  }

  return NS_OK;
}

nsresult FSURLEncoded::AddNameBlobPair(const nsAString& aName, Blob* aBlob) {
  if (!mWarnedFileControl) {
    SendJSWarning(mDocument, "ForgotFileEnctypeWarning", nsTArray<nsString>());
    mWarnedFileControl = true;
  }

  nsAutoString filename;
  RetrieveFileName(aBlob, filename);
  return AddNameValuePair(aName, filename);
}

nsresult FSURLEncoded::AddNameDirectoryPair(const nsAString& aName,
                                            Directory* aDirectory) {
  // No warning about because Directory objects are never sent via form.

  nsAutoString dirname;
  RetrieveDirectoryName(aDirectory, dirname);
  return AddNameValuePair(aName, dirname);
}

void HandleMailtoSubject(nsCString& aPath) {
  // Walk through the string and see if we have a subject already.
  bool hasSubject = false;
  bool hasParams = false;
  int32_t paramSep = aPath.FindChar('?');
  while (paramSep != kNotFound && paramSep < (int32_t)aPath.Length()) {
    hasParams = true;

    // Get the end of the name at the = op.  If it is *after* the next &,
    // assume that someone made a parameter without an = in it
    int32_t nameEnd = aPath.FindChar('=', paramSep + 1);
    int32_t nextParamSep = aPath.FindChar('&', paramSep + 1);
    if (nextParamSep == kNotFound) {
      nextParamSep = aPath.Length();
    }

    // If the = op is after the &, this parameter is a name without value.
    // If there is no = op, same thing.
    if (nameEnd == kNotFound || nextParamSep < nameEnd) {
      nameEnd = nextParamSep;
    }

    if (nameEnd != kNotFound) {
      if (Substring(aPath, paramSep + 1, nameEnd - (paramSep + 1))
              .LowerCaseEqualsLiteral("subject")) {
        hasSubject = true;
        break;
      }
    }

    paramSep = nextParamSep;
  }

  // If there is no subject, append a preformed subject to the mailto line
  if (!hasSubject) {
    if (hasParams) {
      aPath.Append('&');
    } else {
      aPath.Append('?');
    }

    // Get the default subject
    nsAutoString brandName;
    nsresult rv = nsContentUtils::GetLocalizedString(
        nsContentUtils::eBRAND_PROPERTIES, "brandShortName", brandName);
    if (NS_FAILED(rv)) return;
    nsAutoString subjectStr;
    rv = nsContentUtils::FormatLocalizedString(
        subjectStr, nsContentUtils::eFORMS_PROPERTIES, "DefaultFormSubject",
        brandName);
    if (NS_FAILED(rv)) return;
    aPath.AppendLiteral("subject=");
    nsCString subjectStrEscaped;
    rv = NS_EscapeURL(NS_ConvertUTF16toUTF8(subjectStr), esc_Query,
                      subjectStrEscaped, mozilla::fallible);
    if (NS_FAILED(rv)) return;

    aPath.Append(subjectStrEscaped);
  }
}

nsresult FSURLEncoded::GetEncodedSubmission(nsIURI* aURI,
                                            nsIInputStream** aPostDataStream,
                                            nsCOMPtr<nsIURI>& aOutURI) {
  nsresult rv = NS_OK;
  aOutURI = aURI;

  *aPostDataStream = nullptr;

  if (mMethod == NS_FORM_METHOD_POST) {
    if (aURI->SchemeIs("mailto")) {
      nsAutoCString path;
      rv = aURI->GetPathQueryRef(path);
      NS_ENSURE_SUCCESS(rv, rv);

      HandleMailtoSubject(path);

      // Append the body to and force-plain-text args to the mailto line
      nsAutoCString escapedBody;
      if (NS_WARN_IF(!NS_Escape(mQueryString, escapedBody, url_XAlphas))) {
        return NS_ERROR_OUT_OF_MEMORY;
      }

      path += "&force-plain-text=Y&body="_ns + escapedBody;

      return NS_MutateURI(aURI).SetPathQueryRef(path).Finalize(aOutURI);
    } else {
      nsCOMPtr<nsIInputStream> dataStream;
      rv = NS_NewCStringInputStream(getter_AddRefs(dataStream),
                                    std::move(mQueryString));
      NS_ENSURE_SUCCESS(rv, rv);
      mQueryString.Truncate();

      nsCOMPtr<nsIMIMEInputStream> mimeStream(
          do_CreateInstance("@mozilla.org/network/mime-input-stream;1", &rv));
      NS_ENSURE_SUCCESS(rv, rv);

      mimeStream->AddHeader("Content-Type",
                            "application/x-www-form-urlencoded");
      mimeStream->SetData(dataStream);

      mimeStream.forget(aPostDataStream);
    }

  } else {
    // Get the full query string
    if (aURI->SchemeIs("javascript")) {
      return NS_OK;
    }

    nsCOMPtr<nsIURL> url = do_QueryInterface(aURI);
    if (url) {
      // Make sure that we end up with a query component in the URL.  If
      // mQueryString is empty, nsIURI::SetQuery() will remove the query
      // component, which is not what we want.
      rv = NS_MutateURI(aURI)
               .SetQuery(mQueryString.IsEmpty() ? "?"_ns : mQueryString)
               .Finalize(aOutURI);
    } else {
      nsAutoCString path;
      rv = aURI->GetPathQueryRef(path);
      NS_ENSURE_SUCCESS(rv, rv);
      // Bug 42616: Trim off named anchor and save it to add later
      int32_t namedAnchorPos = path.FindChar('#');
      nsAutoCString namedAnchor;
      if (kNotFound != namedAnchorPos) {
        path.Right(namedAnchor, (path.Length() - namedAnchorPos));
        path.Truncate(namedAnchorPos);
      }

      // Chop off old query string (bug 25330, 57333)
      // Only do this for GET not POST (bug 41585)
      int32_t queryStart = path.FindChar('?');
      if (kNotFound != queryStart) {
        path.Truncate(queryStart);
      }

      path.Append('?');
      // Bug 42616: Add named anchor to end after query string
      path.Append(mQueryString + namedAnchor);

      rv = NS_MutateURI(aURI).SetPathQueryRef(path).Finalize(aOutURI);
    }
  }

  return rv;
}

// i18n helper routines
nsresult FSURLEncoded::URLEncode(const nsAString& aStr, nsACString& aEncoded) {
  nsAutoCString encodedBuf;
  // We encode with eValueEncode because the urlencoded format needs the newline
  // normalizations but percent-escapes characters that eNameEncode doesn't,
  // so calling NS_Escape would still be needed.
  nsresult rv = EncodeVal(aStr, encodedBuf, EncodeType::eValueEncode);
  NS_ENSURE_SUCCESS(rv, rv);

  if (NS_WARN_IF(!NS_Escape(encodedBuf, aEncoded, url_XPAlphas))) {
    return NS_ERROR_OUT_OF_MEMORY;
  }

  return NS_OK;
}

}  // anonymous namespace

// --------------------------------------------------------------------------

FSMultipartFormData::FSMultipartFormData(nsIURI* aActionURL,
                                         const nsAString& aTarget,
                                         NotNull<const Encoding*> aEncoding,
                                         Element* aSubmitter)
    : EncodingFormSubmission(aActionURL, aTarget, aEncoding, aSubmitter) {
  mPostData = do_CreateInstance("@mozilla.org/io/multiplex-input-stream;1");

  nsCOMPtr<nsIInputStream> inputStream = do_QueryInterface(mPostData);
  MOZ_ASSERT(SameCOMIdentity(mPostData, inputStream));
  mPostDataStream = inputStream;

  mTotalLength = 0;

  mBoundary.AssignLiteral("---------------------------");
  mBoundary.AppendInt(static_cast<uint32_t>(mozilla::RandomUint64OrDie()));
  mBoundary.AppendInt(static_cast<uint32_t>(mozilla::RandomUint64OrDie()));
  mBoundary.AppendInt(static_cast<uint32_t>(mozilla::RandomUint64OrDie()));
}

FSMultipartFormData::~FSMultipartFormData() {
  NS_ASSERTION(mPostDataChunk.IsEmpty(), "Left unsubmitted data");
}

nsIInputStream* FSMultipartFormData::GetSubmissionBody(
    uint64_t* aContentLength) {
  // Finish data
  mPostDataChunk += "--"_ns + mBoundary + nsLiteralCString("--" CRLF);

  // Add final data input stream
  AddPostDataStream();

  *aContentLength = mTotalLength;
  return mPostDataStream;
}

nsresult FSMultipartFormData::AddNameValuePair(const nsAString& aName,
                                               const nsAString& aValue) {
  nsAutoCString encodedVal;
  nsresult rv = EncodeVal(aValue, encodedVal, EncodeType::eValueEncode);
  NS_ENSURE_SUCCESS(rv, rv);

  nsAutoCString nameStr;
  rv = EncodeVal(aName, nameStr, EncodeType::eNameEncode);
  NS_ENSURE_SUCCESS(rv, rv);

  // Make MIME block for name/value pair

  mPostDataChunk += "--"_ns + mBoundary + nsLiteralCString(CRLF) +
                    "Content-Disposition: form-data; name=\""_ns + nameStr +
                    nsLiteralCString("\"" CRLF CRLF) + encodedVal +
                    nsLiteralCString(CRLF);

  return NS_OK;
}

nsresult FSMultipartFormData::AddNameBlobPair(const nsAString& aName,
                                              Blob* aBlob) {
  MOZ_ASSERT(aBlob);

  // Encode the control name
  nsAutoCString nameStr;
  nsresult rv = EncodeVal(aName, nameStr, EncodeType::eNameEncode);
  NS_ENSURE_SUCCESS(rv, rv);

  ErrorResult error;

  uint64_t size = 0;
  nsAutoCString filename;
  nsAutoCString contentType;
  nsCOMPtr<nsIInputStream> fileStream;
  nsAutoString filename16;

  RefPtr<File> file = aBlob->ToFile();
  if (file) {
    nsAutoString relativePath;
    file->GetRelativePath(relativePath);
    if (StaticPrefs::dom_webkitBlink_dirPicker_enabled() &&
        !relativePath.IsEmpty()) {
      filename16 = relativePath;
    }

    if (filename16.IsEmpty()) {
      RetrieveFileName(aBlob, filename16);
    }
  }

  rv = EncodeVal(filename16, filename, EncodeType::eFilenameEncode);
  NS_ENSURE_SUCCESS(rv, rv);

  // Get content type
  nsAutoString contentType16;
  aBlob->GetType(contentType16);
  if (contentType16.IsEmpty()) {
    contentType16.AssignLiteral("application/octet-stream");
  }

  NS_ConvertUTF16toUTF8 contentType8(contentType16);
  int32_t convertedBufLength = 0;
  char* convertedBuf = nsLinebreakConverter::ConvertLineBreaks(
      contentType8.get(), nsLinebreakConverter::eLinebreakAny,
      nsLinebreakConverter::eLinebreakSpace, contentType8.Length(),
      &convertedBufLength);
  contentType.Adopt(convertedBuf, convertedBufLength);

  // Get input stream
  aBlob->CreateInputStream(getter_AddRefs(fileStream), error);
  if (NS_WARN_IF(error.Failed())) {
    return error.StealNSResult();
  }

  // Get size
  size = aBlob->GetSize(error);
  if (error.Failed()) {
    error.SuppressException();
    fileStream = nullptr;
  }

  if (fileStream) {
    // Create buffered stream (for efficiency)
    nsCOMPtr<nsIInputStream> bufferedStream;
    rv = NS_NewBufferedInputStream(getter_AddRefs(bufferedStream),
                                   fileStream.forget(), 8192);
    NS_ENSURE_SUCCESS(rv, rv);

    fileStream = bufferedStream;
  }

  AddDataChunk(nameStr, filename, contentType, fileStream, size);
  return NS_OK;
}

nsresult FSMultipartFormData::AddNameDirectoryPair(const nsAString& aName,
                                                   Directory* aDirectory) {
  if (!StaticPrefs::dom_webkitBlink_dirPicker_enabled()) {
    return NS_OK;
  }

  // Encode the control name
  nsAutoCString nameStr;
  nsresult rv = EncodeVal(aName, nameStr, EncodeType::eNameEncode);
  NS_ENSURE_SUCCESS(rv, rv);

  nsAutoCString dirname;
  nsAutoString dirname16;

  ErrorResult error;
  nsAutoString path;
  aDirectory->GetPath(path, error);
  if (NS_WARN_IF(error.Failed())) {
    error.SuppressException();
  } else {
    dirname16 = path;
  }

  if (dirname16.IsEmpty()) {
    RetrieveDirectoryName(aDirectory, dirname16);
  }

  rv = EncodeVal(dirname16, dirname, EncodeType::eFilenameEncode);
  NS_ENSURE_SUCCESS(rv, rv);

  AddDataChunk(nameStr, dirname, "application/octet-stream"_ns, nullptr, 0);
  return NS_OK;
}

void FSMultipartFormData::AddDataChunk(const nsACString& aName,
                                       const nsACString& aFilename,
                                       const nsACString& aContentType,
                                       nsIInputStream* aInputStream,
                                       uint64_t aInputStreamSize) {
  //
  // Make MIME block for name/value pair
  //
  // more appropriate than always using binary?
  mPostDataChunk += "--"_ns + mBoundary + nsLiteralCString(CRLF);
  mPostDataChunk += "Content-Disposition: form-data; name=\""_ns + aName +
                    "\"; filename=\""_ns + aFilename +
                    nsLiteralCString("\"" CRLF) + "Content-Type: "_ns +
                    aContentType + nsLiteralCString(CRLF CRLF);

  // We should not try to append an invalid stream. That will happen for example
  // if we try to update a file that actually do not exist.
  if (aInputStream) {
    // We need to dump the data up to this point into the POST data stream
    // here, since we're about to add the file input stream
    AddPostDataStream();

    mPostData->AppendStream(aInputStream);
    mTotalLength += aInputStreamSize;
  }

  // CRLF after file
  mPostDataChunk.AppendLiteral(CRLF);
}

nsresult FSMultipartFormData::GetEncodedSubmission(
    nsIURI* aURI, nsIInputStream** aPostDataStream, nsCOMPtr<nsIURI>& aOutURI) {
  nsresult rv;
  aOutURI = aURI;

  // Make header
  nsCOMPtr<nsIMIMEInputStream> mimeStream =
      do_CreateInstance("@mozilla.org/network/mime-input-stream;1", &rv);
  NS_ENSURE_SUCCESS(rv, rv);

  nsAutoCString contentType;
  GetContentType(contentType);
  mimeStream->AddHeader("Content-Type", contentType.get());

  uint64_t bodySize;
  mimeStream->SetData(GetSubmissionBody(&bodySize));

  mimeStream.forget(aPostDataStream);

  return NS_OK;
}

nsresult FSMultipartFormData::AddPostDataStream() {
  nsresult rv = NS_OK;

  nsCOMPtr<nsIInputStream> postDataChunkStream;
  rv = NS_NewCStringInputStream(getter_AddRefs(postDataChunkStream),
                                mPostDataChunk);
  NS_ASSERTION(postDataChunkStream, "Could not open a stream for POST!");
  if (postDataChunkStream) {
    mPostData->AppendStream(postDataChunkStream);
    mTotalLength += mPostDataChunk.Length();
  }

  mPostDataChunk.Truncate();

  return rv;
}

// --------------------------------------------------------------------------

namespace {

class FSTextPlain : public EncodingFormSubmission {
 public:
  FSTextPlain(nsIURI* aActionURL, const nsAString& aTarget,
              NotNull<const Encoding*> aEncoding, Element* aSubmitter)
      : EncodingFormSubmission(aActionURL, aTarget, aEncoding, aSubmitter) {}

  virtual nsresult AddNameValuePair(const nsAString& aName,
                                    const nsAString& aValue) override;

  virtual nsresult AddNameBlobPair(const nsAString& aName,
                                   Blob* aBlob) override;

  virtual nsresult AddNameDirectoryPair(const nsAString& aName,
                                        Directory* aDirectory) override;

  virtual nsresult GetEncodedSubmission(nsIURI* aURI,
                                        nsIInputStream** aPostDataStream,
                                        nsCOMPtr<nsIURI>& aOutURI) override;

 private:
  nsString mBody;
};

nsresult FSTextPlain::AddNameValuePair(const nsAString& aName,
                                       const nsAString& aValue) {
  // XXX This won't work well with a name like "a=b" or "a\nb" but I suppose
  // text/plain doesn't care about that.  Parsers aren't built for escaped
  // values so we'll have to live with it.
  mBody.Append(aName + u"="_ns + aValue + NS_LITERAL_STRING_FROM_CSTRING(CRLF));

  return NS_OK;
}

nsresult FSTextPlain::AddNameBlobPair(const nsAString& aName, Blob* aBlob) {
  nsAutoString filename;
  RetrieveFileName(aBlob, filename);
  AddNameValuePair(aName, filename);
  return NS_OK;
}

nsresult FSTextPlain::AddNameDirectoryPair(const nsAString& aName,
                                           Directory* aDirectory) {
  nsAutoString dirname;
  RetrieveDirectoryName(aDirectory, dirname);
  AddNameValuePair(aName, dirname);
  return NS_OK;
}

nsresult FSTextPlain::GetEncodedSubmission(nsIURI* aURI,
                                           nsIInputStream** aPostDataStream,
                                           nsCOMPtr<nsIURI>& aOutURI) {
  nsresult rv = NS_OK;
  aOutURI = aURI;

  *aPostDataStream = nullptr;

  // XXX HACK We are using the standard URL mechanism to give the body to the
  // mailer instead of passing the post data stream to it, since that sounds
  // hard.
  if (aURI->SchemeIs("mailto")) {
    nsAutoCString path;
    rv = aURI->GetPathQueryRef(path);
    NS_ENSURE_SUCCESS(rv, rv);

    HandleMailtoSubject(path);

    // Append the body to and force-plain-text args to the mailto line
    nsAutoCString escapedBody;
    if (NS_WARN_IF(!NS_Escape(NS_ConvertUTF16toUTF8(mBody), escapedBody,
                              url_XAlphas))) {
      return NS_ERROR_OUT_OF_MEMORY;
    }

    path += "&force-plain-text=Y&body="_ns + escapedBody;

    rv = NS_MutateURI(aURI).SetPathQueryRef(path).Finalize(aOutURI);
  } else {
    // Create data stream.
    // We use eValueEncode to send the data through the charset encoder and to
    // normalize linebreaks to use the "standard net" format (\r\n), but not
    // perform any other escaping. This means that names and values which
    // contain '=' or newlines are potentially ambiguously encoded, but that is
    // how text/plain is specced.
    nsCString cbody;
    EncodeVal(mBody, cbody, EncodeType::eValueEncode);

    nsCOMPtr<nsIInputStream> bodyStream;
    rv = NS_NewCStringInputStream(getter_AddRefs(bodyStream), std::move(cbody));
    if (!bodyStream) {
      return NS_ERROR_OUT_OF_MEMORY;
    }

    // Create mime stream with headers and such
    nsCOMPtr<nsIMIMEInputStream> mimeStream =
        do_CreateInstance("@mozilla.org/network/mime-input-stream;1", &rv);
    NS_ENSURE_SUCCESS(rv, rv);

    mimeStream->AddHeader("Content-Type", "text/plain");
    mimeStream->SetData(bodyStream);
    mimeStream.forget(aPostDataStream);
  }

  return rv;
}

}  // anonymous namespace

// --------------------------------------------------------------------------

HTMLFormSubmission::HTMLFormSubmission(
    nsIURI* aActionURL, const nsAString& aTarget,
    mozilla::NotNull<const mozilla::Encoding*> aEncoding)
    : mActionURL(aActionURL),
      mTarget(aTarget),
      mEncoding(aEncoding),
      mInitiatedFromUserInput(UserActivation::IsHandlingUserInput()) {
  MOZ_COUNT_CTOR(HTMLFormSubmission);
}

EncodingFormSubmission::EncodingFormSubmission(
    nsIURI* aActionURL, const nsAString& aTarget,
    NotNull<const Encoding*> aEncoding, Element* aSubmitter)
    : HTMLFormSubmission(aActionURL, aTarget, aEncoding) {
  if (!aEncoding->CanEncodeEverything()) {
    nsAutoCString name;
    aEncoding->Name(name);
    AutoTArray<nsString, 1> args;
    CopyUTF8toUTF16(name, *args.AppendElement());
    SendJSWarning(aSubmitter ? aSubmitter->GetOwnerDocument() : nullptr,
                  "CannotEncodeAllUnicode", args);
  }
}

EncodingFormSubmission::~EncodingFormSubmission() = default;

// i18n helper routines
nsresult EncodingFormSubmission::EncodeVal(const nsAString& aStr,
                                           nsCString& aOut,
                                           EncodeType aEncodeType) {
  nsresult rv;
  std::tie(rv, std::ignore) = mEncoding->Encode(aStr, aOut);
  if (NS_FAILED(rv)) {
    return rv;
  }

  if (aEncodeType != EncodeType::eFilenameEncode) {
    // Normalize newlines
    int32_t convertedBufLength = 0;
    char* convertedBuf = nsLinebreakConverter::ConvertLineBreaks(
        aOut.get(), nsLinebreakConverter::eLinebreakAny,
        nsLinebreakConverter::eLinebreakNet, (int32_t)aOut.Length(),
        &convertedBufLength);
    aOut.Adopt(convertedBuf, convertedBufLength);
  }

  if (aEncodeType != EncodeType::eValueEncode) {
    // Percent-escape LF, CR and double quotes.
    int32_t offset = 0;
    while ((offset = aOut.FindCharInSet("\n\r\"", offset)) != kNotFound) {
      if (aOut[offset] == '\n') {
        aOut.ReplaceLiteral(offset, 1, "%0A");
      } else if (aOut[offset] == '\r') {
        aOut.ReplaceLiteral(offset, 1, "%0D");
      } else if (aOut[offset] == '"') {
        aOut.ReplaceLiteral(offset, 1, "%22");
      } else {
        MOZ_ASSERT(false);
        offset++;
        continue;
      }
    }
  }

  return NS_OK;
}

// --------------------------------------------------------------------------

namespace {

void GetEnumAttr(nsGenericHTMLElement* aContent, nsAtom* atom,
                 int32_t* aValue) {
  const nsAttrValue* value = aContent->GetParsedAttr(atom);
  if (value && value->Type() == nsAttrValue::eEnum) {
    *aValue = value->GetEnumValue();
  }
}

}  // anonymous namespace

/* static */
nsresult HTMLFormSubmission::GetFromForm(HTMLFormElement* aForm,
                                         nsGenericHTMLElement* aSubmitter,
                                         NotNull<const Encoding*>& aEncoding,
                                         HTMLFormSubmission** aFormSubmission) {
  // Get all the information necessary to encode the form data
  NS_ASSERTION(aForm->GetComposedDoc(),
               "Should have doc if we're building submission!");

  nsresult rv;

  // Get action
  nsCOMPtr<nsIURI> actionURL;
  rv = aForm->GetActionURL(getter_AddRefs(actionURL), aSubmitter);
  NS_ENSURE_SUCCESS(rv, rv);

  // Check if CSP allows this form-action
  nsCOMPtr<nsIContentSecurityPolicy> csp = aForm->GetCsp();
  if (csp) {
    bool permitsFormAction = true;

    // form-action is only enforced if explicitly defined in the
    // policy - do *not* consult default-src, see:
    // http://www.w3.org/TR/CSP2/#directive-default-src
    rv = csp->Permits(aForm, nullptr /* nsICSPEventListener */, actionURL,
                      nsIContentSecurityPolicy::FORM_ACTION_DIRECTIVE,
                      true /* aSpecific */, true /* aSendViolationReports */,
                      &permitsFormAction);
    NS_ENSURE_SUCCESS(rv, rv);
    if (!permitsFormAction) {
      return NS_ERROR_CSP_FORM_ACTION_VIOLATION;
    }
  }

  // Get target
  // The target is the submitter element formtarget attribute if the element
  // is a submit control and has such an attribute.
  // Otherwise, the target is the form owner's target attribute,
  // if it has such an attribute.
  // Finally, if one of the child nodes of the head element is a base element
  // with a target attribute, then the value of the target attribute of the
  // first such base element; or, if there is no such element, the empty string.
  nsAutoString target;
  if (!(aSubmitter && aSubmitter->GetAttr(nsGkAtoms::formtarget, target)) &&
      !aForm->GetAttr(nsGkAtoms::target, target)) {
    aForm->GetBaseTarget(target);
  }

  // Get encoding type (default: urlencoded)
  int32_t enctype = NS_FORM_ENCTYPE_URLENCODED;
  if (aSubmitter && aSubmitter->HasAttr(nsGkAtoms::formenctype)) {
    GetEnumAttr(aSubmitter, nsGkAtoms::formenctype, &enctype);
  } else {
    GetEnumAttr(aForm, nsGkAtoms::enctype, &enctype);
  }

  // Get method (default: GET)
  int32_t method = NS_FORM_METHOD_GET;
  if (aSubmitter && aSubmitter->HasAttr(nsGkAtoms::formmethod)) {
    GetEnumAttr(aSubmitter, nsGkAtoms::formmethod, &method);
  } else {
    GetEnumAttr(aForm, nsGkAtoms::method, &method);
  }

  if (method == NS_FORM_METHOD_DIALOG) {
    HTMLDialogElement* dialog = aForm->FirstAncestorOfType<HTMLDialogElement>();

    // If there isn't one, or if it does not have an open attribute, do
    // nothing.
    if (!dialog || !dialog->Open()) {
      return NS_ERROR_FAILURE;
    }

    nsAutoString result;
    if (aSubmitter) {
      aSubmitter->ResultForDialogSubmit(result);
    }
    *aFormSubmission =
        new DialogFormSubmission(result, actionURL, target, aEncoding, dialog);
    return NS_OK;
  }

  MOZ_ASSERT(method != NS_FORM_METHOD_DIALOG);

  // Choose encoder
  if (method == NS_FORM_METHOD_POST && enctype == NS_FORM_ENCTYPE_MULTIPART) {
    *aFormSubmission =
        new FSMultipartFormData(actionURL, target, aEncoding, aSubmitter);
  } else if (method == NS_FORM_METHOD_POST &&
             enctype == NS_FORM_ENCTYPE_TEXTPLAIN) {
    *aFormSubmission =
        new FSTextPlain(actionURL, target, aEncoding, aSubmitter);
  } else {
    Document* doc = aForm->OwnerDoc();
    if (enctype == NS_FORM_ENCTYPE_MULTIPART ||
        enctype == NS_FORM_ENCTYPE_TEXTPLAIN) {
      AutoTArray<nsString, 1> args;
      nsString& enctypeStr = *args.AppendElement();
      if (aSubmitter && aSubmitter->HasAttr(nsGkAtoms::formenctype)) {
        aSubmitter->GetAttr(nsGkAtoms::formenctype, enctypeStr);
      } else {
        aForm->GetAttr(nsGkAtoms::enctype, enctypeStr);
      }

      SendJSWarning(doc, "ForgotPostWarning", args);
    }
    *aFormSubmission =
        new FSURLEncoded(actionURL, target, aEncoding, method, doc, aSubmitter);
  }

  return NS_OK;
}

}  // namespace mozilla::dom
