diff options
Diffstat (limited to '')
-rw-r--r-- | netwerk/streamconv/converters/moz.build | 31 | ||||
-rw-r--r-- | netwerk/streamconv/converters/mozTXTToHTMLConv.cpp | 1291 | ||||
-rw-r--r-- | netwerk/streamconv/converters/mozTXTToHTMLConv.h | 286 | ||||
-rw-r--r-- | netwerk/streamconv/converters/nsDirIndex.cpp | 62 | ||||
-rw-r--r-- | netwerk/streamconv/converters/nsDirIndex.h | 32 | ||||
-rw-r--r-- | netwerk/streamconv/converters/nsDirIndexParser.cpp | 266 | ||||
-rw-r--r-- | netwerk/streamconv/converters/nsDirIndexParser.h | 65 | ||||
-rw-r--r-- | netwerk/streamconv/converters/nsHTTPCompressConv.cpp | 767 | ||||
-rw-r--r-- | netwerk/streamconv/converters/nsHTTPCompressConv.h | 109 | ||||
-rw-r--r-- | netwerk/streamconv/converters/nsICompressConvStats.idl | 17 | ||||
-rw-r--r-- | netwerk/streamconv/converters/nsIndexedToHTML.cpp | 825 | ||||
-rw-r--r-- | netwerk/streamconv/converters/nsIndexedToHTML.h | 61 | ||||
-rw-r--r-- | netwerk/streamconv/converters/nsMultiMixedConv.cpp | 1051 | ||||
-rw-r--r-- | netwerk/streamconv/converters/nsMultiMixedConv.h | 259 | ||||
-rw-r--r-- | netwerk/streamconv/converters/nsUnknownDecoder.cpp | 867 | ||||
-rw-r--r-- | netwerk/streamconv/converters/nsUnknownDecoder.h | 150 |
16 files changed, 6139 insertions, 0 deletions
diff --git a/netwerk/streamconv/converters/moz.build b/netwerk/streamconv/converters/moz.build new file mode 100644 index 0000000000..d985eb363a --- /dev/null +++ b/netwerk/streamconv/converters/moz.build @@ -0,0 +1,31 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +XPIDL_SOURCES += ["nsICompressConvStats.idl"] + +EXPORTS += [ + "nsHTTPCompressConv.h", + "nsUnknownDecoder.h", +] + +XPIDL_MODULE = "necko_http" + +UNIFIED_SOURCES += [ + "mozTXTToHTMLConv.cpp", + "nsDirIndex.cpp", + "nsDirIndexParser.cpp", + "nsHTTPCompressConv.cpp", + "nsIndexedToHTML.cpp", + "nsMultiMixedConv.cpp", + "nsUnknownDecoder.cpp", +] + +FINAL_LIBRARY = "xul" + +LOCAL_INCLUDES += [ + "/modules/brotli/dec", + "/netwerk/base", +] diff --git a/netwerk/streamconv/converters/mozTXTToHTMLConv.cpp b/netwerk/streamconv/converters/mozTXTToHTMLConv.cpp new file mode 100644 index 0000000000..a9f1d5183f --- /dev/null +++ b/netwerk/streamconv/converters/mozTXTToHTMLConv.cpp @@ -0,0 +1,1291 @@ +/* -*- 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 "mozilla/TextUtils.h" +#include "mozTXTToHTMLConv.h" +#include "mozilla/intl/Segmenter.h" +#include "mozilla/Maybe.h" +#include "nsIThreadRetargetableStreamListener.h" +#include "nsNetUtil.h" +#include "nsUnicharUtils.h" +#include "nsUnicodeProperties.h" +#include "nsCRT.h" +#include "nsIExternalProtocolHandler.h" +#include "nsIURI.h" + +#include <algorithm> + +#ifdef DEBUG_BenB_Perf +# include "prtime.h" +# include "prinrval.h" +#endif + +using mozilla::IsAscii; +using mozilla::IsAsciiAlpha; +using mozilla::IsAsciiDigit; +using mozilla::Maybe; +using mozilla::Some; +using mozilla::Span; +using mozilla::intl::GraphemeClusterBreakIteratorUtf16; +using mozilla::intl::GraphemeClusterBreakReverseIteratorUtf16; + +const double growthRate = 1.2; + +// Bug 183111, editor now replaces multiple spaces with leading +// 0xA0's and a single ending space, so need to treat 0xA0's as spaces. +// 0xA0 is the Latin1/Unicode character for "non-breaking space (nbsp)" +// Also recognize the Japanese ideographic space 0x3000 as a space. +static inline bool IsSpace(const char16_t aChar) { + return (nsCRT::IsAsciiSpace(aChar) || aChar == 0xA0 || aChar == 0x3000); +} + +// Escape Char will take ch, escape it and append the result to +// aStringToAppendTo +void mozTXTToHTMLConv::EscapeChar(const char16_t ch, + nsAString& aStringToAppendTo, + bool inAttribute) { + switch (ch) { + case '<': + aStringToAppendTo.AppendLiteral("<"); + break; + case '>': + aStringToAppendTo.AppendLiteral(">"); + break; + case '&': + aStringToAppendTo.AppendLiteral("&"); + break; + case '"': + if (inAttribute) { + aStringToAppendTo.AppendLiteral("""); + break; + } + // else fall through + [[fallthrough]]; + default: + aStringToAppendTo += ch; + } +} + +// EscapeStr takes the passed in string and +// escapes it IN PLACE. +void mozTXTToHTMLConv::EscapeStr(nsString& aInString, bool inAttribute) { + // the replace substring routines + // don't seem to work if you have a character + // in the in string that is also in the replacement + // string! =( + // aInString.ReplaceSubstring("&", "&"); + // aInString.ReplaceSubstring("<", "<"); + // aInString.ReplaceSubstring(">", ">"); + for (uint32_t i = 0; i < aInString.Length();) { + switch (aInString[i]) { + case '<': + aInString.Cut(i, 1); + aInString.InsertLiteral(u"<", i); + i += 4; // skip past the integers we just added + break; + case '>': + aInString.Cut(i, 1); + aInString.InsertLiteral(u">", i); + i += 4; // skip past the integers we just added + break; + case '&': + aInString.Cut(i, 1); + aInString.InsertLiteral(u"&", i); + i += 5; // skip past the integers we just added + break; + case '"': + if (inAttribute) { + aInString.Cut(i, 1); + aInString.InsertLiteral(u""", i); + i += 6; + break; + } + // else fall through + [[fallthrough]]; + default: + i++; + } + } +} + +void mozTXTToHTMLConv::UnescapeStr(const char16_t* aInString, int32_t aStartPos, + int32_t aLength, nsString& aOutString) { + const char16_t* subString = nullptr; + for (uint32_t i = aStartPos; int32_t(i) - aStartPos < aLength;) { + int32_t remainingChars = i - aStartPos; + if (aInString[i] == '&') { + subString = &aInString[i]; + if (!NS_strncmp(subString, u"<", + std::min(4, aLength - remainingChars))) { + aOutString.Append(char16_t('<')); + i += 4; + } else if (!NS_strncmp(subString, u">", + std::min(4, aLength - remainingChars))) { + aOutString.Append(char16_t('>')); + i += 4; + } else if (!NS_strncmp(subString, u"&", + std::min(5, aLength - remainingChars))) { + aOutString.Append(char16_t('&')); + i += 5; + } else if (!NS_strncmp(subString, u""", + std::min(6, aLength - remainingChars))) { + aOutString.Append(char16_t('"')); + i += 6; + } else { + aOutString += aInString[i]; + i++; + } + } else { + aOutString += aInString[i]; + i++; + } + } +} + +void mozTXTToHTMLConv::CompleteAbbreviatedURL(const char16_t* aInString, + int32_t aInLength, + const uint32_t pos, + nsString& aOutString) { + NS_ASSERTION(int32_t(pos) < aInLength, + "bad args to CompleteAbbreviatedURL, see bug #190851"); + if (int32_t(pos) >= aInLength) return; + + if (aInString[pos] == '@') { + // only pre-pend a mailto url if the string contains a .domain in it.. + // i.e. we want to linkify johndoe@foo.com but not "let's meet @8pm" + nsDependentString inString(aInString, aInLength); + if (inString.FindChar('.', pos) != + kNotFound) // if we have a '.' after the @ sign.... + { + aOutString.AssignLiteral("mailto:"); + aOutString += aInString; + } + } else if (aInString[pos] == '.') { + if (ItMatchesDelimited(aInString, aInLength, u"www.", 4, LT_IGNORE, + LT_IGNORE)) { + aOutString.AssignLiteral("http://"); + aOutString += aInString; + } + } +} + +bool mozTXTToHTMLConv::FindURLStart(const char16_t* aInString, + int32_t aInLength, const uint32_t pos, + const modetype check, uint32_t& start) { + switch (check) { // no breaks, because end of blocks is never reached + case RFC1738: { + if (!NS_strncmp(&aInString[std::max(int32_t(pos - 4), 0)], u"<URL:", 5)) { + start = pos + 1; + return true; + } + return false; + } + case RFC2396E: { + nsDependentSubstring temp(aInString, aInLength); + int32_t i = pos <= 0 ? kNotFound : temp.RFindCharInSet(u"<>\"", pos - 1); + if (i != kNotFound && + (temp[uint32_t(i)] == '<' || temp[uint32_t(i)] == '"')) { + start = uint32_t(++i); + return start < pos; + } + return false; + } + case freetext: { + int32_t i = pos - 1; + for (; i >= 0 && + (IsAsciiAlpha(aInString[uint32_t(i)]) || + IsAsciiDigit(aInString[uint32_t(i)]) || + aInString[uint32_t(i)] == '+' || aInString[uint32_t(i)] == '-' || + aInString[uint32_t(i)] == '.'); + i--) { + ; + } + if (++i >= 0 && uint32_t(i) < pos && + IsAsciiAlpha(aInString[uint32_t(i)])) { + start = uint32_t(i); + return true; + } + return false; + } + case abbreviated: { + int32_t i = pos - 1; + // This disallows non-ascii-characters for email. + // Currently correct, but revisit later after standards changed. + bool isEmail = aInString[pos] == (char16_t)'@'; + // These chars mark the start of the URL + for (; i >= 0 && aInString[uint32_t(i)] != '>' && + aInString[uint32_t(i)] != '<' && aInString[uint32_t(i)] != '"' && + aInString[uint32_t(i)] != '\'' && aInString[uint32_t(i)] != '`' && + aInString[uint32_t(i)] != ',' && aInString[uint32_t(i)] != '{' && + aInString[uint32_t(i)] != '[' && aInString[uint32_t(i)] != '(' && + aInString[uint32_t(i)] != '|' && aInString[uint32_t(i)] != '\\' && + !IsSpace(aInString[uint32_t(i)]) && + (!isEmail || IsAscii(aInString[uint32_t(i)])) && + (!isEmail || aInString[uint32_t(i)] != ')'); + i--) { + ; + } + if (++i >= 0 && uint32_t(i) < pos && + (IsAsciiAlpha(aInString[uint32_t(i)]) || + IsAsciiDigit(aInString[uint32_t(i)]))) { + start = uint32_t(i); + return true; + } + return false; + } + default: + return false; + } // switch +} + +bool mozTXTToHTMLConv::FindURLEnd(const char16_t* aInString, + int32_t aInStringLength, const uint32_t pos, + const modetype check, const uint32_t start, + uint32_t& end) { + switch (check) { // no breaks, because end of blocks is never reached + case RFC1738: + case RFC2396E: { + nsDependentSubstring temp(aInString, aInStringLength); + + int32_t i = temp.FindCharInSet(u"<>\"", pos + 1); + if (i != kNotFound && + temp[uint32_t(i--)] == + (check == RFC1738 || temp[start - 1] == '<' ? '>' : '"')) { + end = uint32_t(i); + return end > pos; + } + return false; + } + case freetext: + case abbreviated: { + uint32_t i = pos + 1; + bool isEmail = aInString[pos] == (char16_t)'@'; + bool seenOpeningParenthesis = false; // there is a '(' earlier in the URL + bool seenOpeningSquareBracket = + false; // there is a '[' earlier in the URL + for (; int32_t(i) < aInStringLength; i++) { + // These chars mark the end of the URL + if (aInString[i] == '>' || aInString[i] == '<' || aInString[i] == '"' || + aInString[i] == '`' || aInString[i] == '}' || aInString[i] == '{' || + (aInString[i] == ')' && !seenOpeningParenthesis) || + (aInString[i] == ']' && !seenOpeningSquareBracket) || + // Allow IPv6 adresses like http://[1080::8:800:200C:417A]/foo. + (aInString[i] == '[' && i > 2 && + (aInString[i - 1] != '/' || aInString[i - 2] != '/')) || + IsSpace(aInString[i])) { + break; + } + // Disallow non-ascii-characters for email. + // Currently correct, but revisit later after standards changed. + if (isEmail && (aInString[i] == '(' || aInString[i] == '\'' || + !IsAscii(aInString[i]))) { + break; + } + if (aInString[i] == '(') seenOpeningParenthesis = true; + if (aInString[i] == '[') seenOpeningSquareBracket = true; + } + // These chars are allowed in the middle of the URL, but not at end. + // Technically they are, but are used in normal text after the URL. + while (--i > pos && (aInString[i] == '.' || aInString[i] == ',' || + aInString[i] == ';' || aInString[i] == '!' || + aInString[i] == '?' || aInString[i] == '-' || + aInString[i] == ':' || aInString[i] == '\'')) { + ; + } + if (i > pos) { + end = i; + return true; + } + return false; + } + default: + return false; + } // switch +} + +void mozTXTToHTMLConv::CalculateURLBoundaries( + const char16_t* aInString, int32_t aInStringLength, const uint32_t pos, + const uint32_t whathasbeendone, const modetype check, const uint32_t start, + const uint32_t end, nsString& txtURL, nsString& desc, + int32_t& replaceBefore, int32_t& replaceAfter) { + uint32_t descstart = start; + switch (check) { + case RFC1738: { + descstart = start - 5; + desc.Append(&aInString[descstart], + end - descstart + 2); // include "<URL:" and ">" + replaceAfter = end - pos + 1; + } break; + case RFC2396E: { + descstart = start - 1; + desc.Append(&aInString[descstart], + end - descstart + 2); // include brackets + replaceAfter = end - pos + 1; + } break; + case freetext: + case abbreviated: { + descstart = start; + desc.Append(&aInString[descstart], + end - start + 1); // don't include brackets + replaceAfter = end - pos; + } break; + default: + break; + } // switch + + EscapeStr(desc, false); + + txtURL.Append(&aInString[start], end - start + 1); + txtURL.StripWhitespace(); + + // FIX ME + nsAutoString temp2; + ScanTXT(nsDependentSubstring(&aInString[descstart], pos - descstart), + ~kURLs /*prevents loop*/ & whathasbeendone, temp2); + replaceBefore = temp2.Length(); +} + +bool mozTXTToHTMLConv::ShouldLinkify(const nsCString& aURL) { + if (!mIOService) return false; + + nsAutoCString scheme; + nsresult rv = mIOService->ExtractScheme(aURL, scheme); + if (NS_FAILED(rv)) return false; + + if (scheme == "http" || scheme == "https" || scheme == "mailto") { + return true; + } + + // Get the handler for this scheme. + nsCOMPtr<nsIProtocolHandler> handler; + rv = mIOService->GetProtocolHandler(scheme.get(), getter_AddRefs(handler)); + if (NS_FAILED(rv)) return false; + + // Is it an external protocol handler? If not, linkify it. + nsCOMPtr<nsIExternalProtocolHandler> externalHandler = + do_QueryInterface(handler); + if (!externalHandler) return true; // handler is built-in, linkify it! + + // If external app exists for the scheme then linkify it. + bool exists; + rv = externalHandler->ExternalAppExistsForScheme(scheme, &exists); + return (NS_SUCCEEDED(rv) && exists); +} + +bool mozTXTToHTMLConv::CheckURLAndCreateHTML(const nsString& txtURL, + const nsString& desc, + const modetype mode, + nsString& outputHTML) { + // Create *uri from txtURL + nsCOMPtr<nsIURI> uri; + nsresult rv; + // Lazily initialize mIOService + if (!mIOService) { + mIOService = do_GetIOService(); + + if (!mIOService) return false; + } + + // See if the url should be linkified. + NS_ConvertUTF16toUTF8 utf8URL(txtURL); + if (!ShouldLinkify(utf8URL)) return false; + + // it would be faster if we could just check to see if there is a protocol + // handler for the url and return instead of actually trying to create a + // url... + rv = mIOService->NewURI(utf8URL, nullptr, nullptr, getter_AddRefs(uri)); + + // Real work + if (NS_SUCCEEDED(rv) && uri) { + outputHTML.AssignLiteral("<a class=\"moz-txt-link-"); + switch (mode) { + case RFC1738: + outputHTML.AppendLiteral("rfc1738"); + break; + case RFC2396E: + outputHTML.AppendLiteral("rfc2396E"); + break; + case freetext: + outputHTML.AppendLiteral("freetext"); + break; + case abbreviated: + outputHTML.AppendLiteral("abbreviated"); + break; + default: + break; + } + nsAutoString escapedURL(txtURL); + EscapeStr(escapedURL, true); + + outputHTML.AppendLiteral("\" href=\""); + outputHTML += escapedURL; + outputHTML.AppendLiteral("\">"); + outputHTML += desc; + outputHTML.AppendLiteral("</a>"); + return true; + } + return false; +} + +NS_IMETHODIMP mozTXTToHTMLConv::FindURLInPlaintext(const char16_t* aInString, + int32_t aInLength, + int32_t aPos, + int32_t* aStartPos, + int32_t* aEndPos) { + // call FindURL on the passed in string + nsAutoString outputHTML; // we'll ignore the generated output HTML + + *aStartPos = -1; + *aEndPos = -1; + + FindURL(aInString, aInLength, aPos, kURLs, outputHTML, *aStartPos, *aEndPos); + + return NS_OK; +} + +bool mozTXTToHTMLConv::FindURL(const char16_t* aInString, int32_t aInLength, + const uint32_t pos, + const uint32_t whathasbeendone, + nsString& outputHTML, int32_t& replaceBefore, + int32_t& replaceAfter) { + enum statetype { unchecked, invalid, startok, endok, success }; + static const modetype ranking[] = {RFC1738, RFC2396E, freetext, abbreviated}; + + statetype state[mozTXTToHTMLConv_lastMode + 1]; // 0(=unknown)..lastMode + /* I don't like this abuse of enums as index for the array, + but I don't know a better method */ + + // Define, which modes to check + /* all modes but abbreviated are checked for text[pos] == ':', + only abbreviated for '.', RFC2396E and abbreviated for '@' */ + for (modetype iState = unknown; iState <= mozTXTToHTMLConv_lastMode; + iState = modetype(iState + 1)) { + state[iState] = aInString[pos] == ':' ? unchecked : invalid; + } + switch (aInString[pos]) { + case '@': + state[RFC2396E] = unchecked; + [[fallthrough]]; + case '.': + state[abbreviated] = unchecked; + break; + case ':': + state[abbreviated] = invalid; + break; + default: + break; + } + + // Test, first successful mode wins, sequence defined by |ranking| + int32_t iCheck = 0; // the currently tested modetype + modetype check = ranking[iCheck]; + for (; iCheck < mozTXTToHTMLConv_numberOfModes && state[check] != success; + iCheck++) + /* check state from last run. + If this is the first, check this one, which isn't = success yet */ + { + check = ranking[iCheck]; + + uint32_t start, end; + + if (state[check] == unchecked) { + if (FindURLStart(aInString, aInLength, pos, check, start)) { + state[check] = startok; + } + } + + if (state[check] == startok) { + if (FindURLEnd(aInString, aInLength, pos, check, start, end)) { + state[check] = endok; + } + } + + if (state[check] == endok) { + nsAutoString txtURL, desc; + int32_t resultReplaceBefore, resultReplaceAfter; + + CalculateURLBoundaries(aInString, aInLength, pos, whathasbeendone, check, + start, end, txtURL, desc, resultReplaceBefore, + resultReplaceAfter); + + if (aInString[pos] != ':') { + nsAutoString temp = txtURL; + txtURL.SetLength(0); + CompleteAbbreviatedURL(temp.get(), temp.Length(), pos - start, txtURL); + } + + if (!txtURL.IsEmpty() && + CheckURLAndCreateHTML(txtURL, desc, check, outputHTML)) { + replaceBefore = resultReplaceBefore; + replaceAfter = resultReplaceAfter; + state[check] = success; + } + } // if + } // for + return state[check] == success; +} + +static inline bool IsAlpha(const uint32_t aChar) { + return mozilla::unicode::GetGenCategory(aChar) == nsUGenCategory::kLetter; +} + +static inline bool IsDigit(const uint32_t aChar) { + return mozilla::unicode::GetGenCategory(aChar) == nsUGenCategory::kNumber; +} + +bool mozTXTToHTMLConv::ItMatchesDelimited(const char16_t* aInString, + int32_t aInLength, + const char16_t* rep, int32_t aRepLen, + LIMTYPE before, LIMTYPE after) { + // this little method gets called a LOT. I found we were spending a + // lot of time just calculating the length of the variable "rep" + // over and over again every time we called it. So we're now passing + // an integer in here. + int32_t textLen = aInLength; + + if (((before == LT_IGNORE && (after == LT_IGNORE || after == LT_DELIMITER)) && + textLen < aRepLen) || + ((before != LT_IGNORE || (after != LT_IGNORE && after != LT_DELIMITER)) && + textLen < aRepLen + 1) || + (before != LT_IGNORE && after != LT_IGNORE && after != LT_DELIMITER && + textLen < aRepLen + 2)) { + return false; + } + + uint32_t text0 = aInString[0]; + if (aInLength > 1 && NS_IS_SURROGATE_PAIR(text0, aInString[1])) { + text0 = SURROGATE_TO_UCS4(text0, aInString[1]); + } + // find length of the char/cluster to be ignored + int32_t ignoreLen = before == LT_IGNORE ? 0 : 1; + if (ignoreLen) { + GraphemeClusterBreakIteratorUtf16 ci( + Span<const char16_t>(aInString, aInLength)); + ignoreLen = *ci.Next(); + } + + int32_t afterIndex = aRepLen + ignoreLen; + uint32_t textAfterPos = aInString[afterIndex]; + if (aInLength > afterIndex + 1 && + NS_IS_SURROGATE_PAIR(textAfterPos, aInString[afterIndex + 1])) { + textAfterPos = SURROGATE_TO_UCS4(textAfterPos, aInString[afterIndex + 1]); + } + + return !((before == LT_ALPHA && !IsAlpha(text0)) || + (before == LT_DIGIT && !IsDigit(text0)) || + (before == LT_DELIMITER && + (IsAlpha(text0) || IsDigit(text0) || text0 == *rep)) || + (after == LT_ALPHA && !IsAlpha(textAfterPos)) || + (after == LT_DIGIT && !IsDigit(textAfterPos)) || + (after == LT_DELIMITER && + (IsAlpha(textAfterPos) || IsDigit(textAfterPos) || + textAfterPos == *rep)) || + !Substring(Substring(aInString, aInString + aInLength), ignoreLen, + aRepLen) + .Equals(Substring(rep, rep + aRepLen), + nsCaseInsensitiveStringComparator)); +} + +uint32_t mozTXTToHTMLConv::NumberOfMatches(const char16_t* aInString, + int32_t aInStringLength, + const char16_t* rep, int32_t aRepLen, + LIMTYPE before, LIMTYPE after) { + uint32_t result = 0; + + // Limit lookahead length to avoid pathological O(n^2) behavior; looking so + // far ahead is unlikely to be important for cases where styling marked-up + // fragments is actually useful anyhow. + const uint32_t len = + std::min(2000u, mozilla::AssertedCast<uint32_t>(aInStringLength)); + GraphemeClusterBreakIteratorUtf16 ci(Span<const char16_t>(aInString, len)); + for (uint32_t pos = 0; pos < len; pos = *ci.Next()) { + if (ItMatchesDelimited(aInString + pos, aInStringLength - pos, rep, aRepLen, + before, after)) { + result++; + } + } + return result; +} + +// NOTE: the converted html for the phrase is appended to aOutString +// tagHTML and attributeHTML are plain ASCII (literal strings, in fact) +bool mozTXTToHTMLConv::StructPhraseHit( + const char16_t* aInString, int32_t aInStringLength, bool col0, + const char16_t* tagTXT, int32_t aTagTXTLen, const char* tagHTML, + const char* attributeHTML, nsAString& aOutString, uint32_t& openTags) { + /* We're searching for the following pattern: + LT_DELIMITER - "*" - ALPHA - + [ some text (maybe more "*"-pairs) - ALPHA ] "*" - LT_DELIMITER. + <strong> is only inserted, if existence of a pair could be verified + We use the first opening/closing tag, if we can choose */ + + const char16_t* newOffset = aInString; + int32_t newLength = aInStringLength; + if (!col0) // skip the first element? + { + newOffset = &aInString[1]; + newLength = aInStringLength - 1; + } + + // opening tag + if (ItMatchesDelimited(aInString, aInStringLength, tagTXT, aTagTXTLen, + (col0 ? LT_IGNORE : LT_DELIMITER), + LT_ALPHA) // is opening tag + && NumberOfMatches(newOffset, newLength, tagTXT, aTagTXTLen, LT_ALPHA, + LT_DELIMITER) // remaining closing tags + > openTags) { + openTags++; + aOutString.Append('<'); + aOutString.AppendASCII(tagHTML); + aOutString.Append(char16_t(' ')); + aOutString.AppendASCII(attributeHTML); + aOutString.AppendLiteral("><span class=\"moz-txt-tag\">"); + aOutString.Append(tagTXT); + aOutString.AppendLiteral("</span>"); + return true; + } + + // closing tag + if (openTags > 0 && ItMatchesDelimited(aInString, aInStringLength, tagTXT, + aTagTXTLen, LT_ALPHA, LT_DELIMITER)) { + openTags--; + aOutString.AppendLiteral("<span class=\"moz-txt-tag\">"); + aOutString.Append(tagTXT); + aOutString.AppendLiteral("</span></"); + aOutString.AppendASCII(tagHTML); + aOutString.Append(char16_t('>')); + return true; + } + + return false; +} + +bool mozTXTToHTMLConv::SmilyHit(const char16_t* aInString, int32_t aLength, + bool col0, const char* tagTXT, + const nsString& imageName, nsString& outputHTML, + int32_t& glyphTextLen) { + if (!aInString || !tagTXT || imageName.IsEmpty()) return false; + + int32_t tagLen = strlen(tagTXT); + + uint32_t delim = (col0 ? 0 : 1) + tagLen; + + if ((col0 || IsSpace(aInString[0])) && + (aLength <= int32_t(delim) || IsSpace(aInString[delim]) || + (aLength > int32_t(delim + 1) && + (aInString[delim] == '.' || aInString[delim] == ',' || + aInString[delim] == ';' || aInString[delim] == '8' || + aInString[delim] == '>' || aInString[delim] == '!' || + aInString[delim] == '?') && + IsSpace(aInString[delim + 1]))) && + ItMatchesDelimited(aInString, aLength, + NS_ConvertASCIItoUTF16(tagTXT).get(), tagLen, + col0 ? LT_IGNORE : LT_DELIMITER, LT_IGNORE) + // Note: tests at different pos for LT_IGNORE and LT_DELIMITER + ) { + if (!col0) { + outputHTML.Truncate(); + outputHTML.Append(char16_t(' ')); + } + + outputHTML.Append(imageName); // emoji unicode + glyphTextLen = (col0 ? 0 : 1) + tagLen; + return true; + } + + return false; +} + +// the glyph is appended to aOutputString instead of the original string... +bool mozTXTToHTMLConv::GlyphHit(const char16_t* aInString, int32_t aInLength, + bool col0, nsAString& aOutputString, + int32_t& glyphTextLen) { + char16_t text0 = aInString[0]; + char16_t text1 = aInString[1]; + char16_t firstChar = (col0 ? text0 : text1); + + // temporary variable used to store the glyph html text + nsAutoString outputHTML; + bool bTestSmilie; + bool bArg = false; + int i; + + // refactor some of this mess to avoid code duplication and speed execution a + // bit there are two cases that need to be tried one after another. To avoid a + // lot of duplicate code, rolling into a loop + + i = 0; + while (i < 2) { + bTestSmilie = false; + if (!i && (firstChar == ':' || firstChar == ';' || firstChar == '=' || + firstChar == '>' || firstChar == '8' || firstChar == 'O')) { + // first test passed + + bTestSmilie = true; + bArg = col0; + } + if (i && col0 && + (text1 == ':' || text1 == ';' || text1 == '=' || text1 == '>' || + text1 == '8' || text1 == 'O')) { + // second test passed + + bTestSmilie = true; + bArg = false; + } + if (bTestSmilie && (SmilyHit(aInString, aInLength, bArg, ":-)", + u"🙂"_ns, // smile, U+1F642 + outputHTML, glyphTextLen) || + + SmilyHit(aInString, aInLength, bArg, ":)", + u"🙂"_ns, // smile, U+1F642 + outputHTML, glyphTextLen) || + + SmilyHit(aInString, aInLength, bArg, ":-D", + u"😂"_ns, // laughing, U+1F602 + outputHTML, glyphTextLen) || + + SmilyHit(aInString, aInLength, bArg, ":-(", + u"🙁"_ns, // frown, U+1F641 + outputHTML, glyphTextLen) || + + SmilyHit(aInString, aInLength, bArg, ":(", + u"🙁"_ns, // frown, U+1F641 + outputHTML, glyphTextLen) || + + SmilyHit(aInString, aInLength, bArg, ":$", + u"😳"_ns, // embarassed, U+1F633 + outputHTML, glyphTextLen) || + + SmilyHit(aInString, aInLength, bArg, ";-)", + u"😉"_ns, // wink, U+1F609 + outputHTML, glyphTextLen) || + + SmilyHit(aInString, aInLength, col0, ";)", + u"😉"_ns, // wink, U+1F609 + outputHTML, glyphTextLen) || + + SmilyHit(aInString, aInLength, bArg, ":-\\", + u"😕"_ns, // undecided, U+1F615 + outputHTML, glyphTextLen) || + + SmilyHit(aInString, aInLength, bArg, ":-P", + u"😛"_ns, // tongue, U+1F61B + outputHTML, glyphTextLen) || + + SmilyHit(aInString, aInLength, bArg, ";-P", + u"😜"_ns, // winking face with tongue, U+1F61C + outputHTML, glyphTextLen) || + + SmilyHit(aInString, aInLength, bArg, "=-O", + u"😮"_ns, // surprise, U+1F62E + outputHTML, glyphTextLen) || + + SmilyHit(aInString, aInLength, bArg, ":-*", + u"😘"_ns, // kiss, U+1F618 + outputHTML, glyphTextLen) || + + SmilyHit(aInString, aInLength, bArg, ">:o", + u"🤬"_ns, // swearing, U+1F92C + outputHTML, glyphTextLen) || + + SmilyHit(aInString, aInLength, bArg, ">:-o", + u"🤬"_ns, // swearing, U+1F92C + outputHTML, glyphTextLen) || + + SmilyHit(aInString, aInLength, bArg, ">:(", + u"😠"_ns, // angry, U+1F620 + outputHTML, glyphTextLen) || + + SmilyHit(aInString, aInLength, bArg, ">:-(", + u"😠"_ns, // angry, U+1F620 + outputHTML, glyphTextLen) || + + SmilyHit(aInString, aInLength, bArg, "8-)", + u"😎"_ns, // cool, U+1F60E + outputHTML, glyphTextLen) || + + SmilyHit(aInString, aInLength, bArg, ":-$", + u"🤑"_ns, // money, U+1F911 + outputHTML, glyphTextLen) || + + SmilyHit(aInString, aInLength, bArg, ":-!", + u"😬"_ns, // foot, U+1F62C + outputHTML, glyphTextLen) || + + SmilyHit(aInString, aInLength, bArg, "O:-)", + u"😇"_ns, // innocent, U+1F607 + outputHTML, glyphTextLen) || + + SmilyHit(aInString, aInLength, bArg, ":'(", + u"😭"_ns, // cry, U+1F62D + outputHTML, glyphTextLen) || + + SmilyHit(aInString, aInLength, bArg, ":-X", + u"🤐"_ns, // sealed, U+1F910 + outputHTML, glyphTextLen))) { + aOutputString.Append(outputHTML); + return true; + } + i++; + } + if (text0 == '\f') { + aOutputString.AppendLiteral("<span class='moz-txt-formfeed'></span>"); + glyphTextLen = 1; + return true; + } + if (text0 == '+' || text1 == '+') { + if (ItMatchesDelimited(aInString, aInLength, u" +/-", 4, LT_IGNORE, + LT_IGNORE)) { + aOutputString.AppendLiteral(" ±"); + glyphTextLen = 4; + return true; + } + if (col0 && ItMatchesDelimited(aInString, aInLength, u"+/-", 3, LT_IGNORE, + LT_IGNORE)) { + aOutputString.AppendLiteral("±"); + glyphTextLen = 3; + return true; + } + } + + // x^2 => x<sup>2</sup>, also handle powers x^-2, x^0.5 + // implement regular expression /[\dA-Za-z\)\]}]\^-?\d+(\.\d+)*[^\dA-Za-z]/ + if (text1 == '^' && + (IsAsciiDigit(text0) || IsAsciiAlpha(text0) || text0 == ')' || + text0 == ']' || text0 == '}') && + ((2 < aInLength && IsAsciiDigit(aInString[2])) || + (3 < aInLength && aInString[2] == '-' && IsAsciiDigit(aInString[3])))) { + // Find first non-digit + int32_t delimPos = 3; // skip "^" and first digit (or '-') + for (; delimPos < aInLength && + (IsAsciiDigit(aInString[delimPos]) || + (aInString[delimPos] == '.' && delimPos + 1 < aInLength && + IsAsciiDigit(aInString[delimPos + 1]))); + delimPos++) { + ; + } + + if (delimPos < aInLength && IsAsciiAlpha(aInString[delimPos])) { + return false; + } + + outputHTML.Truncate(); + outputHTML += text0; + outputHTML.AppendLiteral( + "<sup class=\"moz-txt-sup\">" + "<span style=\"display:inline-block;width:0;height:0;overflow:hidden\">" + "^</span>"); + + aOutputString.Append(outputHTML); + aOutputString.Append(&aInString[2], delimPos - 2); + aOutputString.AppendLiteral("</sup>"); + + glyphTextLen = delimPos /* - 1 + 1 */; + return true; + } + /* + The following strings are not substituted: + |TXT |HTML |Reason + +------+---------+---------- + -> ← Bug #454 + => ⇐ dito + <- → dito + <= ⇒ dito + (tm) ™ dito + 1/4 ¼ is triggered by 1/4 Part 1, 2/4 Part 2, ... + 3/4 ¾ dito + 1/2 ½ similar + */ + return false; +} + +/*************************************************************************** + Library-internal Interface +****************************************************************************/ + +NS_IMPL_ISUPPORTS(mozTXTToHTMLConv, mozITXTToHTMLConv, nsIStreamConverter, + nsIThreadRetargetableStreamListener, nsIStreamListener, + nsIRequestObserver) + +int32_t mozTXTToHTMLConv::CiteLevelTXT(const char16_t* line, + uint32_t& logLineStart) { + int32_t result = 0; + int32_t lineLength = NS_strlen(line); + + bool moreCites = true; + while (moreCites) { + /* E.g. the following lines count as quote: + + > text + //#ifdef QUOTE_RECOGNITION_AGGRESSIVE + >text + //#ifdef QUOTE_RECOGNITION_AGGRESSIVE + > text + ] text + USER> text + USER] text + //#endif + + logLineStart is the position of "t" in this example + */ + uint32_t i = logLineStart; + +#ifdef QUOTE_RECOGNITION_AGGRESSIVE + for (; int32_t(i) < lineLength && IsSpace(line[i]); i++) + ; + for (; int32_t(i) < lineLength && IsAsciiAlpha(line[i]) && + nsCRT::IsUpper(line[i]); + i++) + ; + if (int32_t(i) < lineLength && (line[i] == '>' || line[i] == ']')) +#else + if (int32_t(i) < lineLength && line[i] == '>') +#endif + { + i++; + if (int32_t(i) < lineLength && line[i] == ' ') i++; + // sendmail/mbox + // Placed here for performance increase + const char16_t* indexString = &line[logLineStart]; + // here, |logLineStart < lineLength| is always true + uint32_t minlength = std::min(uint32_t(6), NS_strlen(indexString)); + if (Substring(indexString, indexString + minlength) + .Equals(Substring(u">From "_ns, 0, minlength), + nsCaseInsensitiveStringComparator)) { + // XXX RFC2646 + moreCites = false; + } else { + result++; + logLineStart = i; + } + } else { + moreCites = false; + } + } + + return result; +} + +NS_IMETHODIMP +mozTXTToHTMLConv::ScanTXT(const nsAString& aInString, uint32_t whattodo, + nsAString& aOutString) { + if (aInString.Length() == 0) { + aOutString.Truncate(); + return NS_OK; + } + + if (!aOutString.SetCapacity(uint32_t(aInString.Length() * growthRate), + mozilla::fallible)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + bool doURLs = 0 != (whattodo & kURLs); + bool doGlyphSubstitution = 0 != (whattodo & kGlyphSubstitution); + bool doStructPhrase = 0 != (whattodo & kStructPhrase); + + uint32_t structPhrase_strong = 0; // Number of currently open tags + uint32_t structPhrase_underline = 0; + uint32_t structPhrase_italic = 0; + uint32_t structPhrase_code = 0; + + uint32_t endOfLastURLOutput = 0; + + nsAutoString outputHTML; // moved here for performance increase + + const char16_t* rawInputString = aInString.BeginReading(); + uint32_t inLength = aInString.Length(); + + const Span<const char16_t> inString(aInString); + GraphemeClusterBreakIteratorUtf16 ci(inString); + uint32_t i = 0; + while (i < inLength) { + if (doGlyphSubstitution) { + int32_t glyphTextLen; + if (GlyphHit(&rawInputString[i], inLength - i, i == 0, aOutString, + glyphTextLen)) { + i = *ci.Seek(i + glyphTextLen - 1); + continue; + } + } + + if (doStructPhrase) { + const char16_t* newOffset = rawInputString; + int32_t newLength = aInString.Length(); + if (i > 0) // skip the first element? + { + GraphemeClusterBreakReverseIteratorUtf16 ri( + Span<const char16_t>(rawInputString, i)); + Maybe<uint32_t> nextPos = ri.Next(); + newOffset += *nextPos; + newLength -= *nextPos; + } + + switch (aInString[i]) // Performance increase + { + case '*': + if (StructPhraseHit(newOffset, newLength, i == 0, u"*", 1, "b", + "class=\"moz-txt-star\"", aOutString, + structPhrase_strong)) { + i = *ci.Next(); + continue; + } + break; + case '/': + if (StructPhraseHit(newOffset, newLength, i == 0, u"/", 1, "i", + "class=\"moz-txt-slash\"", aOutString, + structPhrase_italic)) { + i = *ci.Next(); + continue; + } + break; + case '_': + if (StructPhraseHit(newOffset, newLength, i == 0, u"_", 1, + "span" /* <u> is deprecated */, + "class=\"moz-txt-underscore\"", aOutString, + structPhrase_underline)) { + i = *ci.Next(); + continue; + } + break; + case '|': + if (StructPhraseHit(newOffset, newLength, i == 0, u"|", 1, "code", + "class=\"moz-txt-verticalline\"", aOutString, + structPhrase_code)) { + i = *ci.Next(); + continue; + } + break; + } + } + + if (doURLs) { + switch (aInString[i]) { + case ':': + case '@': + case '.': + if ((i == 0 || ((i > 0) && aInString[i - 1] != ' ')) && + ((i == aInString.Length() - 1) || + (aInString[i + 1] != ' '))) // Performance increase + { + int32_t replaceBefore; + int32_t replaceAfter; + if (FindURL(rawInputString, aInString.Length(), i, whattodo, + outputHTML, replaceBefore, replaceAfter) && + structPhrase_strong + structPhrase_italic + + structPhrase_underline + structPhrase_code == + 0 + /* workaround for bug #19445 */) { + // Don't cut into previously inserted HTML (bug 1509493) + if (aOutString.Length() - replaceBefore < endOfLastURLOutput) { + break; + } + aOutString.Cut(aOutString.Length() - replaceBefore, + replaceBefore); + aOutString += outputHTML; + endOfLastURLOutput = aOutString.Length(); + i = *ci.Seek(i + replaceAfter); + continue; + } + } + break; + } // switch + } + + switch (aInString[i]) { + // Special symbols + case '<': + case '>': + case '&': + EscapeChar(aInString[i], aOutString, false); + i = *ci.Next(); + break; + // Normal characters + default: { + const uint32_t oldIdx = i; + i = *ci.Next(); + aOutString.Append(inString.FromTo(oldIdx, i)); + break; + } + } + } + return NS_OK; +} + +NS_IMETHODIMP +mozTXTToHTMLConv::ScanHTML(const nsAString& input, uint32_t whattodo, + nsAString& aOutString) { + const nsPromiseFlatString& aInString = PromiseFlatString(input); + if (!aOutString.SetCapacity(uint32_t(aInString.Length() * growthRate), + mozilla::fallible)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + // some common variables we were recalculating + // every time inside the for loop... + int32_t lengthOfInString = aInString.Length(); + const char16_t* uniBuffer = aInString.get(); + +#ifdef DEBUG_BenB_Perf + PRTime parsing_start = PR_IntervalNow(); +#endif + + // Look for simple entities not included in a tags and scan them. + // Skip all tags ("<[...]>") and content in an a link tag ("<a [...]</a>"), + // comment tag ("<!--[...]-->"), style tag, script tag or head tag. + // Unescape the rest (text between tags) and pass it to ScanTXT. + nsAutoCString canFollow(" \f\n\r\t>"); + for (int32_t i = 0; i < lengthOfInString;) { + if (aInString[i] == '<') // html tag + { + int32_t start = i; + if (i + 2 < lengthOfInString && nsCRT::ToLower(aInString[i + 1]) == 'a' && + canFollow.FindChar(aInString[i + 2]) != kNotFound) + // if a tag, skip until </a>. + // Make sure there's a white-space character after, not to match "abbr". + { + i = aInString.LowerCaseFindASCII("</a>", i); + if (i == kNotFound) { + i = lengthOfInString; + } else { + i += 4; + } + } else if (Substring(aInString, i + 1, 3).LowerCaseEqualsASCII("!--")) + // if out-commended code, skip until --> + { + i = aInString.Find(u"-->", i); + if (i == kNotFound) { + i = lengthOfInString; + } else { + i += 3; + } + } else if (i + 6 < lengthOfInString && + Substring(aInString, i + 1, 5).LowerCaseEqualsASCII("style") && + canFollow.FindChar(aInString[i + 6]) != kNotFound) + // if style tag, skip until </style> + { + i = aInString.LowerCaseFindASCII("</style>", i); + if (i == kNotFound) { + i = lengthOfInString; + } else { + i += 8; + } + } else if (i + 7 < lengthOfInString && + Substring(aInString, i + 1, 6) + .LowerCaseEqualsASCII("script") && + canFollow.FindChar(aInString[i + 7]) != kNotFound) + // if script tag, skip until </script> + { + i = aInString.LowerCaseFindASCII("</script>", i); + if (i == kNotFound) { + i = lengthOfInString; + } else { + i += 9; + } + } else if (i + 5 < lengthOfInString && + Substring(aInString, i + 1, 4).LowerCaseEqualsASCII("head") && + canFollow.FindChar(aInString[i + 5]) != kNotFound) + // if head tag, skip until </head> + // Make sure not to match <header>. + { + i = aInString.LowerCaseFindASCII("</head>", i); + if (i == kNotFound) { + i = lengthOfInString; + } else { + i += 7; + } + } else // just skip tag (attributes etc.) + { + i = aInString.FindChar('>', i); + if (i == kNotFound) { + i = lengthOfInString; + } else { + i++; + } + } + aOutString.Append(&uniBuffer[start], i - start); + } else { + uint32_t start = uint32_t(i); + i = aInString.FindChar('<', i); + if (i == kNotFound) i = lengthOfInString; + + nsAutoStringN<256> tempString; + tempString.SetCapacity(uint32_t((uint32_t(i) - start) * growthRate)); + UnescapeStr(uniBuffer, start, uint32_t(i) - start, tempString); + ScanTXT(tempString, whattodo, aOutString); + } + } + +#ifdef DEBUG_BenB_Perf + printf("ScanHTML time: %d ms\n", + PR_IntervalToMilliseconds(PR_IntervalNow() - parsing_start)); +#endif + return NS_OK; +} + +/**************************************************************************** + XPCOM Interface +*****************************************************************************/ + +NS_IMETHODIMP +mozTXTToHTMLConv::Convert(nsIInputStream* aFromStream, const char* aFromType, + const char* aToType, nsISupports* aCtxt, + nsIInputStream** _retval) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +mozTXTToHTMLConv::AsyncConvertData(const char* aFromType, const char* aToType, + nsIStreamListener* aListener, + nsISupports* aCtxt) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +mozTXTToHTMLConv::GetConvertedType(const nsACString& aFromType, + nsIChannel* aChannel, nsACString& aToType) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +mozTXTToHTMLConv::OnDataAvailable(nsIRequest* request, nsIInputStream* inStr, + uint64_t sourceOffset, uint32_t count) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +mozTXTToHTMLConv::OnDataFinished(nsresult aStatus) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +mozTXTToHTMLConv::CheckListenerChain() { return NS_ERROR_NOT_IMPLEMENTED; } + +NS_IMETHODIMP +mozTXTToHTMLConv::OnStartRequest(nsIRequest* request) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +mozTXTToHTMLConv::OnStopRequest(nsIRequest* request, nsresult aStatus) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +mozTXTToHTMLConv::CiteLevelTXT(const char16_t* line, uint32_t* logLineStart, + uint32_t* _retval) { + if (!logLineStart || !_retval || !line) return NS_ERROR_NULL_POINTER; + *_retval = CiteLevelTXT(line, *logLineStart); + return NS_OK; +} + +nsresult MOZ_NewTXTToHTMLConv(mozTXTToHTMLConv** aConv) { + MOZ_ASSERT(aConv != nullptr, "null ptr"); + if (!aConv) return NS_ERROR_NULL_POINTER; + + RefPtr<mozTXTToHTMLConv> conv = new mozTXTToHTMLConv(); + conv.forget(aConv); + // return (*aConv)->Init(); + return NS_OK; +} diff --git a/netwerk/streamconv/converters/mozTXTToHTMLConv.h b/netwerk/streamconv/converters/mozTXTToHTMLConv.h new file mode 100644 index 0000000000..dbae8edb0f --- /dev/null +++ b/netwerk/streamconv/converters/mozTXTToHTMLConv.h @@ -0,0 +1,286 @@ +/* -*- 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/. */ + +/** + Description: Currently only functions to enhance plain text with HTML tags. + See mozITXTToHTMLConv. Stream conversion is defunct. +*/ + +#ifndef _mozTXTToHTMLConv_h__ +#define _mozTXTToHTMLConv_h__ + +#include "mozITXTToHTMLConv.h" +#include "nsIThreadRetargetableStreamListener.h" +#include "nsString.h" +#include "nsCOMPtr.h" + +class nsIIOService; + +class mozTXTToHTMLConv : public mozITXTToHTMLConv { + virtual ~mozTXTToHTMLConv() = default; + + ////////////////////////////////////////////////////////// + public: + ////////////////////////////////////////////////////////// + + mozTXTToHTMLConv() = default; + NS_DECL_ISUPPORTS + + NS_DECL_MOZITXTTOHTMLCONV + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSITHREADRETARGETABLESTREAMLISTENER + NS_DECL_NSISTREAMCONVERTER + + /** + see mozITXTToHTMLConv::CiteLevelTXT + */ + int32_t CiteLevelTXT(const char16_t* line, uint32_t& logLineStart); + + ////////////////////////////////////////////////////////// + protected: + ////////////////////////////////////////////////////////// + nsCOMPtr<nsIIOService> + mIOService; // for performance reasons, cache the netwerk service... + /** + Completes<ul> + <li>Case 1: mailto: "mozilla@bucksch.org" -> "mailto:mozilla@bucksch.org" + <li>Case 2: http: "www.mozilla.org" -> "http://www.mozilla.org" + <li>Case 3: ftp: "ftp.mozilla.org" -> "ftp://www.mozilla.org" + </ul> + It does no check, if the resulting URL is valid. + @param text (in): abbreviated URL + @param pos (in): position of "@" (case 1) or first "." (case 2 and 3) + @return Completed URL at success and empty string at failure + */ + void CompleteAbbreviatedURL(const char16_t* aInString, int32_t aInLength, + const uint32_t pos, nsString& aOutString); + + ////////////////////////////////////////////////////////// + private: + ////////////////////////////////////////////////////////// + + enum LIMTYPE { + LT_IGNORE, // limitation not checked + LT_DELIMITER, // not alphanumeric and not rep[0]. End of text is also ok. + LT_ALPHA, // alpha char + LT_DIGIT + }; + + /** + @param text (in): the string to search through.<p> + If before = IGNORE,<br> + rep is compared starting at 1. char of text (text[0]),<br> + else starting at 2. char of text (text[1]). + Chars after "after"-delimiter are ignored. + @param rep (in): the string to look for + @param aRepLen (in): the number of bytes in the string to look for + @param before (in): limitation before rep + @param after (in): limitation after rep + @return true, if rep is found and limitation spec is met or rep is empty + */ + bool ItMatchesDelimited(const char16_t* aInString, int32_t aInLength, + const char16_t* rep, int32_t aRepLen, LIMTYPE before, + LIMTYPE after); + + /** + @param see ItMatchesDelimited + @return Number of ItMatchesDelimited in text + */ + uint32_t NumberOfMatches(const char16_t* aInString, int32_t aInStringLength, + const char16_t* rep, int32_t aRepLen, LIMTYPE before, + LIMTYPE after); + + /** + Currently only changes "<", ">" and "&". All others stay as they are.<p> + "Char" in function name to avoid side effects with nsString(ch) + constructors. + @param ch (in) + @param aStringToAppendto (out) - the string to append the escaped + string to. + @param inAttribute (in) - will escape quotes, too (which is + only needed for attribute values) + */ + void EscapeChar(const char16_t ch, nsAString& aStringToAppendto, + bool inAttribute); + + /** + See EscapeChar. Escapes the string in place. + */ + void EscapeStr(nsString& aInString, bool inAttribute); + + /** + Currently only reverts "<", ">" and "&". All others stay as they are.<p> + @param aInString (in) HTML string + @param aStartPos (in) start index into the buffer + @param aLength (in) length of the buffer + @param aOutString (out) unescaped buffer + */ + void UnescapeStr(const char16_t* aInString, int32_t aStartPos, + int32_t aLength, nsString& aOutString); + + /** + <em>Note</em>: I use different strategies to pass context between the + functions (full text and pos vs. cutted text and col0, glphyTextLen vs. + replaceBefore/-After). It makes some sense, but is hard to understand + (maintain) :-(. + */ + + /** + <p><em>Note:</em> replaceBefore + replaceAfter + 1 (for char at pos) chars + in text should be replaced by outputHTML.</p> + <p><em>Note:</em> This function should be able to process a URL on multiple + lines, but currently, ScanForURLs is called for every line, so it can't.</p> + @param text (in): includes possibly a URL + @param pos (in): position in text, where either ":", "." or "@" are found + @param whathasbeendone (in): What the calling ScanTXT did/has to do with the + (not-linkified) text, i.e. usually the "whattodo" parameter. + (Needed to calculate replaceBefore.) NOT what will be done with + the content of the link. + @param outputHTML (out): URL with HTML-a tag + @param replaceBefore (out): Number of chars of URL before pos + @param replaceAfter (out): Number of chars of URL after pos + @return URL found + */ + bool FindURL(const char16_t* aInString, int32_t aInLength, const uint32_t pos, + const uint32_t whathasbeendone, nsString& outputHTML, + int32_t& replaceBefore, int32_t& replaceAfter); + + enum modetype { + unknown, + RFC1738, /* Check, if RFC1738, APPENDIX compliant, + like "<URL:http://www.mozilla.org>". */ + RFC2396E, /* RFC2396, APPENDIX E allows anglebrackets (like + "<http://www.mozilla.org>") (without "URL:") or + quotation marks(like ""http://www.mozilla.org""). + Also allow email addresses without scheme, + e.g. "<mozilla@bucksch.org>" */ + freetext, /* assume heading scheme + with "[a-zA-Z][a-zA-Z0-9+\-\.]*:" like "news:" + (see RFC2396, Section 3.1). + Certain characters (see code) or any whitespace + (including linebreaks) end the URL. + Other certain (punctation) characters (see code) + at the end are stripped off. */ + abbreviated /* Similar to freetext, but without scheme, e.g. + "www.mozilla.org", "ftp.mozilla.org" and + "mozilla@bucksch.org". */ + /* RFC1738 and RFC2396E type URLs may use multiple lines, + whitespace is stripped. Special characters like ")" stay intact.*/ + }; + + /** + * @param text (in), pos (in): see FindURL + * @param check (in): Start must be conform with this mode + * @param start (out): Position in text, where URL (including brackets or + * similar) starts + * @return |check|-conform start has been found + */ + bool FindURLStart(const char16_t* aInString, int32_t aInLength, + const uint32_t pos, const modetype check, uint32_t& start); + + /** + * @param text (in), pos (in): see FindURL + * @param check (in): End must be conform with this mode + * @param start (in): see FindURLStart + * @param end (out): Similar to |start| param of FindURLStart + * @return |check|-conform end has been found + */ + bool FindURLEnd(const char16_t* aInString, int32_t aInStringLength, + const uint32_t pos, const modetype check, + const uint32_t start, uint32_t& end); + + /** + * @param text (in), pos (in), whathasbeendone (in): see FindURL + * @param check (in): Current mode + * @param start (in), end (in): see FindURLEnd + * @param txtURL (out): Guessed (raw) URL. + * Without whitespace, but not completed. + * @param desc (out): Link as shown to the user, but already escaped. + * Should be placed between the <a> and </a> tags. + * @param replaceBefore(out), replaceAfter (out): see FindURL + */ + void CalculateURLBoundaries(const char16_t* aInString, + int32_t aInStringLength, const uint32_t pos, + const uint32_t whathasbeendone, + const modetype check, const uint32_t start, + const uint32_t end, nsString& txtURL, + nsString& desc, int32_t& replaceBefore, + int32_t& replaceAfter); + + /** + * @param txtURL (in), desc (in): see CalculateURLBoundaries + * @param outputHTML (out): see FindURL + * @return A valid URL could be found (and creation of HTML successful) + */ + bool CheckURLAndCreateHTML(const nsString& txtURL, const nsString& desc, + const modetype mode, nsString& outputHTML); + + /** + @param text (in): line of text possibly with tagTXT.<p> + if col0 is true, + starting with tagTXT<br> + else + starting one char before tagTXT + @param col0 (in): tagTXT is on the beginning of the line (or paragraph). + open must be 0 then. + @param tagTXT (in): Tag in plaintext to search for, e.g. "*" + @param aTagTxtLen (in): length of tagTXT. + @param tagHTML (in): HTML-Tag to replace tagTXT with, + without "<" and ">", e.g. "strong" + @param attributeHTML (in): HTML-attribute to add to opening tagHTML, + e.g. "class=txt_star" + @param aOutString: string to APPEND the converted html into + @param open (in/out): Number of currently open tags of type tagHTML + @return Conversion succeeded + */ + bool StructPhraseHit(const char16_t* aInString, int32_t aInStringLength, + bool col0, const char16_t* tagTXT, int32_t aTagTxtLen, + const char* tagHTML, const char* attributeHTML, + nsAString& aOutString, uint32_t& openTags); + + /** + @param text (in), col0 (in): see GlyphHit + @param tagTXT (in): Smily, see also StructPhraseHit + @param imageName (in): the basename of the file that contains the image for + this smilie + @param outputHTML (out): new string containing the html for the smily + @param glyphTextLen (out): see GlyphHit + */ + bool SmilyHit(const char16_t* aInString, int32_t aLength, bool col0, + const char* tagTXT, const nsString& imageName, + nsString& outputHTML, int32_t& glyphTextLen); + + /** + Checks, if we can replace some chars at the start of line with prettier HTML + code.<p> + If success is reported, replace the first glyphTextLen chars with outputHTML + + @param text (in): line of text possibly with Glyph.<p> + If col0 is true, + starting with Glyph <br><!-- (br not part of text) --> + else + starting one char before Glyph + @param col0 (in): text starts at the beginning of the line (or paragraph) + @param aOutString (out): APPENDS html for the glyph to this string + @param glyphTextLen (out): Length of original text to replace + @return see StructPhraseHit + */ + bool GlyphHit(const char16_t* aInString, int32_t aInLength, bool col0, + nsAString& aOutputString, int32_t& glyphTextLen); + + /** + Check if a given url should be linkified. + @param aURL (in): url to be checked on. + */ + bool ShouldLinkify(const nsCString& aURL); +}; + +// It's said, that Win32 and Mac don't like static const members +const int32_t mozTXTToHTMLConv_lastMode = 4; +// Needed (only) by mozTXTToHTMLConv::FindURL +const int32_t mozTXTToHTMLConv_numberOfModes = 4; // dito; unknown not counted + +#endif diff --git a/netwerk/streamconv/converters/nsDirIndex.cpp b/netwerk/streamconv/converters/nsDirIndex.cpp new file mode 100644 index 0000000000..fefe5e1863 --- /dev/null +++ b/netwerk/streamconv/converters/nsDirIndex.cpp @@ -0,0 +1,62 @@ +/* -*- 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 "nsDirIndex.h" + +NS_IMPL_ISUPPORTS(nsDirIndex, nsIDirIndex) + +NS_IMETHODIMP +nsDirIndex::GetType(uint32_t* aType) { + NS_ENSURE_ARG_POINTER(aType); + + *aType = mType; + return NS_OK; +} + +NS_IMETHODIMP +nsDirIndex::SetType(uint32_t aType) { + mType = aType; + return NS_OK; +} + +NS_IMETHODIMP +nsDirIndex::GetLocation(nsACString& aLocation) { + aLocation = mLocation; + return NS_OK; +} + +NS_IMETHODIMP +nsDirIndex::SetLocation(const nsACString& aLocation) { + mLocation = aLocation; + return NS_OK; +} + +NS_IMETHODIMP +nsDirIndex::GetSize(int64_t* aSize) { + NS_ENSURE_ARG_POINTER(aSize); + + *aSize = mSize; + return NS_OK; +} + +NS_IMETHODIMP +nsDirIndex::SetSize(int64_t aSize) { + mSize = aSize; + return NS_OK; +} + +NS_IMETHODIMP +nsDirIndex::GetLastModified(PRTime* aLastModified) { + NS_ENSURE_ARG_POINTER(aLastModified); + + *aLastModified = mLastModified; + return NS_OK; +} + +NS_IMETHODIMP +nsDirIndex::SetLastModified(PRTime aLastModified) { + mLastModified = aLastModified; + return NS_OK; +} diff --git a/netwerk/streamconv/converters/nsDirIndex.h b/netwerk/streamconv/converters/nsDirIndex.h new file mode 100644 index 0000000000..eb6009295c --- /dev/null +++ b/netwerk/streamconv/converters/nsDirIndex.h @@ -0,0 +1,32 @@ +/* -*- 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/. */ + +#ifndef nsDirIndex_h__ +#define nsDirIndex_h__ + +#include "nsIDirIndex.h" +#include "nsString.h" +#include "mozilla/Attributes.h" + +class nsDirIndex final : public nsIDirIndex { + private: + ~nsDirIndex() = default; + + public: + nsDirIndex() = default; + + NS_DECL_ISUPPORTS + NS_DECL_NSIDIRINDEX + + protected: + uint32_t mType{TYPE_UNKNOWN}; + nsCString mContentType; + nsCString mLocation; + nsString mDescription; + int64_t mSize{INT64_MAX}; + PRTime mLastModified{-1LL}; +}; + +#endif diff --git a/netwerk/streamconv/converters/nsDirIndexParser.cpp b/netwerk/streamconv/converters/nsDirIndexParser.cpp new file mode 100644 index 0000000000..600f43a1aa --- /dev/null +++ b/netwerk/streamconv/converters/nsDirIndexParser.cpp @@ -0,0 +1,266 @@ +/* -*- 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/. */ + +/* This parsing code originally lived in xpfe/components/directory/ - bbaetz */ + +#include "nsDirIndexParser.h" + +#include "mozilla/ArrayUtils.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/Encoding.h" +#include "mozilla/StaticPtr.h" +#include "prprf.h" +#include "nsCRT.h" +#include "nsDirIndex.h" +#include "nsEscape.h" +#include "nsIDirIndex.h" +#include "nsIInputStream.h" + +using namespace mozilla; + +NS_IMPL_ISUPPORTS(nsDirIndexParser, nsIRequestObserver, nsIStreamListener, + nsIDirIndexParser) + +void nsDirIndexParser::Init() { + mLineStart = 0; + mFormat[0] = -1; +} + +NS_IMETHODIMP +nsDirIndexParser::SetListener(nsIDirIndexListener* aListener) { + mListener = aListener; + return NS_OK; +} + +NS_IMETHODIMP +nsDirIndexParser::GetListener(nsIDirIndexListener** aListener) { + *aListener = do_AddRef(mListener).take(); + return NS_OK; +} + +NS_IMETHODIMP +nsDirIndexParser::OnStartRequest(nsIRequest* aRequest) { return NS_OK; } + +NS_IMETHODIMP +nsDirIndexParser::OnStopRequest(nsIRequest* aRequest, nsresult aStatusCode) { + // Finish up + if (mBuf.Length() > (uint32_t)mLineStart) { + ProcessData(aRequest); + } + + return NS_OK; +} + +nsDirIndexParser::Field nsDirIndexParser::gFieldTable[] = { + {"Filename", FIELD_FILENAME}, + {"Content-Length", FIELD_CONTENTLENGTH}, + {"Last-Modified", FIELD_LASTMODIFIED}, + {"File-Type", FIELD_FILETYPE}, + {nullptr, FIELD_UNKNOWN}}; + +void nsDirIndexParser::ParseFormat(const char* aFormatStr) { + // Parse a "200" format line, and remember the fields and their + // ordering in mFormat. Multiple 200 lines stomp on each other. + unsigned int formatNum = 0; + mFormat[0] = -1; + + do { + while (*aFormatStr && nsCRT::IsAsciiSpace(char16_t(*aFormatStr))) { + ++aFormatStr; + } + + if (!*aFormatStr) break; + + nsAutoCString name; + int32_t len = 0; + while (aFormatStr[len] && !nsCRT::IsAsciiSpace(char16_t(aFormatStr[len]))) { + ++len; + } + name.Append(aFormatStr, len); + aFormatStr += len; + + // Okay, we're gonna monkey with the nsStr. Bold! + name.SetLength(nsUnescapeCount(name.BeginWriting())); + + for (Field* i = gFieldTable; i->mName; ++i) { + if (name.EqualsIgnoreCase(i->mName)) { + mFormat[formatNum] = i->mType; + mFormat[++formatNum] = -1; + break; + } + } + + } while (*aFormatStr && (formatNum < (ArrayLength(mFormat) - 1))); +} + +void nsDirIndexParser::ParseData(nsIDirIndex* aIdx, char* aDataStr, + int32_t aLineLen) { + // Parse a "201" data line, using the field ordering specified in + // mFormat. + + if (mFormat[0] == -1) { + // Ignore if we haven't seen a format yet. + return; + } + + nsAutoCString filename; + int32_t lineLen = aLineLen; + + for (int32_t i = 0; mFormat[i] != -1; ++i) { + // If we've exhausted the data before we run out of fields, just bail. + if (!*aDataStr || (lineLen < 1)) { + return; + } + + while ((lineLen > 0) && nsCRT::IsAsciiSpace(*aDataStr)) { + ++aDataStr; + --lineLen; + } + + if (lineLen < 1) { + // invalid format, bail + return; + } + + char* value = aDataStr; + if (*aDataStr == '"' || *aDataStr == '\'') { + // it's a quoted string. snarf everything up to the next quote character + const char quotechar = *(aDataStr++); + lineLen--; + ++value; + while ((lineLen > 0) && *aDataStr != quotechar) { + ++aDataStr; + --lineLen; + } + if (lineLen > 0) { + *aDataStr++ = '\0'; + --lineLen; + } + + if (!lineLen) { + // invalid format, bail + return; + } + } else { + // it's unquoted. snarf until we see whitespace. + value = aDataStr; + while ((lineLen > 0) && (!nsCRT::IsAsciiSpace(*aDataStr))) { + ++aDataStr; + --lineLen; + } + if (lineLen > 0) { + *aDataStr++ = '\0'; + --lineLen; + } + // even if we ran out of line length here, there's still a trailing zero + // byte afterwards + } + + fieldType t = fieldType(mFormat[i]); + switch (t) { + case FIELD_FILENAME: { + // don't unescape at this point, so that UnEscapeAndConvert() can + filename = value; + aIdx->SetLocation(filename); + } break; + case FIELD_CONTENTLENGTH: { + int64_t len; + int32_t status = PR_sscanf(value, "%lld", &len); + if (status == 1) { + aIdx->SetSize(len); + } else { + aIdx->SetSize(UINT64_MAX); // UINT64_MAX means unknown + } + } break; + case FIELD_LASTMODIFIED: { + PRTime tm; + nsUnescape(value); + if (PR_ParseTimeString(value, false, &tm) == PR_SUCCESS) { + aIdx->SetLastModified(tm); + } + } break; + case FIELD_FILETYPE: + // unescape in-place + nsUnescape(value); + if (!nsCRT::strcasecmp(value, "directory")) { + aIdx->SetType(nsIDirIndex::TYPE_DIRECTORY); + } else if (!nsCRT::strcasecmp(value, "file")) { + aIdx->SetType(nsIDirIndex::TYPE_FILE); + } else if (!nsCRT::strcasecmp(value, "symbolic-link")) { + aIdx->SetType(nsIDirIndex::TYPE_SYMLINK); + } else { + aIdx->SetType(nsIDirIndex::TYPE_UNKNOWN); + } + break; + case FIELD_UNKNOWN: + // ignore + break; + } + } +} + +NS_IMETHODIMP +nsDirIndexParser::OnDataAvailable(nsIRequest* aRequest, nsIInputStream* aStream, + uint64_t aSourceOffset, uint32_t aCount) { + if (aCount < 1) return NS_OK; + + uint32_t len = mBuf.Length(); + + // Ensure that our mBuf has capacity to hold the data we're about to + // read. + // Before adjusting the capacity, guard against any potential overflow + // resulting from the addition of aCount with len. See Bug 1823551. + NS_ENSURE_TRUE((UINT32_MAX - aCount) >= len, NS_ERROR_FAILURE); + if (!mBuf.SetLength(len + aCount, fallible)) return NS_ERROR_OUT_OF_MEMORY; + + // Now read the data into our buffer. + nsresult rv; + uint32_t count; + rv = aStream->Read(mBuf.BeginWriting() + len, aCount, &count); + if (NS_FAILED(rv)) return rv; + + // Set the string's length according to the amount of data we've read. + // Note: we know this to work on nsCString. This isn't guaranteed to + // work on other strings. + mBuf.SetLength(len + count); + + return ProcessData(aRequest); +} + +nsresult nsDirIndexParser::ProcessData(nsIRequest* aRequest) { + if (!mListener) return NS_ERROR_FAILURE; + + while (true) { + int32_t eol = mBuf.FindCharInSet("\n\r", mLineStart); + if (eol < 0) break; + mBuf.SetCharAt(char16_t('\0'), eol); + + const char* line = mBuf.get() + mLineStart; + + int32_t lineLen = eol - mLineStart; + mLineStart = eol + 1; + + if (lineLen >= 4) { + const char* buf = line; + + if (buf[0] == '2') { + if (buf[1] == '0') { + if (buf[2] == '0' && buf[3] == ':') { + // 200. Define field names + ParseFormat(buf + 4); + } else if (buf[2] == '1' && buf[3] == ':') { + // 201. Field data + nsCOMPtr<nsIDirIndex> idx = new nsDirIndex(); + + ParseData(idx, ((char*)buf) + 4, lineLen - 4); + mListener->OnIndexAvailable(aRequest, idx); + } + } + } + } + } + + return NS_OK; +} diff --git a/netwerk/streamconv/converters/nsDirIndexParser.h b/netwerk/streamconv/converters/nsDirIndexParser.h new file mode 100644 index 0000000000..291a86337e --- /dev/null +++ b/netwerk/streamconv/converters/nsDirIndexParser.h @@ -0,0 +1,65 @@ +/* -*- 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/. */ + +#ifndef __NSDIRINDEX_H_ +#define __NSDIRINDEX_H_ + +#include "nsString.h" +#include "nsCOMPtr.h" +#include "nsIDirIndexListener.h" +#include "mozilla/RefPtr.h" + +class nsIDirIndex; +class nsITextToSubURI; + +/* CID: {a0d6ad32-1dd1-11b2-aa55-a40187b54036} */ + +class nsDirIndexParser : public nsIDirIndexParser { + private: + virtual ~nsDirIndexParser() = default; + + nsDirIndexParser() = default; + void Init(); + + public: + NS_DECL_ISUPPORTS + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSIDIRINDEXPARSER + + static already_AddRefed<nsIDirIndexParser> CreateInstance() { + RefPtr<nsDirIndexParser> parser = new nsDirIndexParser(); + parser->Init(); + return parser.forget(); + } + + enum fieldType { + FIELD_UNKNOWN = 0, // MUST be 0 + FIELD_FILENAME, + FIELD_CONTENTLENGTH, + FIELD_LASTMODIFIED, + FIELD_FILETYPE + }; + + protected: + nsCOMPtr<nsIDirIndexListener> mListener; + + nsCString mBuf; + int32_t mLineStart{0}; + int mFormat[8]{-1}; + + nsresult ProcessData(nsIRequest* aRequest); + void ParseFormat(const char* aFormatStr); + void ParseData(nsIDirIndex* aIdx, char* aDataStr, int32_t lineLen); + + struct Field { + const char* mName; + fieldType mType; + }; + + static Field gFieldTable[]; +}; + +#endif diff --git a/netwerk/streamconv/converters/nsHTTPCompressConv.cpp b/netwerk/streamconv/converters/nsHTTPCompressConv.cpp new file mode 100644 index 0000000000..da430a0611 --- /dev/null +++ b/netwerk/streamconv/converters/nsHTTPCompressConv.cpp @@ -0,0 +1,767 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et 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 "nsHTTPCompressConv.h" +#include "nsCOMPtr.h" +#include "nsCRT.h" +#include "nsError.h" +#include "nsIThreadRetargetableStreamListener.h" +#include "nsStreamUtils.h" +#include "nsStringStream.h" +#include "nsComponentManagerUtils.h" +#include "nsThreadUtils.h" +#include "mozilla/Preferences.h" +#include "mozilla/StaticPrefs_network.h" +#include "mozilla/Logging.h" +#include "nsIForcePendingChannel.h" +#include "nsIRequest.h" +#include "mozilla/UniquePtrExtensions.h" + +// brotli headers +#undef assert +#include "assert.h" +#include "state.h" +#include "brotli/decode.h" + +namespace mozilla { +namespace net { + +extern LazyLogModule gHttpLog; +#define LOG(args) \ + MOZ_LOG(mozilla::net::gHttpLog, mozilla::LogLevel::Debug, args) + +class BrotliWrapper { + public: + BrotliWrapper() { + BrotliDecoderStateInit(&mState, nullptr, nullptr, nullptr); + } + ~BrotliWrapper() { BrotliDecoderStateCleanup(&mState); } + + BrotliDecoderState mState{}; + Atomic<size_t, Relaxed> mTotalOut{0}; + nsresult mStatus = NS_OK; + Atomic<bool, Relaxed> mBrotliStateIsStreamEnd{false}; + + nsIRequest* mRequest{nullptr}; + nsISupports* mContext{nullptr}; + uint64_t mSourceOffset{0}; +}; + +// nsISupports implementation +NS_IMPL_ISUPPORTS(nsHTTPCompressConv, nsIStreamConverter, nsIStreamListener, + nsIRequestObserver, nsICompressConvStats, + nsIThreadRetargetableStreamListener) + +// nsFTPDirListingConv methods +nsHTTPCompressConv::nsHTTPCompressConv() { + LOG(("nsHttpCompresssConv %p ctor\n", this)); + if (NS_IsMainThread()) { + mFailUncleanStops = + Preferences::GetBool("network.http.enforce-framing.http", false); + } else { + mFailUncleanStops = false; + } +} + +nsHTTPCompressConv::~nsHTTPCompressConv() { + LOG(("nsHttpCompresssConv %p dtor\n", this)); + if (mInpBuffer) { + free(mInpBuffer); + } + + if (mOutBuffer) { + free(mOutBuffer); + } + + // For some reason we are not getting Z_STREAM_END. But this was also seen + // for mozilla bug 198133. Need to handle this case. + if (mStreamInitialized && !mStreamEnded) { + inflateEnd(&d_stream); + } +} + +NS_IMETHODIMP +nsHTTPCompressConv::GetDecodedDataLength(uint64_t* aDecodedDataLength) { + *aDecodedDataLength = mDecodedDataLength; + return NS_OK; +} + +NS_IMETHODIMP +nsHTTPCompressConv::AsyncConvertData(const char* aFromType, const char* aToType, + nsIStreamListener* aListener, + nsISupports* aCtxt) { + if (!nsCRT::strncasecmp(aFromType, HTTP_COMPRESS_TYPE, + sizeof(HTTP_COMPRESS_TYPE) - 1) || + !nsCRT::strncasecmp(aFromType, HTTP_X_COMPRESS_TYPE, + sizeof(HTTP_X_COMPRESS_TYPE) - 1)) { + mMode = HTTP_COMPRESS_COMPRESS; + } else if (!nsCRT::strncasecmp(aFromType, HTTP_GZIP_TYPE, + sizeof(HTTP_GZIP_TYPE) - 1) || + !nsCRT::strncasecmp(aFromType, HTTP_X_GZIP_TYPE, + sizeof(HTTP_X_GZIP_TYPE) - 1)) { + mMode = HTTP_COMPRESS_GZIP; + } else if (!nsCRT::strncasecmp(aFromType, HTTP_DEFLATE_TYPE, + sizeof(HTTP_DEFLATE_TYPE) - 1)) { + mMode = HTTP_COMPRESS_DEFLATE; + } else if (!nsCRT::strncasecmp(aFromType, HTTP_BROTLI_TYPE, + sizeof(HTTP_BROTLI_TYPE) - 1)) { + mMode = HTTP_COMPRESS_BROTLI; + } + LOG(("nsHttpCompresssConv %p AsyncConvertData %s %s mode %d\n", this, + aFromType, aToType, (CompressMode)mMode)); + + MutexAutoLock lock(mMutex); + // hook ourself up with the receiving listener. + mListener = aListener; + + return NS_OK; +} + +NS_IMETHODIMP +nsHTTPCompressConv::GetConvertedType(const nsACString& aFromType, + nsIChannel* aChannel, + nsACString& aToType) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsHTTPCompressConv::OnStartRequest(nsIRequest* request) { + LOG(("nsHttpCompresssConv %p onstart\n", this)); + nsCOMPtr<nsIStreamListener> listener; + { + MutexAutoLock lock(mMutex); + listener = mListener; + } + return listener->OnStartRequest(request); +} + +NS_IMETHODIMP +nsHTTPCompressConv::OnStopRequest(nsIRequest* request, nsresult aStatus) { + nsresult status = aStatus; + LOG(("nsHttpCompresssConv %p onstop %" PRIx32 "\n", this, + static_cast<uint32_t>(aStatus))); + + // Framing integrity is enforced for content-encoding: gzip, but not for + // content-encoding: deflate. Note that gzip vs deflate is NOT determined + // by content sniffing but only via header. + if (!mStreamEnded && NS_SUCCEEDED(status) && + (mFailUncleanStops && (mMode == HTTP_COMPRESS_GZIP))) { + // This is not a clean end of gzip stream: the transfer is incomplete. + status = NS_ERROR_NET_PARTIAL_TRANSFER; + LOG(("nsHttpCompresssConv %p onstop partial gzip\n", this)); + } + if (NS_SUCCEEDED(status) && mMode == HTTP_COMPRESS_BROTLI) { + nsCOMPtr<nsIForcePendingChannel> fpChannel = do_QueryInterface(request); + bool isPending = false; + if (request) { + request->IsPending(&isPending); + } + if (fpChannel && !isPending) { + fpChannel->ForcePending(true); + } + bool allowTruncatedEmpty = + StaticPrefs::network_compress_allow_truncated_empty_brotli(); + if (mBrotli && ((allowTruncatedEmpty && NS_FAILED(mBrotli->mStatus)) || + (!allowTruncatedEmpty && mBrotli->mTotalOut == 0 && + !mBrotli->mBrotliStateIsStreamEnd))) { + status = NS_ERROR_INVALID_CONTENT_ENCODING; + } + LOG(("nsHttpCompresssConv %p onstop brotlihandler rv %" PRIx32 "\n", this, + static_cast<uint32_t>(status))); + if (fpChannel && !isPending) { + fpChannel->ForcePending(false); + } + } + + nsCOMPtr<nsIStreamListener> listener; + { + MutexAutoLock lock(mMutex); + listener = mListener; + } + return listener->OnStopRequest(request, status); +} + +/* static */ +nsresult nsHTTPCompressConv::BrotliHandler(nsIInputStream* stream, + void* closure, const char* dataIn, + uint32_t, uint32_t aAvail, + uint32_t* countRead) { + MOZ_ASSERT(stream); + nsHTTPCompressConv* self = static_cast<nsHTTPCompressConv*>(closure); + *countRead = 0; + + const size_t kOutSize = 128 * 1024; // just a chunk size, we call in a loop + uint8_t* outPtr; + size_t outSize; + size_t avail = aAvail; + BrotliDecoderResult res; + + if (!self->mBrotli) { + *countRead = aAvail; + return NS_OK; + } + + auto outBuffer = MakeUniqueFallible<uint8_t[]>(kOutSize); + if (outBuffer == nullptr) { + self->mBrotli->mStatus = NS_ERROR_OUT_OF_MEMORY; + return self->mBrotli->mStatus; + } + do { + outSize = kOutSize; + outPtr = outBuffer.get(); + + // brotli api is documented in brotli/dec/decode.h and brotli/dec/decode.c + LOG(("nsHttpCompresssConv %p brotlihandler decompress %zu\n", self, avail)); + size_t totalOut = self->mBrotli->mTotalOut; + res = ::BrotliDecoderDecompressStream( + &self->mBrotli->mState, &avail, + reinterpret_cast<const unsigned char**>(&dataIn), &outSize, &outPtr, + &totalOut); + outSize = kOutSize - outSize; + self->mBrotli->mTotalOut = totalOut; + self->mBrotli->mBrotliStateIsStreamEnd = + BrotliDecoderIsFinished(&self->mBrotli->mState); + LOG(("nsHttpCompresssConv %p brotlihandler decompress rv=%" PRIx32 + " out=%zu\n", + self, static_cast<uint32_t>(res), outSize)); + + if (res == BROTLI_DECODER_RESULT_ERROR) { + LOG(("nsHttpCompressConv %p marking invalid encoding", self)); + self->mBrotli->mStatus = NS_ERROR_INVALID_CONTENT_ENCODING; + return self->mBrotli->mStatus; + } + + // in 'the current implementation' brotli must consume everything before + // asking for more input + if (res == BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT) { + MOZ_ASSERT(!avail); + if (avail) { + LOG(("nsHttpCompressConv %p did not consume all input", self)); + self->mBrotli->mStatus = NS_ERROR_UNEXPECTED; + return self->mBrotli->mStatus; + } + } + + auto callOnDataAvailable = [&](uint64_t aSourceOffset, const char* aBuffer, + uint32_t aCount) { + nsresult rv = self->do_OnDataAvailable(self->mBrotli->mRequest, + aSourceOffset, aBuffer, aCount); + LOG(("nsHttpCompressConv %p BrotliHandler ODA rv=%" PRIx32, self, + static_cast<uint32_t>(rv))); + if (NS_FAILED(rv)) { + self->mBrotli->mStatus = rv; + } + + return rv; + }; + + if (outSize > 0) { + if (NS_FAILED(callOnDataAvailable( + self->mBrotli->mSourceOffset, + reinterpret_cast<const char*>(outBuffer.get()), outSize))) { + return self->mBrotli->mStatus; + } + } + + // See bug 1759745. If the decoder has more output data, take it. + while (::BrotliDecoderHasMoreOutput(&self->mBrotli->mState)) { + outSize = kOutSize; + const uint8_t* buffer = + ::BrotliDecoderTakeOutput(&self->mBrotli->mState, &outSize); + if (NS_FAILED(callOnDataAvailable(self->mBrotli->mSourceOffset, + reinterpret_cast<const char*>(buffer), + outSize))) { + return self->mBrotli->mStatus; + } + } + + if (res == BROTLI_DECODER_RESULT_SUCCESS || + res == BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT) { + *countRead = aAvail; + return NS_OK; + } + MOZ_ASSERT(res == BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT); + } while (res == BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT); + + self->mBrotli->mStatus = NS_ERROR_UNEXPECTED; + return self->mBrotli->mStatus; +} + +NS_IMETHODIMP +nsHTTPCompressConv::OnDataAvailable(nsIRequest* request, nsIInputStream* iStr, + uint64_t aSourceOffset, uint32_t aCount) { + nsresult rv = NS_ERROR_INVALID_CONTENT_ENCODING; + uint32_t streamLen = aCount; + LOG(("nsHttpCompressConv %p OnDataAvailable %d", this, aCount)); + + if (streamLen == 0) { + NS_ERROR("count of zero passed to OnDataAvailable"); + return NS_ERROR_UNEXPECTED; + } + + if (mStreamEnded) { + // Hmm... this may just indicate that the data stream is done and that + // what's left is either metadata or padding of some sort.... throwing + // it out is probably the safe thing to do. + uint32_t n; + return iStr->ReadSegments(NS_DiscardSegment, nullptr, streamLen, &n); + } + + switch (mMode) { + case HTTP_COMPRESS_GZIP: + streamLen = check_header(iStr, streamLen, &rv); + + if (rv != NS_OK) { + return rv; + } + + if (streamLen == 0) { + return NS_OK; + } + + [[fallthrough]]; + + case HTTP_COMPRESS_DEFLATE: + + if (mInpBuffer != nullptr && streamLen > mInpBufferLen) { + unsigned char* originalInpBuffer = mInpBuffer; + if (!(mInpBuffer = (unsigned char*)realloc( + originalInpBuffer, mInpBufferLen = streamLen))) { + free(originalInpBuffer); + } + + if (mOutBufferLen < streamLen * 2) { + unsigned char* originalOutBuffer = mOutBuffer; + if (!(mOutBuffer = (unsigned char*)realloc( + mOutBuffer, mOutBufferLen = streamLen * 3))) { + free(originalOutBuffer); + } + } + + if (mInpBuffer == nullptr || mOutBuffer == nullptr) { + return NS_ERROR_OUT_OF_MEMORY; + } + } + + if (mInpBuffer == nullptr) { + mInpBuffer = (unsigned char*)malloc(mInpBufferLen = streamLen); + } + + if (mOutBuffer == nullptr) { + mOutBuffer = (unsigned char*)malloc(mOutBufferLen = streamLen * 3); + } + + if (mInpBuffer == nullptr || mOutBuffer == nullptr) { + return NS_ERROR_OUT_OF_MEMORY; + } + + uint32_t unused; + iStr->Read((char*)mInpBuffer, streamLen, &unused); + + if (mMode == HTTP_COMPRESS_DEFLATE) { + if (!mStreamInitialized) { + memset(&d_stream, 0, sizeof(d_stream)); + + if (inflateInit(&d_stream) != Z_OK) { + return NS_ERROR_FAILURE; + } + + mStreamInitialized = true; + } + d_stream.next_in = mInpBuffer; + d_stream.avail_in = (uInt)streamLen; + + mDummyStreamInitialised = false; + for (;;) { + d_stream.next_out = mOutBuffer; + d_stream.avail_out = (uInt)mOutBufferLen; + + int code = inflate(&d_stream, Z_NO_FLUSH); + unsigned bytesWritten = (uInt)mOutBufferLen - d_stream.avail_out; + + if (code == Z_STREAM_END) { + if (bytesWritten) { + rv = do_OnDataAvailable(request, aSourceOffset, (char*)mOutBuffer, + bytesWritten); + if (NS_FAILED(rv)) { + return rv; + } + } + + inflateEnd(&d_stream); + mStreamEnded = true; + break; + } + if (code == Z_OK) { + if (bytesWritten) { + rv = do_OnDataAvailable(request, aSourceOffset, (char*)mOutBuffer, + bytesWritten); + if (NS_FAILED(rv)) { + return rv; + } + } + } else if (code == Z_BUF_ERROR) { + if (bytesWritten) { + rv = do_OnDataAvailable(request, aSourceOffset, (char*)mOutBuffer, + bytesWritten); + if (NS_FAILED(rv)) { + return rv; + } + } + break; + } else if (code == Z_DATA_ERROR) { + // some servers (notably Apache with mod_deflate) don't generate + // zlib headers insert a dummy header and try again + static char dummy_head[2] = { + 0x8 + 0x7 * 0x10, + (((0x8 + 0x7 * 0x10) * 0x100 + 30) / 31 * 31) & 0xFF, + }; + inflateReset(&d_stream); + d_stream.next_in = (Bytef*)dummy_head; + d_stream.avail_in = sizeof(dummy_head); + + code = inflate(&d_stream, Z_NO_FLUSH); + if (code != Z_OK) { + return NS_ERROR_FAILURE; + } + + // stop an endless loop caused by non-deflate data being labelled as + // deflate + if (mDummyStreamInitialised) { + NS_WARNING( + "endless loop detected" + " - invalid deflate"); + return NS_ERROR_INVALID_CONTENT_ENCODING; + } + mDummyStreamInitialised = true; + // reset stream pointers to our original data + d_stream.next_in = mInpBuffer; + d_stream.avail_in = (uInt)streamLen; + } else { + return NS_ERROR_INVALID_CONTENT_ENCODING; + } + } /* for */ + } else { + if (!mStreamInitialized) { + memset(&d_stream, 0, sizeof(d_stream)); + + if (inflateInit2(&d_stream, -MAX_WBITS) != Z_OK) { + return NS_ERROR_FAILURE; + } + + mStreamInitialized = true; + } + + d_stream.next_in = mInpBuffer; + d_stream.avail_in = (uInt)streamLen; + + for (;;) { + d_stream.next_out = mOutBuffer; + d_stream.avail_out = (uInt)mOutBufferLen; + + int code = inflate(&d_stream, Z_NO_FLUSH); + unsigned bytesWritten = (uInt)mOutBufferLen - d_stream.avail_out; + + if (code == Z_STREAM_END) { + if (bytesWritten) { + rv = do_OnDataAvailable(request, aSourceOffset, (char*)mOutBuffer, + bytesWritten); + if (NS_FAILED(rv)) { + return rv; + } + } + + inflateEnd(&d_stream); + mStreamEnded = true; + break; + } + if (code == Z_OK) { + if (bytesWritten) { + rv = do_OnDataAvailable(request, aSourceOffset, (char*)mOutBuffer, + bytesWritten); + if (NS_FAILED(rv)) { + return rv; + } + } + } else if (code == Z_BUF_ERROR) { + if (bytesWritten) { + rv = do_OnDataAvailable(request, aSourceOffset, (char*)mOutBuffer, + bytesWritten); + if (NS_FAILED(rv)) { + return rv; + } + } + break; + } else { + return NS_ERROR_INVALID_CONTENT_ENCODING; + } + } /* for */ + } /* gzip */ + break; + + case HTTP_COMPRESS_BROTLI: { + if (!mBrotli) { + mBrotli = MakeUnique<BrotliWrapper>(); + } + + mBrotli->mRequest = request; + mBrotli->mContext = nullptr; + mBrotli->mSourceOffset = aSourceOffset; + + uint32_t countRead; + rv = iStr->ReadSegments(BrotliHandler, this, streamLen, &countRead); + if (NS_SUCCEEDED(rv)) { + rv = mBrotli->mStatus; + } + if (NS_FAILED(rv)) { + return rv; + } + } break; + + default: + nsCOMPtr<nsIStreamListener> listener; + { + MutexAutoLock lock(mMutex); + listener = mListener; + } + rv = listener->OnDataAvailable(request, iStr, aSourceOffset, aCount); + if (NS_FAILED(rv)) { + return rv; + } + } /* switch */ + + return NS_OK; +} /* OnDataAvailable */ + +// XXX/ruslan: need to implement this too + +NS_IMETHODIMP +nsHTTPCompressConv::Convert(nsIInputStream* aFromStream, const char* aFromType, + const char* aToType, nsISupports* aCtxt, + nsIInputStream** _retval) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +nsresult nsHTTPCompressConv::do_OnDataAvailable(nsIRequest* request, + uint64_t offset, + const char* buffer, + uint32_t count) { + if (!mStream) { + mStream = do_CreateInstance(NS_STRINGINPUTSTREAM_CONTRACTID); + NS_ENSURE_STATE(mStream); + } + + mStream->ShareData(buffer, count); + + nsCOMPtr<nsIStreamListener> listener; + { + MutexAutoLock lock(mMutex); + listener = mListener; + } + nsresult rv = listener->OnDataAvailable(request, mStream, offset, count); + + // Make sure the stream no longer references |buffer| in case our listener + // is crazy enough to try to read from |mStream| after ODA. + mStream->ShareData("", 0); + mDecodedDataLength += count; + + return rv; +} + +#define ASCII_FLAG 0x01 /* bit 0 set: file probably ascii text */ +#define HEAD_CRC 0x02 /* bit 1 set: header CRC present */ +#define EXTRA_FIELD 0x04 /* bit 2 set: extra field present */ +#define ORIG_NAME 0x08 /* bit 3 set: original file name present */ +#define COMMENT 0x10 /* bit 4 set: file comment present */ +#define RESERVED 0xE0 /* bits 5..7: reserved */ + +static unsigned gz_magic[2] = {0x1f, 0x8b}; /* gzip magic header */ + +uint32_t nsHTTPCompressConv::check_header(nsIInputStream* iStr, + uint32_t streamLen, nsresult* rs) { + enum { + GZIP_INIT = 0, + GZIP_OS, + GZIP_EXTRA0, + GZIP_EXTRA1, + GZIP_EXTRA2, + GZIP_ORIG, + GZIP_COMMENT, + GZIP_CRC + }; + char c; + + *rs = NS_OK; + + if (mCheckHeaderDone) { + return streamLen; + } + + while (streamLen) { + switch (hMode) { + case GZIP_INIT: + uint32_t unused; + iStr->Read(&c, 1, &unused); + streamLen--; + + if (mSkipCount == 0 && ((unsigned)c & 0377) != gz_magic[0]) { + *rs = NS_ERROR_INVALID_CONTENT_ENCODING; + return 0; + } + + if (mSkipCount == 1 && ((unsigned)c & 0377) != gz_magic[1]) { + *rs = NS_ERROR_INVALID_CONTENT_ENCODING; + return 0; + } + + if (mSkipCount == 2 && ((unsigned)c & 0377) != Z_DEFLATED) { + *rs = NS_ERROR_INVALID_CONTENT_ENCODING; + return 0; + } + + mSkipCount++; + if (mSkipCount == 4) { + mFlags = (unsigned)c & 0377; + if (mFlags & RESERVED) { + *rs = NS_ERROR_INVALID_CONTENT_ENCODING; + return 0; + } + hMode = GZIP_OS; + mSkipCount = 0; + } + break; + + case GZIP_OS: + iStr->Read(&c, 1, &unused); + streamLen--; + mSkipCount++; + + if (mSkipCount == 6) { + hMode = GZIP_EXTRA0; + } + break; + + case GZIP_EXTRA0: + if (mFlags & EXTRA_FIELD) { + iStr->Read(&c, 1, &unused); + streamLen--; + mLen = (uInt)c & 0377; + hMode = GZIP_EXTRA1; + } else { + hMode = GZIP_ORIG; + } + break; + + case GZIP_EXTRA1: + iStr->Read(&c, 1, &unused); + streamLen--; + mLen |= ((uInt)c & 0377) << 8; + mSkipCount = 0; + hMode = GZIP_EXTRA2; + break; + + case GZIP_EXTRA2: + if (mSkipCount == mLen) { + hMode = GZIP_ORIG; + } else { + iStr->Read(&c, 1, &unused); + streamLen--; + mSkipCount++; + } + break; + + case GZIP_ORIG: + if (mFlags & ORIG_NAME) { + iStr->Read(&c, 1, &unused); + streamLen--; + if (c == 0) hMode = GZIP_COMMENT; + } else { + hMode = GZIP_COMMENT; + } + break; + + case GZIP_COMMENT: + if (mFlags & COMMENT) { + iStr->Read(&c, 1, &unused); + streamLen--; + if (c == 0) { + hMode = GZIP_CRC; + mSkipCount = 0; + } + } else { + hMode = GZIP_CRC; + mSkipCount = 0; + } + break; + + case GZIP_CRC: + if (mFlags & HEAD_CRC) { + iStr->Read(&c, 1, &unused); + streamLen--; + mSkipCount++; + if (mSkipCount == 2) { + mCheckHeaderDone = true; + return streamLen; + } + } else { + mCheckHeaderDone = true; + return streamLen; + } + break; + } + } + return streamLen; +} + +NS_IMETHODIMP +nsHTTPCompressConv::CheckListenerChain() { + nsCOMPtr<nsIThreadRetargetableStreamListener> listener; + { + MutexAutoLock lock(mMutex); + listener = do_QueryInterface(mListener); + } + + if (!listener) { + return NS_ERROR_NO_INTERFACE; + } + + return listener->CheckListenerChain(); +} + +NS_IMETHODIMP +nsHTTPCompressConv::OnDataFinished(nsresult aStatus) { + nsCOMPtr<nsIThreadRetargetableStreamListener> listener; + + { + MutexAutoLock lock(mMutex); + listener = do_QueryInterface(mListener); + } + + if (listener) { + return listener->OnDataFinished(aStatus); + } + + return NS_OK; +} + +} // namespace net +} // namespace mozilla + +nsresult NS_NewHTTPCompressConv( + mozilla::net::nsHTTPCompressConv** aHTTPCompressConv) { + MOZ_ASSERT(aHTTPCompressConv != nullptr, "null ptr"); + if (!aHTTPCompressConv) { + return NS_ERROR_NULL_POINTER; + } + + RefPtr<mozilla::net::nsHTTPCompressConv> outVal = + new mozilla::net::nsHTTPCompressConv(); + if (!outVal) { + return NS_ERROR_OUT_OF_MEMORY; + } + outVal.forget(aHTTPCompressConv); + return NS_OK; +} diff --git a/netwerk/streamconv/converters/nsHTTPCompressConv.h b/netwerk/streamconv/converters/nsHTTPCompressConv.h new file mode 100644 index 0000000000..4ba2d9819c --- /dev/null +++ b/netwerk/streamconv/converters/nsHTTPCompressConv.h @@ -0,0 +1,109 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et 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/. */ + +#if !defined(__nsHTTPCompressConv__h__) +# define __nsHTTPCompressConv__h__ 1 + +# include "nsIStreamConverter.h" +# include "nsICompressConvStats.h" +# include "nsIThreadRetargetableStreamListener.h" +# include "nsCOMPtr.h" +# include "mozilla/Atomics.h" +# include "mozilla/Mutex.h" + +# include "zlib.h" + +class nsIStringInputStream; + +# define NS_HTTPCOMPRESSCONVERTER_CID \ + { \ + /* 66230b2b-17fa-4bd3-abf4-07986151022d */ \ + 0x66230b2b, 0x17fa, 0x4bd3, { \ + 0xab, 0xf4, 0x07, 0x98, 0x61, 0x51, 0x02, 0x2d \ + } \ + } + +# define HTTP_DEFLATE_TYPE "deflate" +# define HTTP_GZIP_TYPE "gzip" +# define HTTP_X_GZIP_TYPE "x-gzip" +# define HTTP_COMPRESS_TYPE "compress" +# define HTTP_X_COMPRESS_TYPE "x-compress" +# define HTTP_BROTLI_TYPE "br" +# define HTTP_IDENTITY_TYPE "identity" +# define HTTP_UNCOMPRESSED_TYPE "uncompressed" + +namespace mozilla { +namespace net { + +class BrotliWrapper; + +class nsHTTPCompressConv : public nsIStreamConverter, + public nsICompressConvStats { + public: + // nsISupports methods + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSICOMPRESSCONVSTATS + NS_DECL_NSITHREADRETARGETABLESTREAMLISTENER + + // nsIStreamConverter methods + NS_DECL_NSISTREAMCONVERTER + + nsHTTPCompressConv(); + + using CompressMode = enum { + HTTP_COMPRESS_GZIP, + HTTP_COMPRESS_DEFLATE, + HTTP_COMPRESS_COMPRESS, + HTTP_COMPRESS_BROTLI, + HTTP_COMPRESS_IDENTITY + }; + + private: + virtual ~nsHTTPCompressConv(); + + nsCOMPtr<nsIStreamListener> + mListener; // this guy gets the converted data via his OnDataAvailable () + Atomic<CompressMode, Relaxed> mMode{HTTP_COMPRESS_IDENTITY}; + + unsigned char* mOutBuffer{nullptr}; + unsigned char* mInpBuffer{nullptr}; + + uint32_t mOutBufferLen{0}; + uint32_t mInpBufferLen{0}; + + UniquePtr<BrotliWrapper> mBrotli; + + nsCOMPtr<nsIStringInputStream> mStream; + + static nsresult BrotliHandler(nsIInputStream* stream, void* closure, + const char* dataIn, uint32_t, uint32_t avail, + uint32_t* countRead); + + nsresult do_OnDataAvailable(nsIRequest* request, uint64_t aSourceOffset, + const char* buffer, uint32_t aCount); + + bool mCheckHeaderDone{false}; + Atomic<bool> mStreamEnded{false}; + bool mStreamInitialized{false}; + bool mDummyStreamInitialised{false}; + bool mFailUncleanStops; + + z_stream d_stream{}; + unsigned mLen{0}, hMode{0}, mSkipCount{0}, mFlags{0}; + + uint32_t check_header(nsIInputStream* iStr, uint32_t streamLen, nsresult* rs); + + Atomic<uint32_t, Relaxed> mDecodedDataLength{0}; + + mutable mozilla::Mutex mMutex MOZ_UNANNOTATED{"nsHTTPCompressConv"}; +}; + +} // namespace net +} // namespace mozilla + +#endif diff --git a/netwerk/streamconv/converters/nsICompressConvStats.idl b/netwerk/streamconv/converters/nsICompressConvStats.idl new file mode 100644 index 0000000000..a8837563ed --- /dev/null +++ b/netwerk/streamconv/converters/nsICompressConvStats.idl @@ -0,0 +1,17 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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 "nsISupports.idl" + +/** + * nsICompressConvStats + * + * This interface allows for the observation of decoded resource sizes + */ +[builtinclass, scriptable, uuid(58172ad0-46a9-4893-8fde-cd909c10792a)] +interface nsICompressConvStats : nsISupports +{ + readonly attribute uint64_t decodedDataLength; +}; diff --git a/netwerk/streamconv/converters/nsIndexedToHTML.cpp b/netwerk/streamconv/converters/nsIndexedToHTML.cpp new file mode 100644 index 0000000000..efc8962459 --- /dev/null +++ b/netwerk/streamconv/converters/nsIndexedToHTML.cpp @@ -0,0 +1,825 @@ +/* -*- 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 "nsIndexedToHTML.h" + +#include "mozilla/Encoding.h" +#include "mozilla/intl/AppDateTimeFormat.h" +#include "mozilla/intl/LocaleService.h" +#include "nsIThreadRetargetableStreamListener.h" +#include "nsNetUtil.h" +#include "netCore.h" +#include "nsStringStream.h" +#include "nsIFile.h" +#include "nsIFileURL.h" +#include "nsEscape.h" +#include "nsIDirIndex.h" +#include "nsURLHelper.h" +#include "nsIStringBundle.h" +#include "nsDirIndexParser.h" +#include "nsNativeCharsetUtils.h" +#include "nsString.h" +#include "nsContentUtils.h" +#include <algorithm> +#include "nsIChannel.h" +#include "mozilla/Unused.h" +#include "nsIURIMutator.h" +#include "nsITextToSubURI.h" + +using mozilla::intl::LocaleService; +using namespace mozilla; + +NS_IMPL_ISUPPORTS(nsIndexedToHTML, nsIDirIndexListener, nsIStreamConverter, + nsIThreadRetargetableStreamListener, nsIRequestObserver, + nsIStreamListener) + +static void AppendNonAsciiToNCR(const nsAString& in, nsCString& out) { + nsAString::const_iterator start, end; + + in.BeginReading(start); + in.EndReading(end); + + while (start != end) { + if (*start < 128) { + out.Append(*start++); + } else { + out.AppendLiteral("&#x"); + out.AppendInt(*start++, 16); + out.Append(';'); + } + } +} + +nsresult nsIndexedToHTML::Create(REFNSIID aIID, void** aResult) { + nsresult rv; + + nsIndexedToHTML* _s = new nsIndexedToHTML(); + if (_s == nullptr) return NS_ERROR_OUT_OF_MEMORY; + + rv = _s->QueryInterface(aIID, aResult); + return rv; +} + +nsresult nsIndexedToHTML::Init(nsIStreamListener* aListener) { + nsresult rv = NS_OK; + + mListener = aListener; + + nsCOMPtr<nsIStringBundleService> sbs = + do_GetService(NS_STRINGBUNDLE_CONTRACTID, &rv); + if (NS_FAILED(rv)) return rv; + rv = sbs->CreateBundle(NECKO_MSGS_URL, getter_AddRefs(mBundle)); + + mExpectAbsLoc = false; + + return rv; +} + +NS_IMETHODIMP +nsIndexedToHTML::Convert(nsIInputStream* aFromStream, const char* aFromType, + const char* aToType, nsISupports* aCtxt, + nsIInputStream** res) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsIndexedToHTML::AsyncConvertData(const char* aFromType, const char* aToType, + nsIStreamListener* aListener, + nsISupports* aCtxt) { + return Init(aListener); +} + +NS_IMETHODIMP +nsIndexedToHTML::GetConvertedType(const nsACString& aFromType, + nsIChannel* aChannel, nsACString& aToType) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsIndexedToHTML::OnStartRequest(nsIRequest* request) { + nsCString buffer; + nsresult rv = DoOnStartRequest(request, buffer); + if (NS_FAILED(rv)) { + request->Cancel(rv); + } + + rv = mListener->OnStartRequest(request); + if (NS_FAILED(rv)) return rv; + + // The request may have been canceled, and if that happens, we want to + // suppress calls to OnDataAvailable. + request->GetStatus(&rv); + if (NS_FAILED(rv)) return rv; + + // Push our buffer to the listener. + + rv = SendToListener(request, buffer); + return rv; +} + +nsresult nsIndexedToHTML::DoOnStartRequest(nsIRequest* request, + nsCString& aBuffer) { + nsresult rv; + + nsCOMPtr<nsIChannel> channel = do_QueryInterface(request); + nsCOMPtr<nsIURI> uri; + rv = channel->GetOriginalURI(getter_AddRefs(uri)); + if (NS_FAILED(rv)) return rv; + + // We use the original URI for the title and parent link when it's a + // resource:// url, instead of the jar:file:// url it resolves to. + if (!uri->SchemeIs("resource")) { + rv = channel->GetURI(getter_AddRefs(uri)); + if (NS_FAILED(rv)) return rv; + } + + channel->SetContentType("text/html"_ns); + + mParser = nsDirIndexParser::CreateInstance(); + if (!mParser) return NS_ERROR_FAILURE; + + rv = mParser->SetListener(this); + if (NS_FAILED(rv)) return rv; + + rv = mParser->OnStartRequest(request); + if (NS_FAILED(rv)) return rv; + + nsAutoCString baseUri, titleUri; + rv = uri->GetAsciiSpec(baseUri); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr<nsIURI> titleURL; + rv = NS_MutateURI(uri).SetQuery(""_ns).SetRef(""_ns).Finalize(titleURL); + if (NS_FAILED(rv)) { + titleURL = uri; + } + + nsCString parentStr; + + nsCString buffer; + buffer.AppendLiteral("<!DOCTYPE html>\n<html>\n<head>\n"); + + // XXX - should be using the 300: line from the parser. + // We can't guarantee that that comes before any entry, so we'd have to + // buffer, and do other painful stuff. + // I'll deal with this when I make the changes to handle welcome messages + // The .. stuff should also come from the lower level protocols, but that + // would muck up the XUL display + // - bbaetz + + if (uri->SchemeIs("file")) { + nsCOMPtr<nsIFileURL> fileUrl = do_QueryInterface(uri); + nsCOMPtr<nsIFile> file; + rv = fileUrl->GetFile(getter_AddRefs(file)); + if (NS_FAILED(rv)) return rv; + + nsAutoCString url; + rv = net_GetURLSpecFromFile(file, url); + if (NS_FAILED(rv)) return rv; + baseUri.Assign(url); + + nsCOMPtr<nsIFile> parent; + rv = file->GetParent(getter_AddRefs(parent)); + + if (parent && NS_SUCCEEDED(rv)) { + net_GetURLSpecFromDir(parent, url); + if (NS_FAILED(rv)) return rv; + parentStr.Assign(url); + } + + // Directory index will be always encoded in UTF-8 if this is file url + buffer.AppendLiteral("<meta charset=\"UTF-8\">\n"); + + } else if (uri->SchemeIs("jar")) { + nsAutoCString path; + rv = uri->GetPathQueryRef(path); + if (NS_FAILED(rv)) return rv; + + // a top-level jar directory URL is of the form jar:foo.zip!/ + // path will be of the form foo.zip!/, and its last two characters + // will be "!/" + // XXX this won't work correctly when the name of the directory being + // XXX displayed ends with "!", but then again, jar: URIs don't deal + // XXX particularly well with such directories anyway + if (!StringEndsWith(path, "!/"_ns)) { + rv = uri->Resolve(".."_ns, parentStr); + if (NS_FAILED(rv)) return rv; + } + } else { + // default behavior for other protocols is to assume the channel's + // URL references a directory ending in '/' -- fixup if necessary. + nsAutoCString path; + rv = uri->GetPathQueryRef(path); + if (NS_FAILED(rv)) return rv; + if (baseUri.Last() != '/') { + baseUri.Append('/'); + path.Append('/'); + mozilla::Unused << NS_MutateURI(uri).SetPathQueryRef(path).Finalize(uri); + } + if (!path.EqualsLiteral("/")) { + rv = uri->Resolve(".."_ns, parentStr); + if (NS_FAILED(rv)) return rv; + } + } + + rv = titleURL->GetAsciiSpec(titleUri); + if (NS_FAILED(rv)) { + return rv; + } + + buffer.AppendLiteral( + "<style type=\"text/css\">\n" + ":root {\n" + " font-family: sans-serif;\n" + "}\n" + "img {\n" + " border: 0;\n" + "}\n" + "th {\n" + " text-align: start;\n" + " white-space: nowrap;\n" + "}\n" + "th > a {\n" + " color: inherit;\n" + "}\n" + "table[order] > thead > tr > th {\n" + " cursor: pointer;\n" + "}\n" + "table[order] > thead > tr > th::after {\n" + " display: none;\n" + " width: .8em;\n" + " margin-inline-end: -.8em;\n" + " text-align: end;\n" + "}\n" + "table[order=\"asc\"] > thead > tr > th::after {\n" + " content: \"\\2193\"; /* DOWNWARDS ARROW (U+2193) */\n" + "}\n" + "table[order=\"desc\"] > thead > tr > th::after {\n" + " content: \"\\2191\"; /* UPWARDS ARROW (U+2191) */\n" + "}\n" + "table[order][order-by=\"0\"] > thead > tr > th:first-child > a ,\n" + "table[order][order-by=\"1\"] > thead > tr > th:first-child + th > a ,\n" + "table[order][order-by=\"2\"] > thead > tr > th:first-child + th + th > " + "a {\n" + " text-decoration: underline;\n" + "}\n" + "table[order][order-by=\"0\"] > thead > tr > th:first-child::after ,\n" + "table[order][order-by=\"1\"] > thead > tr > th:first-child + th::after " + ",\n" + "table[order][order-by=\"2\"] > thead > tr > th:first-child + th + " + "th::after {\n" + " display: inline-block;\n" + "}\n" + "table.remove-hidden > tbody > tr.hidden-object {\n" + " display: none;\n" + "}\n" + "td {\n" + " white-space: nowrap;\n" + "}\n" + "table.ellipsis {\n" + " width: 100%;\n" + " table-layout: fixed;\n" + " border-spacing: 0;\n" + "}\n" + "table.ellipsis > tbody > tr > td {\n" + " overflow: hidden;\n" + " text-overflow: ellipsis;\n" + "}\n" + "/* name */\n" + "/* name */\n" + "th:first-child {\n" + " padding-inline-end: 2em;\n" + "}\n" + "/* size */\n" + "th:first-child + th {\n" + " padding-inline-end: 1em;\n" + "}\n" + "td:first-child + td {\n" + " text-align: end;\n" + " padding-inline-end: 1em;\n" + "}\n" + "/* date */\n" + "td:first-child + td + td {\n" + " padding-inline-start: 1em;\n" + " padding-inline-end: .5em;\n" + "}\n" + "/* time */\n" + "td:first-child + td + td + td {\n" + " padding-inline-start: .5em;\n" + "}\n" + ".symlink {\n" + " font-style: italic;\n" + "}\n" + ".dir ,\n" + ".symlink ,\n" + ".file {\n" + " margin-inline-start: 20px;\n" + "}\n" + ".dir::before ,\n" + ".file > img {\n" + " margin-inline-end: 4px;\n" + " margin-inline-start: -20px;\n" + " max-width: 16px;\n" + " max-height: 16px;\n" + " vertical-align: middle;\n" + "}\n" + ".dir::before {\n" + " content: url(resource://content-accessible/html/folder.png);\n" + "}\n" + "</style>\n" + "<link rel=\"stylesheet\" media=\"screen, projection\" type=\"text/css\"" + " href=\"chrome://global/skin/dirListing/dirListing.css\">\n" + "<script type=\"application/javascript\">\n" + "'use strict';\n" + "var gTable, gOrderBy, gTBody, gRows, gUI_showHidden;\n" + "document.addEventListener(\"DOMContentLoaded\", function() {\n" + " gTable = document.getElementsByTagName(\"table\")[0];\n" + " gTBody = gTable.tBodies[0];\n" + " if (gTBody.rows.length < 2)\n" + " return;\n" + " gUI_showHidden = document.getElementById(\"UI_showHidden\");\n" + " var headCells = gTable.tHead.rows[0].cells,\n" + " hiddenObjects = false;\n" + " function rowAction(i) {\n" + " return function(event) {\n" + " event.preventDefault();\n" + " orderBy(i);\n" + " }\n" + " }\n" + " for (var i = headCells.length - 1; i >= 0; i--) {\n" + " var anchor = document.createElement(\"a\");\n" + " anchor.href = \"\";\n" + " anchor.appendChild(headCells[i].firstChild);\n" + " headCells[i].appendChild(anchor);\n" + " headCells[i].addEventListener(\"click\", rowAction(i), true);\n" + " }\n" + " if (gUI_showHidden) {\n" + " gRows = Array.from(gTBody.rows);\n" + " hiddenObjects = gRows.some(row => row.className == " + "\"hidden-object\");\n" + " }\n" + " gTable.setAttribute(\"order\", \"\");\n" + " if (hiddenObjects) {\n" + " gUI_showHidden.style.display = \"block\";\n" + " updateHidden();\n" + " }\n" + "}, \"false\");\n" + "function compareRows(rowA, rowB) {\n" + " var a = rowA.cells[gOrderBy].getAttribute(\"sortable-data\") || " + "\"\";\n" + " var b = rowB.cells[gOrderBy].getAttribute(\"sortable-data\") || " + "\"\";\n" + " var intA = +a;\n" + " var intB = +b;\n" + " if (a == intA && b == intB) {\n" + " a = intA;\n" + " b = intB;\n" + " } else {\n" + " a = a.toLowerCase();\n" + " b = b.toLowerCase();\n" + " }\n" + " if (a < b)\n" + " return -1;\n" + " if (a > b)\n" + " return 1;\n" + " return 0;\n" + "}\n" + "function orderBy(column) {\n" + " if (!gRows)\n" + " gRows = Array.from(gTBody.rows);\n" + " var order;\n" + " if (gOrderBy == column) {\n" + " order = gTable.getAttribute(\"order\") == \"asc\" ? \"desc\" : " + "\"asc\";\n" + " } else {\n" + " order = \"asc\";\n" + " gOrderBy = column;\n" + " gTable.setAttribute(\"order-by\", column);\n" + " gRows.sort(compareRows);\n" + " }\n" + " gTable.removeChild(gTBody);\n" + " gTable.setAttribute(\"order\", order);\n" + " if (order == \"asc\")\n" + " for (var i = 0; i < gRows.length; i++)\n" + " gTBody.appendChild(gRows[i]);\n" + " else\n" + " for (var i = gRows.length - 1; i >= 0; i--)\n" + " gTBody.appendChild(gRows[i]);\n" + " gTable.appendChild(gTBody);\n" + "}\n" + "function updateHidden() {\n" + " gTable.className = " + "gUI_showHidden.getElementsByTagName(\"input\")[0].checked ?\n" + " \"\" :\n" + " \"remove-hidden\";\n" + "}\n" + "</script>\n"); + + buffer.AppendLiteral(R"(<link rel="icon" type="image/png" href=")"); + nsCOMPtr<nsIURI> innerUri = NS_GetInnermostURI(uri); + if (!innerUri) return NS_ERROR_UNEXPECTED; + nsCOMPtr<nsIFileURL> fileURL(do_QueryInterface(innerUri)); + // XXX bug 388553: can't use skinnable icons here due to security restrictions + if (fileURL) { + buffer.AppendLiteral( + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB" + "AAAAAQCAYAAAAf8%2F9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9i" + "ZSBJbWFnZVJlYWR5ccllPAAAAjFJREFUeNqsU8uOElEQPffR" + "3XQ3ONASdBJCSBxHos5%2B3Bg3rvkCv8PElS78gPkO%2FATj" + "QoUdO2ftrJiRh6aneTb9sOpC4weMN6lcuFV16pxDIfI8x12O" + "YIDhcPiu2Wx%2B%2FHF5CW1Z6Jyegt%2FTNEWSJIjjGFEUIQ" + "xDrFYrWFSzXC4%2FdLvd95pRKpXKy%2BpRFZ7nwaWo1%2BsG" + "nQG2260BKJfLKJVKGI1GEEJw7ateryd0v993W63WEwjgxfn5" + "obGYzgCbzcaEbdsIggDj8Riu6z6iUk9SYZMSx8W0LMsM%2FS" + "KK75xnJlIq80anQXdbEp0OhcPJ0eiaJnGRMEyyPDsAKKUM9c" + "lkYoDo3SZJzzSdp0VSKYmfV1co%2Bz580kw5KDIM8RbRfEnU" + "f1HzxtQyMAGcaGruTKczMzEIaqhKifV6jd%2BzGQQB5llunF" + "%2FM52BizC2K5sYPYvZcu653tjOM9O93wnYc08gmkgg4VAxi" + "xfqFUJT36AYBZGd6PJkFCZnnlBxMp38gqIgLpZB0y4Nph18l" + "yWh5FFbrOSxbl3V4G%2BVB7T4ajYYxTyuLtO%2BCvWGgJE1M" + "c7JNsJEhvgw%2FQV4fo%2F24nbEsX2u1d5sVyn8sJO0ZAQiI" + "YnFh%2BxrfLz%2Fj29cBS%2FO14zg3i8XigW3ZkErDtmKoeM" + "%2BAJGRMnXeEPGKf0nCD1ydvkDzU9Jbc6OpR7WIw6L8lQ%2B" + "4pQ1%2FlPF0RGM9Ns91Wmptk0GfB4EJkt77vXYj%2F8m%2B8" + "y%2FkrwABHbz2H9V68DQAAAABJRU5ErkJggg%3D%3D"); + } else { + buffer.AppendLiteral( + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB" + "AAAAAQCAYAAAAf8%2F9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9i" + "ZSBJbWFnZVJlYWR5ccllPAAAAeBJREFUeNqcU81O20AQ%2Ft" + "Z2AgQSYQRqL1UPVG2hAUQkxLEStz4DrXpLpD5Drz31Cajax%" + "2Bghhx6qHIJURBTxIwQRwopCBbZjHMcOTrzermPipsSt1Iw0" + "3p3ZmW%2B%2B2R0TxhgOD34wjCHZlQ0iDYz9yvEfhxMTCYhE" + "QDIZhkxKd2sqzX2TOD2vBQCQhpPefng1ZP2dVPlLLdpL8SEM" + "cxng%2Fbs0RIHhtgs4twxOh%2BHjZxvzDx%2F3GQQiDFISiR" + "BLFMPKTRMollzcWECrDVhtxtdRVsL9youPxGj%2FbdfFlUZh" + "tDyYbYqWRUdai1oQRZ5oHeHl2gNM%2B01Uqio8RlH%2Bnsaz" + "JzNwXcq1B%2BiXPHprlEEymeBfXs1w8XxxihfyuXqoHqpoGj" + "ZM04bddgG%2F9%2B8WGj87qDdsrK9m%2BoA%2BpbhQTDh2l1" + "%2Bi2weNbSHMZyjvNXmVbqh9Fj5Oz27uEoP%2BSTxANruJs9" + "L%2FT6P0ewqPx5nmiAG5f6AoCtN1PbJzuRyJAyDBzzSQYvEr" + "f06yYxhGXlEa8H2KVGoasjwLx3Ewk858opQWXm%2B%2Fib9E" + "QrBzclLLLy89xYvlpchvtixcX6uo1y%2FzsiwHrkIsgKbp%2" + "BYWFOWicuqppoNTnStHzPFCPQhBEBOyGAX4JMADFetubi4BS" + "YAAAAABJRU5ErkJggg%3D%3D"); + } + buffer.AppendLiteral("\">\n<title>"); + + // Everything needs to end in a /, + // otherwise we end up linking to file:///foo/dirfile + + if (!mTextToSubURI) { + mTextToSubURI = do_GetService(NS_ITEXTTOSUBURI_CONTRACTID, &rv); + if (NS_FAILED(rv)) return rv; + } + + nsAutoString unEscapeSpec; + rv = mTextToSubURI->UnEscapeAndConvert("UTF-8"_ns, titleUri, unEscapeSpec); + if (NS_FAILED(rv)) { + return rv; + } + + nsCString htmlEscSpecUtf8; + nsAppendEscapedHTML(NS_ConvertUTF16toUTF8(unEscapeSpec), htmlEscSpecUtf8); + AutoTArray<nsString, 1> formatTitle; + CopyUTF8toUTF16(htmlEscSpecUtf8, *formatTitle.AppendElement()); + + nsAutoString title; + rv = mBundle->FormatStringFromName("DirTitle", formatTitle, title); + if (NS_FAILED(rv)) return rv; + + // we want to convert string bundle to NCR + // to ensure they're shown in any charsets + AppendNonAsciiToNCR(title, buffer); + + buffer.AppendLiteral("</title>\n"); + + // If there is a quote character in the baseUri, then + // lets not add a base URL. The reason for this is that + // if we stick baseUri containing a quote into a quoted + // string, the quote character will prematurely close + // the base href string. This is a fall-back check; + // that's why it is OK to not use a base rather than + // trying to play nice and escaping the quotes. See bug + // 358128. + + if (!baseUri.Contains('"')) { + // Great, the baseUri does not contain a char that + // will prematurely close the string. Go ahead an + // add a base href, but only do so if we're not + // dealing with a resource URI. + if (!uri->SchemeIs("resource")) { + buffer.AppendLiteral("<base href=\""); + nsAppendEscapedHTML(baseUri, buffer); + buffer.AppendLiteral("\" />\n"); + } + } else { + NS_ERROR("broken protocol handler didn't escape double-quote."); + } + + nsCString direction("ltr"_ns); + if (LocaleService::GetInstance()->IsAppLocaleRTL()) { + direction.AssignLiteral("rtl"); + } + + buffer.AppendLiteral("</head>\n<body dir=\""); + buffer.Append(direction); + buffer.AppendLiteral("\">\n<h1>"); + AppendNonAsciiToNCR(title, buffer); + buffer.AppendLiteral("</h1>\n"); + + if (!parentStr.IsEmpty()) { + nsAutoString parentText; + rv = mBundle->GetStringFromName("DirGoUp", parentText); + if (NS_FAILED(rv)) return rv; + + buffer.AppendLiteral(R"(<p id="UI_goUp"><a class="up" href=")"); + nsAppendEscapedHTML(parentStr, buffer); + buffer.AppendLiteral("\">"); + AppendNonAsciiToNCR(parentText, buffer); + buffer.AppendLiteral("</a></p>\n"); + } + + if (uri->SchemeIs("file")) { + nsAutoString showHiddenText; + rv = mBundle->GetStringFromName("ShowHidden", showHiddenText); + if (NS_FAILED(rv)) return rv; + + buffer.AppendLiteral( + "<p id=\"UI_showHidden\" style=\"display:none\"><label><input " + "type=\"checkbox\" checked onchange=\"updateHidden()\">"); + AppendNonAsciiToNCR(showHiddenText, buffer); + buffer.AppendLiteral("</label></p>\n"); + } + + buffer.AppendLiteral( + "<table>\n" + " <thead>\n" + " <tr>\n" + " <th>"); + + nsAutoString columnText; + rv = mBundle->GetStringFromName("DirColName", columnText); + if (NS_FAILED(rv)) return rv; + AppendNonAsciiToNCR(columnText, buffer); + buffer.AppendLiteral( + "</th>\n" + " <th>"); + + rv = mBundle->GetStringFromName("DirColSize", columnText); + if (NS_FAILED(rv)) return rv; + AppendNonAsciiToNCR(columnText, buffer); + buffer.AppendLiteral( + "</th>\n" + " <th colspan=\"2\">"); + + rv = mBundle->GetStringFromName("DirColMTime", columnText); + if (NS_FAILED(rv)) return rv; + AppendNonAsciiToNCR(columnText, buffer); + buffer.AppendLiteral( + "</th>\n" + " </tr>\n" + " </thead>\n"); + buffer.AppendLiteral(" <tbody>\n"); + + aBuffer = buffer; + return rv; +} + +NS_IMETHODIMP +nsIndexedToHTML::OnStopRequest(nsIRequest* request, nsresult aStatus) { + if (NS_SUCCEEDED(aStatus)) { + nsCString buffer; + buffer.AssignLiteral("</tbody></table></body></html>\n"); + + aStatus = SendToListener(request, buffer); + } + + mParser->OnStopRequest(request, aStatus); + mParser = nullptr; + + return mListener->OnStopRequest(request, aStatus); +} + +nsresult nsIndexedToHTML::SendToListener(nsIRequest* aRequest, + const nsACString& aBuffer) { + nsCOMPtr<nsIInputStream> inputData; + nsresult rv = NS_NewCStringInputStream(getter_AddRefs(inputData), aBuffer); + NS_ENSURE_SUCCESS(rv, rv); + return mListener->OnDataAvailable(aRequest, inputData, 0, aBuffer.Length()); +} + +NS_IMETHODIMP +nsIndexedToHTML::OnDataAvailable(nsIRequest* aRequest, nsIInputStream* aInput, + uint64_t aOffset, uint32_t aCount) { + return mParser->OnDataAvailable(aRequest, aInput, aOffset, aCount); +} + +NS_IMETHODIMP +nsIndexedToHTML::OnDataFinished(nsresult aStatus) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsIndexedToHTML::CheckListenerChain() { + // nsIndexedToHTML does not support OnDataAvailable to run OMT. This class + // should only pass-through OnDataFinished notification. + return NS_ERROR_NO_INTERFACE; +} + +static nsresult FormatTime( + const mozilla::intl::DateTimeFormat::StyleBag& aStyleBag, + const PRTime aPrTime, nsAString& aStringOut) { + // FormatPRExplodedTime will use GMT based formatted string (e.g. GMT+1) + // instead of local time zone name (e.g. CEST). + // To avoid this case when ResistFingerprinting is disabled, use + // |FormatPRTime| to show exact time zone name. + if (!nsContentUtils::ShouldResistFingerprinting(true, + RFPTarget::JSDateTimeUTC)) { + return mozilla::intl::AppDateTimeFormat::Format(aStyleBag, aPrTime, + aStringOut); + } + + PRExplodedTime prExplodedTime; + PR_ExplodeTime(aPrTime, PR_GMTParameters, &prExplodedTime); + return mozilla::intl::AppDateTimeFormat::Format(aStyleBag, &prExplodedTime, + aStringOut); +} + +NS_IMETHODIMP +nsIndexedToHTML::OnIndexAvailable(nsIRequest* aRequest, nsIDirIndex* aIndex) { + nsresult rv; + if (!aIndex) return NS_ERROR_NULL_POINTER; + + nsCString pushBuffer; + pushBuffer.AppendLiteral("<tr"); + + // We don't know the file's character set yet, so retrieve the raw bytes + // which will be decoded by the HTML parser. + nsCString loc; + aIndex->GetLocation(loc); + + // Adjust the length in case unescaping shortened the string. + loc.Truncate(nsUnescapeCount(loc.BeginWriting())); + + if (loc.IsEmpty()) { + return NS_ERROR_ILLEGAL_VALUE; + } + if (loc.First() == char16_t('.')) { + pushBuffer.AppendLiteral(" class=\"hidden-object\""); + } + + pushBuffer.AppendLiteral(">\n <td sortable-data=\""); + + // The sort key is the name of the item, prepended by either 0, 1 or 2 + // in order to group items. + uint32_t type; + aIndex->GetType(&type); + switch (type) { + case nsIDirIndex::TYPE_SYMLINK: + pushBuffer.Append('0'); + break; + case nsIDirIndex::TYPE_DIRECTORY: + pushBuffer.Append('1'); + break; + default: + pushBuffer.Append('2'); + break; + } + nsCString escaped; + nsAppendEscapedHTML(loc, escaped); + pushBuffer.Append(escaped); + + pushBuffer.AppendLiteral( + R"("><table class="ellipsis"><tbody><tr><td><a class=")"); + switch (type) { + case nsIDirIndex::TYPE_DIRECTORY: + pushBuffer.AppendLiteral("dir"); + break; + case nsIDirIndex::TYPE_SYMLINK: + pushBuffer.AppendLiteral("symlink"); + break; + default: + pushBuffer.AppendLiteral("file"); + break; + } + + pushBuffer.AppendLiteral("\" href=\""); + + // need to escape links + nsAutoCString locEscaped; + + // Adding trailing slash helps to recognize whether the URL points to a file + // or a directory (bug #214405). + if ((type == nsIDirIndex::TYPE_DIRECTORY) && (loc.Last() != '/')) { + loc.Append('/'); + } + + // now minimally re-escape the location... + uint32_t escFlags; + // for some protocols, we expect the location to be absolute. + // if so, and if the location indeed appears to be a valid URI, then go + // ahead and treat it like one. + + nsAutoCString scheme; + if (mExpectAbsLoc && NS_SUCCEEDED(net_ExtractURLScheme(loc, scheme))) { + // escape as absolute + escFlags = esc_Forced | esc_AlwaysCopy | esc_Minimal; + } else { + // escape as relative + // esc_Directory is needed because directories have a trailing slash. + // Without it, the trailing '/' will be escaped, and links from within + // that directory will be incorrect + escFlags = esc_Forced | esc_AlwaysCopy | esc_FileBaseName | esc_Colon | + esc_Directory; + } + NS_EscapeURL(loc.get(), loc.Length(), escFlags, locEscaped); + // esc_Directory does not escape the semicolons, so if a filename + // contains semicolons we need to manually escape them. + // This replacement should be removed in bug #473280 + locEscaped.ReplaceSubstring(";", "%3b"); + nsAppendEscapedHTML(locEscaped, pushBuffer); + pushBuffer.AppendLiteral("\">"); + + if (type == nsIDirIndex::TYPE_FILE || type == nsIDirIndex::TYPE_UNKNOWN) { + pushBuffer.AppendLiteral("<img src=\"moz-icon://"); + int32_t lastDot = locEscaped.RFindChar('.'); + if (lastDot != kNotFound) { + locEscaped.Cut(0, lastDot); + nsAppendEscapedHTML(locEscaped, pushBuffer); + } else { + pushBuffer.AppendLiteral("unknown"); + } + pushBuffer.AppendLiteral("?size=16\" alt=\""); + + nsAutoString altText; + rv = mBundle->GetStringFromName("DirFileLabel", altText); + if (NS_FAILED(rv)) return rv; + AppendNonAsciiToNCR(altText, pushBuffer); + pushBuffer.AppendLiteral("\">"); + } + + pushBuffer.Append(escaped); + pushBuffer.AppendLiteral("</a></td></tr></tbody></table></td>\n <td"); + + if (type == nsIDirIndex::TYPE_DIRECTORY || + type == nsIDirIndex::TYPE_SYMLINK) { + pushBuffer.Append('>'); + } else { + int64_t size; + aIndex->GetSize(&size); + + if (uint64_t(size) != UINT64_MAX) { + pushBuffer.AppendLiteral(" sortable-data=\""); + pushBuffer.AppendInt(size); + pushBuffer.AppendLiteral("\">"); + nsAutoCString sizeString; + FormatSizeString(size, sizeString); + pushBuffer.Append(sizeString); + } else { + pushBuffer.Append('>'); + } + } + pushBuffer.AppendLiteral("</td>\n <td"); + + PRTime t; + aIndex->GetLastModified(&t); + + if (t == -1LL) { + pushBuffer.AppendLiteral("></td>\n <td>"); + } else { + pushBuffer.AppendLiteral(" sortable-data=\""); + pushBuffer.AppendInt(static_cast<int64_t>(t)); + pushBuffer.AppendLiteral("\">"); + // Add date string + nsAutoString formatted; + mozilla::intl::DateTimeFormat::StyleBag dateBag; + dateBag.date = Some(mozilla::intl::DateTimeFormat::Style::Short); + FormatTime(dateBag, t, formatted); + AppendNonAsciiToNCR(formatted, pushBuffer); + pushBuffer.AppendLiteral("</td>\n <td>"); + // Add time string + mozilla::intl::DateTimeFormat::StyleBag timeBag; + timeBag.time = Some(mozilla::intl::DateTimeFormat::Style::Long); + FormatTime(timeBag, t, formatted); + // use NCR to show date in any doc charset + AppendNonAsciiToNCR(formatted, pushBuffer); + } + + pushBuffer.AppendLiteral("</td>\n</tr>"); + + return SendToListener(aRequest, pushBuffer); +} + +void nsIndexedToHTML::FormatSizeString(int64_t inSize, + nsCString& outSizeString) { + outSizeString.Truncate(); + if (inSize > int64_t(0)) { + // round up to the nearest Kilobyte + int64_t upperSize = (inSize + int64_t(1023)) / int64_t(1024); + outSizeString.AppendInt(upperSize); + outSizeString.AppendLiteral(" KB"); + } +} diff --git a/netwerk/streamconv/converters/nsIndexedToHTML.h b/netwerk/streamconv/converters/nsIndexedToHTML.h new file mode 100644 index 0000000000..736df3ebf7 --- /dev/null +++ b/netwerk/streamconv/converters/nsIndexedToHTML.h @@ -0,0 +1,61 @@ +/* -*- 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/. */ + +#ifndef ____nsindexedtohtml___h___ +#define ____nsindexedtohtml___h___ + +#include "nsCOMPtr.h" +#include "nsIThreadRetargetableStreamListener.h" +#include "nsString.h" +#include "nsIStreamConverter.h" +#include "nsIDirIndexListener.h" + +#define NS_NSINDEXEDTOHTMLCONVERTER_CID \ + { \ + 0xcf0f71fd, 0xfafd, 0x4e2b, { \ + 0x9f, 0xdc, 0x13, 0x4d, 0x97, 0x2e, 0x16, 0xe2 \ + } \ + } + +class nsIStringBundle; +class nsITextToSubURI; + +class nsIndexedToHTML : public nsIStreamConverter, public nsIDirIndexListener { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSISTREAMCONVERTER + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSITHREADRETARGETABLESTREAMLISTENER + NS_DECL_NSIDIRINDEXLISTENER + + nsIndexedToHTML() = default; + + nsresult Init(nsIStreamListener* aListener); + + static nsresult Create(REFNSIID aIID, void** aResult); + + protected: + void FormatSizeString(int64_t inSize, nsCString& outSizeString); + nsresult SendToListener(nsIRequest* aRequest, const nsACString& aBuffer); + // Helper to properly implement OnStartRequest + nsresult DoOnStartRequest(nsIRequest* request, nsCString& aBuffer); + + protected: + nsCOMPtr<nsIDirIndexParser> mParser; + nsCOMPtr<nsIStreamListener> mListener; // final listener (consumer) + + nsCOMPtr<nsIStringBundle> mBundle; + + nsCOMPtr<nsITextToSubURI> mTextToSubURI; + + private: + // Expecting absolute locations, given by 201 lines. + bool mExpectAbsLoc{false}; + + virtual ~nsIndexedToHTML() = default; +}; + +#endif diff --git a/netwerk/streamconv/converters/nsMultiMixedConv.cpp b/netwerk/streamconv/converters/nsMultiMixedConv.cpp new file mode 100644 index 0000000000..fb21d6673f --- /dev/null +++ b/netwerk/streamconv/converters/nsMultiMixedConv.cpp @@ -0,0 +1,1051 @@ +/* -*- 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 "nsMultiMixedConv.h" +#include "nsIHttpChannel.h" +#include "nsIThreadRetargetableStreamListener.h" +#include "nsNetCID.h" +#include "nsMimeTypes.h" +#include "nsIStringStream.h" +#include "nsCRT.h" +#include "nsIHttpChannelInternal.h" +#include "nsURLHelper.h" +#include "nsIStreamConverterService.h" +#include <algorithm> +#include "nsContentSecurityManager.h" +#include "nsHttp.h" +#include "nsNetUtil.h" +#include "nsIURI.h" +#include "nsHttpHeaderArray.h" +#include "mozilla/AutoRestore.h" +#include "mozilla/Tokenizer.h" +#include "nsComponentManagerUtils.h" +#include "mozilla/StaticPrefs_network.h" + +using namespace mozilla; + +nsPartChannel::nsPartChannel(nsIChannel* aMultipartChannel, uint32_t aPartID, + bool aIsFirstPart, nsIStreamListener* aListener) + : mMultipartChannel(aMultipartChannel), + mListener(aListener), + mPartID(aPartID), + mIsFirstPart(aIsFirstPart) { + // Inherit the load flags from the original channel... + mMultipartChannel->GetLoadFlags(&mLoadFlags); + + mMultipartChannel->GetLoadGroup(getter_AddRefs(mLoadGroup)); +} + +void nsPartChannel::InitializeByteRange(int64_t aStart, int64_t aEnd) { + mIsByteRangeRequest = true; + + mByteRangeStart = aStart; + mByteRangeEnd = aEnd; +} + +nsresult nsPartChannel::SendOnStartRequest(nsISupports* aContext) { + return mListener->OnStartRequest(this); +} + +nsresult nsPartChannel::SendOnDataAvailable(nsISupports* aContext, + nsIInputStream* aStream, + uint64_t aOffset, uint32_t aLen) { + return mListener->OnDataAvailable(this, aStream, aOffset, aLen); +} + +nsresult nsPartChannel::SendOnStopRequest(nsISupports* aContext, + nsresult aStatus) { + // Drop the listener + nsCOMPtr<nsIStreamListener> listener; + listener.swap(mListener); + return listener->OnStopRequest(this, aStatus); +} + +void nsPartChannel::SetContentDisposition( + const nsACString& aContentDispositionHeader) { + mContentDispositionHeader = aContentDispositionHeader; + nsCOMPtr<nsIURI> uri; + GetURI(getter_AddRefs(uri)); + NS_GetFilenameFromDisposition(mContentDispositionFilename, + mContentDispositionHeader); + mContentDisposition = + NS_GetContentDispositionFromHeader(mContentDispositionHeader, this); +} + +// +// nsISupports implementation... +// + +NS_IMPL_ADDREF(nsPartChannel) +NS_IMPL_RELEASE(nsPartChannel) + +NS_INTERFACE_MAP_BEGIN(nsPartChannel) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIChannel) + NS_INTERFACE_MAP_ENTRY(nsIRequest) + NS_INTERFACE_MAP_ENTRY(nsIChannel) + NS_INTERFACE_MAP_ENTRY(nsIByteRangeRequest) + NS_INTERFACE_MAP_ENTRY(nsIMultiPartChannel) +NS_INTERFACE_MAP_END + +// +// nsIRequest implementation... +// + +NS_IMETHODIMP +nsPartChannel::GetName(nsACString& aResult) { + return mMultipartChannel->GetName(aResult); +} + +NS_IMETHODIMP +nsPartChannel::IsPending(bool* aResult) { + // For now, consider the active lifetime of each part the same as + // the underlying multipart channel... This is not exactly right, + // but it is good enough :-) + return mMultipartChannel->IsPending(aResult); +} + +NS_IMETHODIMP +nsPartChannel::GetStatus(nsresult* aResult) { + nsresult rv = NS_OK; + + if (NS_FAILED(mStatus)) { + *aResult = mStatus; + } else { + rv = mMultipartChannel->GetStatus(aResult); + } + + return rv; +} + +NS_IMETHODIMP nsPartChannel::SetCanceledReason(const nsACString& aReason) { + return SetCanceledReasonImpl(aReason); +} + +NS_IMETHODIMP nsPartChannel::GetCanceledReason(nsACString& aReason) { + return GetCanceledReasonImpl(aReason); +} + +NS_IMETHODIMP nsPartChannel::CancelWithReason(nsresult aStatus, + const nsACString& aReason) { + return CancelWithReasonImpl(aStatus, aReason); +} + +NS_IMETHODIMP +nsPartChannel::Cancel(nsresult aStatus) { + // Cancelling an individual part must not cancel the underlying + // multipart channel... + // XXX but we should stop sending data for _this_ part channel! + mStatus = aStatus; + return NS_OK; +} + +NS_IMETHODIMP +nsPartChannel::GetCanceled(bool* aCanceled) { + *aCanceled = NS_FAILED(mStatus); + return NS_OK; +} + +NS_IMETHODIMP +nsPartChannel::Suspend(void) { + // Suspending an individual part must not suspend the underlying + // multipart channel... + // XXX why not? + return NS_OK; +} + +NS_IMETHODIMP +nsPartChannel::Resume(void) { + // Resuming an individual part must not resume the underlying + // multipart channel... + // XXX why not? + return NS_OK; +} + +// +// nsIChannel implementation +// + +NS_IMETHODIMP +nsPartChannel::GetOriginalURI(nsIURI** aURI) { + return mMultipartChannel->GetOriginalURI(aURI); +} + +NS_IMETHODIMP +nsPartChannel::SetOriginalURI(nsIURI* aURI) { + return mMultipartChannel->SetOriginalURI(aURI); +} + +NS_IMETHODIMP +nsPartChannel::GetURI(nsIURI** aURI) { return mMultipartChannel->GetURI(aURI); } + +NS_IMETHODIMP +nsPartChannel::Open(nsIInputStream** aStream) { + nsCOMPtr<nsIStreamListener> listener; + nsresult rv = + nsContentSecurityManager::doContentSecurityCheck(this, listener); + NS_ENSURE_SUCCESS(rv, rv); + + // This channel cannot be opened! + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsPartChannel::AsyncOpen(nsIStreamListener* aListener) { + nsCOMPtr<nsIStreamListener> listener = aListener; + nsresult rv = + nsContentSecurityManager::doContentSecurityCheck(this, listener); + NS_ENSURE_SUCCESS(rv, rv); + + // This channel cannot be opened! + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsPartChannel::GetLoadFlags(nsLoadFlags* aLoadFlags) { + *aLoadFlags = mLoadFlags; + return NS_OK; +} + +NS_IMETHODIMP +nsPartChannel::SetLoadFlags(nsLoadFlags aLoadFlags) { + mLoadFlags = aLoadFlags; + return NS_OK; +} + +NS_IMETHODIMP +nsPartChannel::GetTRRMode(nsIRequest::TRRMode* aTRRMode) { + return GetTRRModeImpl(aTRRMode); +} + +NS_IMETHODIMP +nsPartChannel::SetTRRMode(nsIRequest::TRRMode aTRRMode) { + return SetTRRModeImpl(aTRRMode); +} + +NS_IMETHODIMP +nsPartChannel::GetIsDocument(bool* aIsDocument) { + return NS_GetIsDocumentChannel(this, aIsDocument); +} + +NS_IMETHODIMP +nsPartChannel::GetLoadGroup(nsILoadGroup** aLoadGroup) { + *aLoadGroup = do_AddRef(mLoadGroup).take(); + return NS_OK; +} + +NS_IMETHODIMP +nsPartChannel::SetLoadGroup(nsILoadGroup* aLoadGroup) { + mLoadGroup = aLoadGroup; + + return NS_OK; +} + +NS_IMETHODIMP +nsPartChannel::GetOwner(nsISupports** aOwner) { + return mMultipartChannel->GetOwner(aOwner); +} + +NS_IMETHODIMP +nsPartChannel::SetOwner(nsISupports* aOwner) { + return mMultipartChannel->SetOwner(aOwner); +} + +NS_IMETHODIMP +nsPartChannel::GetLoadInfo(nsILoadInfo** aLoadInfo) { + return mMultipartChannel->GetLoadInfo(aLoadInfo); +} + +NS_IMETHODIMP +nsPartChannel::SetLoadInfo(nsILoadInfo* aLoadInfo) { + MOZ_RELEASE_ASSERT(aLoadInfo, "loadinfo can't be null"); + return mMultipartChannel->SetLoadInfo(aLoadInfo); +} + +NS_IMETHODIMP +nsPartChannel::GetNotificationCallbacks(nsIInterfaceRequestor** aCallbacks) { + return mMultipartChannel->GetNotificationCallbacks(aCallbacks); +} + +NS_IMETHODIMP +nsPartChannel::SetNotificationCallbacks(nsIInterfaceRequestor* aCallbacks) { + return mMultipartChannel->SetNotificationCallbacks(aCallbacks); +} + +NS_IMETHODIMP +nsPartChannel::GetSecurityInfo(nsITransportSecurityInfo** aSecurityInfo) { + return mMultipartChannel->GetSecurityInfo(aSecurityInfo); +} + +NS_IMETHODIMP +nsPartChannel::GetContentType(nsACString& aContentType) { + aContentType = mContentType; + return NS_OK; +} + +NS_IMETHODIMP +nsPartChannel::SetContentType(const nsACString& aContentType) { + bool dummy; + net_ParseContentType(aContentType, mContentType, mContentCharset, &dummy); + return NS_OK; +} + +NS_IMETHODIMP +nsPartChannel::GetContentCharset(nsACString& aContentCharset) { + aContentCharset = mContentCharset; + return NS_OK; +} + +NS_IMETHODIMP +nsPartChannel::SetContentCharset(const nsACString& aContentCharset) { + mContentCharset = aContentCharset; + return NS_OK; +} + +NS_IMETHODIMP +nsPartChannel::GetContentLength(int64_t* aContentLength) { + *aContentLength = mContentLength; + return NS_OK; +} + +NS_IMETHODIMP +nsPartChannel::SetContentLength(int64_t aContentLength) { + mContentLength = aContentLength; + return NS_OK; +} + +NS_IMETHODIMP +nsPartChannel::GetContentDisposition(uint32_t* aContentDisposition) { + if (mContentDispositionHeader.IsEmpty()) return NS_ERROR_NOT_AVAILABLE; + + *aContentDisposition = mContentDisposition; + return NS_OK; +} + +NS_IMETHODIMP +nsPartChannel::SetContentDisposition(uint32_t aContentDisposition) { + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +nsPartChannel::GetContentDispositionFilename( + nsAString& aContentDispositionFilename) { + if (mContentDispositionFilename.IsEmpty()) return NS_ERROR_NOT_AVAILABLE; + + aContentDispositionFilename = mContentDispositionFilename; + return NS_OK; +} + +NS_IMETHODIMP +nsPartChannel::SetContentDispositionFilename( + const nsAString& aContentDispositionFilename) { + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +nsPartChannel::GetContentDispositionHeader( + nsACString& aContentDispositionHeader) { + if (mContentDispositionHeader.IsEmpty()) return NS_ERROR_NOT_AVAILABLE; + + aContentDispositionHeader = mContentDispositionHeader; + return NS_OK; +} + +NS_IMETHODIMP +nsPartChannel::GetPartID(uint32_t* aPartID) { + *aPartID = mPartID; + return NS_OK; +} + +NS_IMETHODIMP +nsPartChannel::GetIsFirstPart(bool* aIsFirstPart) { + *aIsFirstPart = mIsFirstPart; + return NS_OK; +} + +NS_IMETHODIMP +nsPartChannel::GetIsLastPart(bool* aIsLastPart) { + *aIsLastPart = mIsLastPart; + return NS_OK; +} + +// +// nsIByteRangeRequest implementation... +// + +NS_IMETHODIMP +nsPartChannel::GetIsByteRangeRequest(bool* aIsByteRangeRequest) { + *aIsByteRangeRequest = mIsByteRangeRequest; + + return NS_OK; +} + +NS_IMETHODIMP +nsPartChannel::GetStartRange(int64_t* aStartRange) { + *aStartRange = mByteRangeStart; + + return NS_OK; +} + +NS_IMETHODIMP +nsPartChannel::GetEndRange(int64_t* aEndRange) { + *aEndRange = mByteRangeEnd; + return NS_OK; +} + +NS_IMETHODIMP +nsPartChannel::GetBaseChannel(nsIChannel** aReturn) { + NS_ENSURE_ARG_POINTER(aReturn); + + *aReturn = do_AddRef(mMultipartChannel).take(); + return NS_OK; +} + +// nsISupports implementation +NS_IMPL_ISUPPORTS(nsMultiMixedConv, nsIStreamConverter, nsIStreamListener, + nsIThreadRetargetableStreamListener, nsIRequestObserver) + +// nsIStreamConverter implementation + +// No syncronous conversion at this time. +NS_IMETHODIMP +nsMultiMixedConv::Convert(nsIInputStream* aFromStream, const char* aFromType, + const char* aToType, nsISupports* aCtxt, + nsIInputStream** _retval) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +// Stream converter service calls this to initialize the actual stream converter +// (us). +NS_IMETHODIMP +nsMultiMixedConv::AsyncConvertData(const char* aFromType, const char* aToType, + nsIStreamListener* aListener, + nsISupports* aCtxt) { + NS_ASSERTION(aListener && aFromType && aToType, + "null pointer passed into multi mixed converter"); + + // hook up our final listener. this guy gets the various On*() calls we want + // to throw at him. + // + // WARNING: this listener must be able to handle multiple OnStartRequest, + // OnDataAvail() and OnStopRequest() call combinations. We call of series + // of these for each sub-part in the raw stream. + mFinalListener = aListener; + + return NS_OK; +} + +NS_IMETHODIMP +nsMultiMixedConv::GetConvertedType(const nsACString& aFromType, + nsIChannel* aChannel, nsACString& aToType) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +// nsIRequestObserver implementation +NS_IMETHODIMP +nsMultiMixedConv::OnStartRequest(nsIRequest* request) { + // we're assuming the content-type is available at this stage + NS_ASSERTION(mBoundary.IsEmpty(), "a second on start???"); + + nsresult rv; + + mTotalSent = 0; + mChannel = do_QueryInterface(request, &rv); + if (NS_FAILED(rv)) return rv; + + nsAutoCString contentType; + + // ask the HTTP channel for the content-type and extract the boundary from it. + nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(mChannel, &rv); + if (NS_SUCCEEDED(rv)) { + rv = httpChannel->GetResponseHeader("content-type"_ns, contentType); + if (NS_FAILED(rv)) { + return rv; + } + nsCString csp; + rv = httpChannel->GetResponseHeader("content-security-policy"_ns, csp); + if (NS_SUCCEEDED(rv)) { + mRootContentSecurityPolicy = csp; + } + } else { + // try asking the channel directly + rv = mChannel->GetContentType(contentType); + if (NS_FAILED(rv)) { + return NS_ERROR_FAILURE; + } + } + + Tokenizer p(contentType); + p.SkipUntil(Token::Char(';')); + if (!p.CheckChar(';')) { + return NS_ERROR_CORRUPTED_CONTENT; + } + p.SkipWhites(); + if (!p.CheckWord("boundary")) { + return NS_ERROR_CORRUPTED_CONTENT; + } + p.SkipWhites(); + if (!p.CheckChar('=')) { + return NS_ERROR_CORRUPTED_CONTENT; + } + p.SkipWhites(); + Unused << p.ReadUntil(Token::Char(';'), mBoundary); + mBoundary.Trim( + " \""); // ignoring potential quoted string formatting violations + if (mBoundary.IsEmpty()) { + return NS_ERROR_CORRUPTED_CONTENT; + } + + mHeaderTokens[HEADER_CONTENT_TYPE] = mTokenizer.AddCustomToken( + "content-type", mTokenizer.CASE_INSENSITIVE, false); + mHeaderTokens[HEADER_CONTENT_LENGTH] = mTokenizer.AddCustomToken( + "content-length", mTokenizer.CASE_INSENSITIVE, false); + mHeaderTokens[HEADER_CONTENT_DISPOSITION] = mTokenizer.AddCustomToken( + "content-disposition", mTokenizer.CASE_INSENSITIVE, false); + mHeaderTokens[HEADER_SET_COOKIE] = mTokenizer.AddCustomToken( + "set-cookie", mTokenizer.CASE_INSENSITIVE, false); + mHeaderTokens[HEADER_CONTENT_RANGE] = mTokenizer.AddCustomToken( + "content-range", mTokenizer.CASE_INSENSITIVE, false); + mHeaderTokens[HEADER_RANGE] = + mTokenizer.AddCustomToken("range", mTokenizer.CASE_INSENSITIVE, false); + mHeaderTokens[HEADER_CONTENT_SECURITY_POLICY] = mTokenizer.AddCustomToken( + "content-security-policy", mTokenizer.CASE_INSENSITIVE, false); + + mLFToken = mTokenizer.AddCustomToken("\n", mTokenizer.CASE_SENSITIVE, false); + mCRLFToken = + mTokenizer.AddCustomToken("\r\n", mTokenizer.CASE_SENSITIVE, false); + + SwitchToControlParsing(); + + mBoundaryToken = + mTokenizer.AddCustomToken(mBoundary, mTokenizer.CASE_SENSITIVE); + mBoundaryTokenWithDashes = + mTokenizer.AddCustomToken("--"_ns + mBoundary, mTokenizer.CASE_SENSITIVE); + + return NS_OK; +} + +// nsIStreamListener implementation +NS_IMETHODIMP +nsMultiMixedConv::OnDataAvailable(nsIRequest* request, nsIInputStream* inStr, + uint64_t sourceOffset, uint32_t count) { + // Failing these assertions may indicate that some of the target listeners of + // this converter is looping the thead queue, which is harmful to how we + // collect the raw (content) data. + MOZ_DIAGNOSTIC_ASSERT(!mInOnDataAvailable, + "nsMultiMixedConv::OnDataAvailable reentered!"); + MOZ_DIAGNOSTIC_ASSERT( + !mRawData, "There are unsent data from the previous tokenizer feed!"); + + if (mInOnDataAvailable) { + // The multipart logic is incapable of being reentered. + return NS_ERROR_UNEXPECTED; + } + + mozilla::AutoRestore<bool> restore(mInOnDataAvailable); + mInOnDataAvailable = true; + + nsresult rv_feed = mTokenizer.FeedInput(inStr, count); + // We must do this every time. Regardless if something has failed during the + // parsing process. Otherwise the raw data reference would not be thrown + // away. + nsresult rv_send = SendData(); + + return NS_FAILED(rv_send) ? rv_send : rv_feed; +} + +NS_IMETHODIMP +nsMultiMixedConv::OnDataFinished(nsresult aStatus) { return NS_OK; } + +NS_IMETHODIMP +nsMultiMixedConv::CheckListenerChain() { return NS_ERROR_NOT_IMPLEMENTED; } + +NS_IMETHODIMP +nsMultiMixedConv::OnStopRequest(nsIRequest* request, nsresult aStatus) { + nsresult rv; + + if (mPartChannel) { + mPartChannel->SetIsLastPart(); + + MOZ_DIAGNOSTIC_ASSERT( + !mRawData, "There are unsent data from the previous tokenizer feed!"); + + rv = mTokenizer.FinishInput(); + if (NS_SUCCEEDED(aStatus)) { + aStatus = rv; + } + rv = SendData(); + if (NS_SUCCEEDED(aStatus)) { + aStatus = rv; + } + + (void)SendStop(aStatus); + } else if (NS_FAILED(aStatus) && !mRequestListenerNotified) { + // underlying data production problem. we should not be in + // the middle of sending data. if we were, mPartChannel, + // above, would have been non-null. + + (void)mFinalListener->OnStartRequest(request); + (void)mFinalListener->OnStopRequest(request, aStatus); + } + + nsCOMPtr<nsIMultiPartChannelListener> multiListener = + do_QueryInterface(mFinalListener); + if (multiListener) { + multiListener->OnAfterLastPart(aStatus); + } + + return NS_OK; +} + +nsresult nsMultiMixedConv::ConsumeToken(Token const& token) { + nsresult rv; + + switch (mParserState) { + case PREAMBLE: + if (token.Equals(mBoundaryTokenWithDashes)) { + // The server first used boundary '--boundary'. Hence, we no longer + // accept plain 'boundary' token as a delimiter. + mTokenizer.RemoveCustomToken(mBoundaryToken); + mParserState = BOUNDARY_CRLF; + break; + } + if (token.Equals(mBoundaryToken)) { + // And here the opposite from the just above block... + mTokenizer.RemoveCustomToken(mBoundaryTokenWithDashes); + mParserState = BOUNDARY_CRLF; + break; + } + + // This is a preamble, just ignore it and wait for the boundary. + break; + + case BOUNDARY_CRLF: + if (token.Equals(Token::NewLine())) { + mParserState = HEADER_NAME; + mResponseHeader = HEADER_UNKNOWN; + HeadersToDefault(); + SetHeaderTokensEnabled(true); + break; + } + return NS_ERROR_CORRUPTED_CONTENT; + + case HEADER_NAME: + SetHeaderTokensEnabled(false); + if (token.Equals(Token::NewLine())) { + mParserState = BODY_INIT; + SwitchToBodyParsing(); + break; + } + for (uint32_t h = HEADER_CONTENT_TYPE; h < HEADER_UNKNOWN; ++h) { + if (token.Equals(mHeaderTokens[h])) { + mResponseHeader = static_cast<EHeader>(h); + break; + } + } + mParserState = HEADER_SEP; + break; + + case HEADER_SEP: + if (token.Equals(Token::Char(':'))) { + mParserState = HEADER_VALUE; + mResponseHeaderValue.Truncate(); + break; + } + if (mResponseHeader == HEADER_UNKNOWN) { + // If the header is not of any we understand, just pass everything till + // ':' + break; + } + if (token.Equals(Token::Whitespace())) { + // Accept only header-name traling whitespaces after known headers + break; + } + return NS_ERROR_CORRUPTED_CONTENT; + + case HEADER_VALUE: + if (token.Equals(Token::Whitespace()) && mResponseHeaderValue.IsEmpty()) { + // Eat leading whitespaces + break; + } + if (token.Equals(Token::NewLine())) { + nsresult rv = ProcessHeader(); + if (NS_FAILED(rv)) { + return rv; + } + mParserState = HEADER_NAME; + mResponseHeader = HEADER_UNKNOWN; + SetHeaderTokensEnabled(true); + } else { + mResponseHeaderValue.Append(token.Fragment()); + } + break; + + case BODY_INIT: + rv = SendStart(); + if (NS_FAILED(rv)) { + return rv; + } + mParserState = BODY; + [[fallthrough]]; + + case BODY: { + if (!token.Equals(mLFToken) && !token.Equals(mCRLFToken)) { + if (token.Equals(mBoundaryTokenWithDashes) || + token.Equals(mBoundaryToken)) { + // Allow CRLF to NOT be part of the boundary as well + SwitchToControlParsing(); + mParserState = TRAIL_DASH1; + break; + } + AccumulateData(token); + break; + } + + // After CRLF we must explicitly check for boundary. If found, + // that CRLF is part of the boundary and must not be send to the + // data listener. + Token token2; + if (!mTokenizer.Next(token2)) { + // Note: this will give us the CRLF token again when more data + // or OnStopRequest arrive. I.e. we will enter BODY case in + // the very same state as we are now and start this block over. + mTokenizer.NeedMoreInput(); + break; + } + if (token2.Equals(mBoundaryTokenWithDashes) || + token2.Equals(mBoundaryToken)) { + SwitchToControlParsing(); + mParserState = TRAIL_DASH1; + break; + } + + AccumulateData(token); + AccumulateData(token2); + break; + } + + case TRAIL_DASH1: + if (token.Equals(Token::NewLine())) { + rv = SendStop(NS_OK); + if (NS_FAILED(rv)) { + return rv; + } + mParserState = BOUNDARY_CRLF; + mTokenizer.Rollback(); + break; + } + if (token.Equals(Token::Char('-'))) { + mParserState = TRAIL_DASH2; + break; + } + return NS_ERROR_CORRUPTED_CONTENT; + + case TRAIL_DASH2: + if (token.Equals(Token::Char('-'))) { + mPartChannel->SetIsLastPart(); + // SendStop calls SendData first. + rv = SendStop(NS_OK); + if (NS_FAILED(rv)) { + return rv; + } + mParserState = EPILOGUE; + break; + } + return NS_ERROR_CORRUPTED_CONTENT; + + case EPILOGUE: + // Just ignore + break; + + default: + MOZ_ASSERT(false, "Missing parser state handling branch"); + break; + } // switch + + return NS_OK; +} + +void nsMultiMixedConv::SetHeaderTokensEnabled(bool aEnable) { + for (uint32_t h = HEADER_FIRST; h < HEADER_UNKNOWN; ++h) { + mTokenizer.EnableCustomToken(mHeaderTokens[h], aEnable); + } +} + +void nsMultiMixedConv::SwitchToBodyParsing() { + mTokenizer.SetTokenizingMode(Tokenizer::Mode::CUSTOM_ONLY); + mTokenizer.EnableCustomToken(mLFToken, true); + mTokenizer.EnableCustomToken(mCRLFToken, true); + mTokenizer.EnableCustomToken(mBoundaryTokenWithDashes, true); + mTokenizer.EnableCustomToken(mBoundaryToken, true); +} + +void nsMultiMixedConv::SwitchToControlParsing() { + mTokenizer.SetTokenizingMode(Tokenizer::Mode::FULL); + mTokenizer.EnableCustomToken(mLFToken, false); + mTokenizer.EnableCustomToken(mCRLFToken, false); + mTokenizer.EnableCustomToken(mBoundaryTokenWithDashes, false); + mTokenizer.EnableCustomToken(mBoundaryToken, false); +} + +// nsMultiMixedConv methods +nsMultiMixedConv::nsMultiMixedConv() + // XXX: This is a hack to bypass the raw pointer to refcounted object in + // lambda analysis. It should be removed and replaced when the + // IncrementalTokenizer API is improved to avoid the need for such + // workarounds. + // + // This is safe because `mTokenizer` will not outlive `this`, meaning + // that this std::bind object will be destroyed before `this` dies. + : mTokenizer(std::bind(&nsMultiMixedConv::ConsumeToken, this, + std::placeholders::_1)) {} + +nsresult nsMultiMixedConv::SendStart() { + nsresult rv = NS_OK; + + nsCOMPtr<nsIStreamListener> partListener(mFinalListener); + if (mContentType.IsEmpty()) { + mContentType.AssignLiteral(UNKNOWN_CONTENT_TYPE); + nsCOMPtr<nsIStreamConverterService> serv = + do_GetService(NS_STREAMCONVERTERSERVICE_CONTRACTID, &rv); + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<nsIStreamListener> converter; + rv = serv->AsyncConvertData(UNKNOWN_CONTENT_TYPE, "*/*", mFinalListener, + mContext, getter_AddRefs(converter)); + if (NS_SUCCEEDED(rv)) { + partListener = converter; + } + } + } + + // if we already have an mPartChannel, that means we never sent a Stop() + // before starting up another "part." that would be bad. + MOZ_ASSERT(!mPartChannel, "tisk tisk, shouldn't be overwriting a channel"); + + nsPartChannel* newChannel; + newChannel = new nsPartChannel(mChannel, mCurrentPartID, mCurrentPartID == 0, + partListener); + + ++mCurrentPartID; + + if (mIsByteRangeRequest) { + newChannel->InitializeByteRange(mByteRangeStart, mByteRangeEnd); + } + + mTotalSent = 0; + + // Set up the new part channel... + mPartChannel = newChannel; + + rv = mPartChannel->SetContentType(mContentType); + if (NS_FAILED(rv)) return rv; + + rv = mPartChannel->SetContentLength(mContentLength); + if (NS_FAILED(rv)) return rv; + + mPartChannel->SetContentDisposition(mContentDisposition); + + // Each part of a multipart/replace response can be used + // for the top level document. We must inform upper layers + // about this by setting the LOAD_REPLACE flag so that certain + // state assertions are evaluated as positive. + nsLoadFlags loadFlags = 0; + mPartChannel->GetLoadFlags(&loadFlags); + loadFlags |= nsIChannel::LOAD_REPLACE; + mPartChannel->SetLoadFlags(loadFlags); + + nsCOMPtr<nsILoadGroup> loadGroup; + (void)mPartChannel->GetLoadGroup(getter_AddRefs(loadGroup)); + + // Add the new channel to the load group (if any) + if (loadGroup) { + rv = loadGroup->AddRequest(mPartChannel, nullptr); + if (NS_FAILED(rv)) return rv; + } + + // This prevents artificial call to OnStart/StopRequest when the root + // channel fails. Since now it's ensured to keep with the nsIStreamListener + // contract every time. + mRequestListenerNotified = true; + + // Let's start off the load. NOTE: we don't forward on the channel passed + // into our OnDataAvailable() as it's the root channel for the raw stream. + return mPartChannel->SendOnStartRequest(mContext); +} + +nsresult nsMultiMixedConv::SendStop(nsresult aStatus) { + // Make sure we send out all accumulcated data prior call to OnStopRequest. + // If there is no data, this is a no-op. + nsresult rv = SendData(); + if (NS_SUCCEEDED(aStatus)) { + aStatus = rv; + } + if (mPartChannel) { + rv = mPartChannel->SendOnStopRequest(mContext, aStatus); + // don't check for failure here, we need to remove the channel from + // the loadgroup. + + // Remove the channel from its load group (if any) + nsCOMPtr<nsILoadGroup> loadGroup; + (void)mPartChannel->GetLoadGroup(getter_AddRefs(loadGroup)); + if (loadGroup) { + (void)loadGroup->RemoveRequest(mPartChannel, mContext, aStatus); + } + } + + mPartChannel = nullptr; + return rv; +} + +void nsMultiMixedConv::AccumulateData(Token const& aToken) { + if (!mRawData) { + // This is the first read of raw data during this FeedInput loop + // of the incremental tokenizer. All 'raw' tokens are coming from + // the same linear buffer, hence begining of this loop raw data + // is begining of the first raw token. Length of this loop raw + // data is just sum of all 'raw' tokens we collect during this loop. + // + // It's ensured we flush (send to to the listener via OnDataAvailable) + // and nullify the collected raw data right after FeedInput call. + // Hence, the reference can't outlive the actual buffer. + mRawData = aToken.Fragment().BeginReading(); + mRawDataLength = 0; + } + + mRawDataLength += aToken.Fragment().Length(); +} + +nsresult nsMultiMixedConv::SendData() { + nsresult rv; + + if (!mRawData) { + return NS_OK; + } + + nsACString::const_char_iterator rawData = mRawData; + mRawData = nullptr; + + if (!mPartChannel) { + return NS_ERROR_FAILURE; // something went wrong w/ processing + } + + if (mContentLength != UINT64_MAX) { + // make sure that we don't send more than the mContentLength + // XXX why? perhaps the Content-Length header was actually wrong!! + if ((uint64_t(mRawDataLength) + mTotalSent) > mContentLength) { + mRawDataLength = static_cast<uint32_t>(mContentLength - mTotalSent); + } + + if (mRawDataLength == 0) return NS_OK; + } + + uint64_t offset = mTotalSent; + mTotalSent += mRawDataLength; + + nsCOMPtr<nsIStringInputStream> ss( + do_CreateInstance("@mozilla.org/io/string-input-stream;1", &rv)); + if (NS_FAILED(rv)) return rv; + + rv = ss->ShareData(rawData, mRawDataLength); + mRawData = nullptr; + if (NS_FAILED(rv)) return rv; + + return mPartChannel->SendOnDataAvailable(mContext, ss, offset, + mRawDataLength); +} + +void nsMultiMixedConv::HeadersToDefault() { + mContentLength = UINT64_MAX; + mContentType.Truncate(); + mContentDisposition.Truncate(); + mContentSecurityPolicy.Truncate(); + mIsByteRangeRequest = false; +} + +nsresult nsMultiMixedConv::ProcessHeader() { + mozilla::Tokenizer p(mResponseHeaderValue); + + switch (mResponseHeader) { + case HEADER_CONTENT_TYPE: + mContentType = mResponseHeaderValue; + mContentType.CompressWhitespace(); + break; + case HEADER_CONTENT_LENGTH: + p.SkipWhites(); + if (!p.ReadInteger(&mContentLength)) { + return NS_ERROR_CORRUPTED_CONTENT; + } + break; + case HEADER_CONTENT_DISPOSITION: + mContentDisposition = mResponseHeaderValue; + mContentDisposition.CompressWhitespace(); + break; + case HEADER_SET_COOKIE: { + nsCOMPtr<nsIHttpChannelInternal> httpInternal = + do_QueryInterface(mChannel); + mResponseHeaderValue.CompressWhitespace(); + if (!StaticPrefs::network_cookie_prevent_set_cookie_from_multipart() && + httpInternal) { + DebugOnly<nsresult> rv = httpInternal->SetCookie(mResponseHeaderValue); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + break; + } + case HEADER_RANGE: + case HEADER_CONTENT_RANGE: { + if (!p.CheckWord("bytes") || !p.CheckWhite()) { + return NS_ERROR_CORRUPTED_CONTENT; + } + p.SkipWhites(); + if (p.CheckChar('*')) { + mByteRangeStart = mByteRangeEnd = 0; + } else if (!p.ReadInteger(&mByteRangeStart) || !p.CheckChar('-') || + !p.ReadInteger(&mByteRangeEnd)) { + return NS_ERROR_CORRUPTED_CONTENT; + } + mIsByteRangeRequest = true; + if (mContentLength == UINT64_MAX) { + mContentLength = uint64_t(mByteRangeEnd - mByteRangeStart + 1); + } + break; + } + case HEADER_CONTENT_SECURITY_POLICY: { + mContentSecurityPolicy = mResponseHeaderValue; + mContentSecurityPolicy.CompressWhitespace(); + nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(mChannel); + if (httpChannel) { + nsCString resultCSP = mRootContentSecurityPolicy; + if (!mContentSecurityPolicy.IsEmpty()) { + // We are updating the root channel CSP header respectively for + // each part as: CSP-root + CSP-partN, where N is the part number. + // Here we append current part's CSP to root CSP and reset CSP + // header for each part. + if (!resultCSP.IsEmpty()) { + resultCSP.Append(";"); + } + resultCSP.Append(mContentSecurityPolicy); + } + nsresult rv = httpChannel->SetResponseHeader( + "Content-Security-Policy"_ns, resultCSP, false); + if (NS_FAILED(rv)) { + return NS_ERROR_CORRUPTED_CONTENT; + } + } + break; + } + case HEADER_UNKNOWN: + // We ignore anything else... + break; + } + + return NS_OK; +} + +nsresult NS_NewMultiMixedConv(nsMultiMixedConv** aMultiMixedConv) { + MOZ_ASSERT(aMultiMixedConv != nullptr, "null ptr"); + + RefPtr<nsMultiMixedConv> conv = new nsMultiMixedConv(); + conv.forget(aMultiMixedConv); + return NS_OK; +} diff --git a/netwerk/streamconv/converters/nsMultiMixedConv.h b/netwerk/streamconv/converters/nsMultiMixedConv.h new file mode 100644 index 0000000000..4f773a69eb --- /dev/null +++ b/netwerk/streamconv/converters/nsMultiMixedConv.h @@ -0,0 +1,259 @@ +/* -*- 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/. */ +#ifndef __nsmultimixedconv__h__ +#define __nsmultimixedconv__h__ + +#include "nsIStreamConverter.h" +#include "nsIChannel.h" +#include "nsString.h" +#include "nsCOMPtr.h" +#include "nsIByteRangeRequest.h" +#include "nsIMultiPartChannel.h" +#include "mozilla/Attributes.h" +#include "mozilla/IncrementalTokenizer.h" +#include "nsHttpResponseHead.h" +#include "mozilla/UniquePtr.h" + +#define NS_MULTIMIXEDCONVERTER_CID \ + { /* 7584CE90-5B25-11d3-A175-0050041CAF44 */ \ + 0x7584ce90, 0x5b25, 0x11d3, { \ + 0xa1, 0x75, 0x0, 0x50, 0x4, 0x1c, 0xaf, 0x44 \ + } \ + } + +// +// nsPartChannel is a "dummy" channel which represents an individual part of +// a multipart/mixed stream... +// +// Instances on this channel are passed out to the consumer through the +// nsIStreamListener interface. +// +class nsPartChannel final : public nsIChannel, + public nsIByteRangeRequest, + public nsIMultiPartChannel { + public: + nsPartChannel(nsIChannel* aMultipartChannel, uint32_t aPartID, + bool aIsFirstPart, nsIStreamListener* aListener); + + void InitializeByteRange(int64_t aStart, int64_t aEnd); + void SetIsLastPart() { mIsLastPart = true; } + nsresult SendOnStartRequest(nsISupports* aContext); + nsresult SendOnDataAvailable(nsISupports* aContext, nsIInputStream* aStream, + uint64_t aOffset, uint32_t aLen); + nsresult SendOnStopRequest(nsISupports* aContext, nsresult aStatus); + /* SetContentDisposition expects the full value of the Content-Disposition + * header */ + void SetContentDisposition(const nsACString& aContentDispositionHeader); + // TODO(ER): This appears to be dead code + void SetResponseHead(mozilla::net::nsHttpResponseHead* head) { + mResponseHead.reset(head); + } + + NS_DECL_ISUPPORTS + NS_DECL_NSIREQUEST + NS_DECL_NSICHANNEL + NS_DECL_NSIBYTERANGEREQUEST + NS_DECL_NSIMULTIPARTCHANNEL + + protected: + ~nsPartChannel() = default; + + protected: + nsCOMPtr<nsIChannel> mMultipartChannel; + nsCOMPtr<nsIStreamListener> mListener; + mozilla::UniquePtr<mozilla::net::nsHttpResponseHead> mResponseHead; + + nsresult mStatus{NS_OK}; + nsLoadFlags mLoadFlags{0}; + + nsCOMPtr<nsILoadGroup> mLoadGroup; + + nsCString mContentType; + nsCString mContentCharset; + uint32_t mContentDisposition{0}; + nsString mContentDispositionFilename; + nsCString mContentDispositionHeader; + uint64_t mContentLength{UINT64_MAX}; + + bool mIsByteRangeRequest{false}; + int64_t mByteRangeStart{0}; + int64_t mByteRangeEnd{0}; + + uint32_t mPartID; // unique ID that can be used to identify + // this part of the multipart document + bool mIsFirstPart; + bool mIsLastPart{false}; +}; + +// The nsMultiMixedConv stream converter converts a stream of type +// "multipart/x-mixed-replace" to it's subparts. There was some debate as to +// whether or not the functionality desired when HTTP confronted this type +// required a stream converter. After all, this type really prompts various +// viewer related actions rather than stream conversion. There simply needs to +// be a piece in place that can strip out the multiple parts of a stream of this +// type, and "display" them accordingly. +// +// With that said, this "stream converter" spends more time packaging up the sub +// parts of the main stream and sending them off the destination stream +// listener, than doing any real stream parsing/converting. +// +// WARNING: This converter requires that it's destination stream listener be +// able to handle multiple OnStartRequest(), OnDataAvailable(), and +// OnStopRequest() call combinations. Each series represents the beginning, +// data production, and ending phase of each sub- part of the original +// stream. +// +// NOTE: this MIME-type is used by HTTP, *not* SMTP, or IMAP. +// +// NOTE: For reference, a general description of how this MIME type should be +// handled via HTTP, see +// http://home.netscape.com/assist/net_sites/pushpull.html . Note that real +// world server content deviates considerably from this overview. +// +// Implementation assumptions: +// Assumed structue: +// --BoundaryToken[\r]\n +// content-type: foo/bar[\r]\n +// ... (other headers if any) +// [\r]\n (second line feed to delimit end of headers) +// data +// --BoundaryToken-- (end delimited by final "--") +// +// linebreaks can be either CRLF or LFLF. linebreaks preceding +// boundary tokens are NOT considered part of the data. BoundaryToken +// is any opaque string. +// +// + +class nsMultiMixedConv : public nsIStreamConverter { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSISTREAMCONVERTER + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSITHREADRETARGETABLESTREAMLISTENER + NS_DECL_NSIREQUESTOBSERVER + + explicit nsMultiMixedConv(); + + protected: + using Token = mozilla::IncrementalTokenizer::Token; + + virtual ~nsMultiMixedConv() = default; + + nsresult SendStart(); + void AccumulateData(Token const& aToken); + nsresult SendData(); + nsresult SendStop(nsresult aStatus); + + // member data + nsCOMPtr<nsIStreamListener> mFinalListener; // this guy gets the converted + // data via his OnDataAvailable() + + nsCOMPtr<nsIChannel> + mChannel; // The channel as we get in in OnStartRequest call + RefPtr<nsPartChannel> mPartChannel; // the channel for the given part we're + // processing. one channel per part. + nsCOMPtr<nsISupports> mContext; + nsCString mContentType; + nsCString mContentDisposition; + nsCString mContentSecurityPolicy; + nsCString mRootContentSecurityPolicy; + uint64_t mContentLength{UINT64_MAX}; + uint64_t mTotalSent{0}; + + // The following members are for tracking the byte ranges in + // multipart/mixed content which specified the 'Content-Range:' + // header... + int64_t mByteRangeStart{0}; + int64_t mByteRangeEnd{0}; + bool mIsByteRangeRequest{false}; + // This flag is set first time we create a part channel. + // We use it to prevent duplicated OnStopRequest call on the listener + // when we fail from some reason to ever create a part channel that + // ensures correct notifications. + bool mRequestListenerNotified{false}; + + uint32_t mCurrentPartID{0}; + + // Flag preventing reenter of OnDataAvailable in case the target listener + // ends up spinning the event loop. + bool mInOnDataAvailable{false}; + + // Current state of the incremental parser + enum EParserState { + PREAMBLE, + BOUNDARY_CRLF, + HEADER_NAME, + HEADER_SEP, + HEADER_VALUE, + BODY_INIT, + BODY, + TRAIL_DASH1, + TRAIL_DASH2, + EPILOGUE, + + INIT = PREAMBLE + } mParserState{INIT}; + + // Response part header value, valid when we find a header name + // we recognize. + enum EHeader : uint32_t { + HEADER_FIRST, + HEADER_CONTENT_TYPE = HEADER_FIRST, + HEADER_CONTENT_LENGTH, + HEADER_CONTENT_DISPOSITION, + HEADER_SET_COOKIE, + HEADER_CONTENT_RANGE, + HEADER_RANGE, + HEADER_CONTENT_SECURITY_POLICY, + HEADER_UNKNOWN + } mResponseHeader{HEADER_UNKNOWN}; + // Cumulated value of a response header. + nsCString mResponseHeaderValue; + + nsCString mBoundary; + mozilla::IncrementalTokenizer mTokenizer; + + // When in the "body parsing" mode, see below, we cumulate raw data + // incrementally to mainly avoid any unnecessary granularity. + // mRawData points to the first byte in the tokenizer buffer where part + // body data begins or continues. mRawDataLength is a cumulated length + // of that data during a single tokenizer input feed. This is always + // flushed right after we fed the tokenizer. + nsACString::const_char_iterator mRawData{nullptr}; + nsACString::size_type mRawDataLength{0}; + + // At the start we don't know if the server will be sending boundary with + // or without the leading dashes. + Token mBoundaryToken; + Token mBoundaryTokenWithDashes; + // We need these custom tokens to allow finding CRLF when in the binary mode. + // CRLF before boundary is considered part of the boundary and not part of + // the data. + Token mLFToken; + Token mCRLFToken; + // Custom tokens for each of the response headers we recognize. + Token mHeaderTokens[HEADER_UNKNOWN]; + + // Resets values driven by part headers, like content type, to their defaults, + // called at the start of every part processing. + void HeadersToDefault(); + // Processes captured value of mResponseHeader header. + nsresult ProcessHeader(); + // Switches the parser and tokenizer state to "binary mode" which only + // searches for the 'CRLF boundary' delimiter. + void SwitchToBodyParsing(); + // Switches to the default mode, we are in this mode when parsing headers and + // control data around the boundary delimiters. + void SwitchToControlParsing(); + // Turns on or off recognition of the headers we recognize in part heads. + void SetHeaderTokensEnabled(bool aEnable); + + // The main parser callback called by the IncrementalTokenizer + // instance from OnDataAvailable or OnStopRequest. + nsresult ConsumeToken(Token const& token); +}; + +#endif /* __nsmultimixedconv__h__ */ diff --git a/netwerk/streamconv/converters/nsUnknownDecoder.cpp b/netwerk/streamconv/converters/nsUnknownDecoder.cpp new file mode 100644 index 0000000000..b572b1c276 --- /dev/null +++ b/netwerk/streamconv/converters/nsUnknownDecoder.cpp @@ -0,0 +1,867 @@ +/* -*- 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 "nsUnknownDecoder.h" +#include "nsIPipe.h" +#include "nsIInputStream.h" +#include "nsIOutputStream.h" +#include "nsMimeTypes.h" + +#include "nsCRT.h" + +#include "nsIMIMEService.h" + +#include "nsIViewSourceChannel.h" +#include "nsIHttpChannel.h" +#include "nsIForcePendingChannel.h" +#include "nsIEncodedChannel.h" +#include "nsIURI.h" +#include "nsStringStream.h" +#include "nsNetCID.h" +#include "nsNetUtil.h" +#include "nsQueryObject.h" +#include "nsComponentManagerUtils.h" +#include "nsServiceManagerUtils.h" + +#include <algorithm> + +#define MAX_BUFFER_SIZE 512u + +using namespace mozilla; + +NS_IMPL_ISUPPORTS(nsUnknownDecoder::ConvertedStreamListener, nsIStreamListener, + nsIRequestObserver) + +nsUnknownDecoder::ConvertedStreamListener::ConvertedStreamListener( + nsUnknownDecoder* aDecoder) { + mDecoder = aDecoder; +} + +nsresult nsUnknownDecoder::ConvertedStreamListener::AppendDataToString( + nsIInputStream* inputStream, void* closure, const char* rawSegment, + uint32_t toOffset, uint32_t count, uint32_t* writeCount) { + nsCString* decodedData = static_cast<nsCString*>(closure); + decodedData->Append(rawSegment, count); + *writeCount = count; + return NS_OK; +} + +NS_IMETHODIMP +nsUnknownDecoder::ConvertedStreamListener::OnStartRequest(nsIRequest* request) { + return NS_OK; +} + +NS_IMETHODIMP +nsUnknownDecoder::ConvertedStreamListener::OnDataAvailable( + nsIRequest* request, nsIInputStream* stream, uint64_t offset, + uint32_t count) { + uint32_t read; + nsAutoCString decodedData; + { + MutexAutoLock lock(mDecoder->mMutex); + decodedData = mDecoder->mDecodedData; + } + nsresult rv = + stream->ReadSegments(AppendDataToString, &decodedData, count, &read); + if (NS_FAILED(rv)) { + return rv; + } + MutexAutoLock lock(mDecoder->mMutex); + mDecoder->mDecodedData = decodedData; + return NS_OK; +} + +NS_IMETHODIMP +nsUnknownDecoder::ConvertedStreamListener::OnStopRequest(nsIRequest* request, + nsresult status) { + return NS_OK; +} + +nsUnknownDecoder::nsUnknownDecoder(nsIStreamListener* aListener) + : mNextListener(aListener), + mBuffer(nullptr), + mBufferLen(0), + mMutex("nsUnknownDecoder"), + mDecodedData("") {} + +nsUnknownDecoder::~nsUnknownDecoder() { + if (mBuffer) { + delete[] mBuffer; + mBuffer = nullptr; + } +} + +// ---- +// +// nsISupports implementation... +// +// ---- + +NS_IMPL_ADDREF(nsUnknownDecoder) +NS_IMPL_RELEASE(nsUnknownDecoder) + +NS_INTERFACE_MAP_BEGIN(nsUnknownDecoder) + NS_INTERFACE_MAP_ENTRY(nsIStreamConverter) + NS_INTERFACE_MAP_ENTRY(nsIStreamListener) + NS_INTERFACE_MAP_ENTRY(nsIRequestObserver) + NS_INTERFACE_MAP_ENTRY(nsIContentSniffer) + NS_INTERFACE_MAP_ENTRY(nsIThreadRetargetableStreamListener) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, + nsIThreadRetargetableStreamListener) +NS_INTERFACE_MAP_END + +// ---- +// +// nsIStreamConverter methods... +// +// ---- + +NS_IMETHODIMP +nsUnknownDecoder::Convert(nsIInputStream* aFromStream, const char* aFromType, + const char* aToType, nsISupports* aCtxt, + nsIInputStream** aResultStream) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsUnknownDecoder::AsyncConvertData(const char* aFromType, const char* aToType, + nsIStreamListener* aListener, + nsISupports* aCtxt) { + NS_ASSERTION(aListener && aFromType && aToType, + "null pointer passed into multi mixed converter"); + // hook up our final listener. this guy gets the various On*() calls we want + // to throw at him. + // + + MutexAutoLock lock(mMutex); + mNextListener = aListener; + return (aListener) ? NS_OK : NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsUnknownDecoder::GetConvertedType(const nsACString& aFromType, + nsIChannel* aChannel, nsACString& aToType) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +// ---- +// +// nsIStreamListener methods... +// +// ---- + +NS_IMETHODIMP +nsUnknownDecoder::OnDataAvailable(nsIRequest* request, nsIInputStream* aStream, + uint64_t aSourceOffset, uint32_t aCount) { + nsresult rv = NS_OK; + + bool contentTypeEmpty; + { + MutexAutoLock lock(mMutex); + if (!mNextListener) return NS_ERROR_FAILURE; + + contentTypeEmpty = mContentType.IsEmpty(); + } + + if (contentTypeEmpty) { + uint32_t count, len; + + // If the buffer has not been allocated by now, just fail... + if (!mBuffer) return NS_ERROR_OUT_OF_MEMORY; + + // + // Determine how much of the stream should be read to fill up the + // sniffer buffer... + // + if (mBufferLen + aCount >= MAX_BUFFER_SIZE) { + count = MAX_BUFFER_SIZE - mBufferLen; + } else { + count = aCount; + } + + // Read the data into the buffer... + rv = aStream->Read((mBuffer + mBufferLen), count, &len); + if (NS_FAILED(rv)) return rv; + + mBufferLen += len; + aCount -= len; + + if (aCount) { + // + // Adjust the source offset... The call to FireListenerNotifications(...) + // will make the first OnDataAvailable(...) call with an offset of 0. + // So, this offset needs to be adjusted to reflect that... + // + aSourceOffset += mBufferLen; + + DetermineContentType(request); + + rv = FireListenerNotifications(request, nullptr); + } + } + + // Must not fire ODA again if it failed once + if (aCount && NS_SUCCEEDED(rv)) { +#ifdef DEBUG + { + MutexAutoLock lock(mMutex); + NS_ASSERTION(!mContentType.IsEmpty(), + "Content type should be known by now."); + } +#endif + + nsCOMPtr<nsIStreamListener> listener; + { + MutexAutoLock lock(mMutex); + listener = mNextListener; + } + rv = listener->OnDataAvailable(request, aStream, aSourceOffset, aCount); + } + + return rv; +} + +// ---- +// +// nsIRequestObserver methods... +// +// ---- + +NS_IMETHODIMP +nsUnknownDecoder::OnStartRequest(nsIRequest* request) { + nsresult rv = NS_OK; + + { + MutexAutoLock lock(mMutex); + if (!mNextListener) return NS_ERROR_FAILURE; + } + + // Allocate the sniffer buffer... + if (NS_SUCCEEDED(rv) && !mBuffer) { + mBuffer = new char[MAX_BUFFER_SIZE]; + + if (!mBuffer) { + rv = NS_ERROR_OUT_OF_MEMORY; + } + } + + // Do not pass the OnStartRequest on to the next listener (yet)... + return rv; +} + +NS_IMETHODIMP +nsUnknownDecoder::OnStopRequest(nsIRequest* request, nsresult aStatus) { + nsresult rv = NS_OK; + + bool contentTypeEmpty; + { + MutexAutoLock lock(mMutex); + if (!mNextListener) return NS_ERROR_FAILURE; + + contentTypeEmpty = mContentType.IsEmpty(); + } + + // + // The total amount of data is less than the size of the sniffer buffer. + // Analyze the buffer now... + // + if (contentTypeEmpty) { + DetermineContentType(request); + + // Make sure channel listeners see channel as pending while we call + // OnStartRequest/OnDataAvailable, even though the underlying channel + // has already hit OnStopRequest. + nsCOMPtr<nsIForcePendingChannel> forcePendingChannel = + do_QueryInterface(request); + if (forcePendingChannel) { + forcePendingChannel->ForcePending(true); + } + + rv = FireListenerNotifications(request, nullptr); + + if (NS_FAILED(rv)) { + aStatus = rv; + } + + // now we need to set pending state to false before calling OnStopRequest + if (forcePendingChannel) { + forcePendingChannel->ForcePending(false); + } + } + + nsCOMPtr<nsIStreamListener> listener; + { + MutexAutoLock lock(mMutex); + listener = mNextListener; + mNextListener = nullptr; + } + rv = listener->OnStopRequest(request, aStatus); + + return rv; +} + +// ---- +// +// nsIContentSniffer methods... +// +// ---- +NS_IMETHODIMP +nsUnknownDecoder::GetMIMETypeFromContent(nsIRequest* aRequest, + const uint8_t* aData, uint32_t aLength, + nsACString& type) { + // This is only used by sniffer, therefore we do not need to lock anything + // here. + nsCOMPtr<nsIChannel> channel(do_QueryInterface(aRequest)); + if (channel) { + nsCOMPtr<nsILoadInfo> loadInfo = channel->LoadInfo(); + if (loadInfo->GetSkipContentSniffing()) { + return NS_ERROR_NOT_AVAILABLE; + } + } + + mBuffer = const_cast<char*>(reinterpret_cast<const char*>(aData)); + mBufferLen = aLength; + DetermineContentType(aRequest); + mBuffer = nullptr; + mBufferLen = 0; + type.Assign(mContentType); + mContentType.Truncate(); + return type.IsEmpty() ? NS_ERROR_NOT_AVAILABLE : NS_OK; +} + +// Actual sniffing code + +/** + * This is the array of sniffer entries that depend on "magic numbers" + * in the file. Each entry has either a type associated with it (set + * these with the SNIFFER_ENTRY macro) or a function to be executed + * (set these with the SNIFFER_ENTRY_WITH_FUNC macro). The function + * should take a single nsIRequest* and returns bool -- true if + * it sets mContentType, false otherwise + */ +nsUnknownDecoder::nsSnifferEntry nsUnknownDecoder::sSnifferEntries[] = { + SNIFFER_ENTRY("%PDF-", APPLICATION_PDF), + + SNIFFER_ENTRY("%!PS-Adobe-", APPLICATION_POSTSCRIPT), + + // Files that start with mailbox delimiters let's provisionally call + // text/plain + SNIFFER_ENTRY("From", TEXT_PLAIN), SNIFFER_ENTRY(">From", TEXT_PLAIN), + + // If the buffer begins with "#!" or "%!" then it is a script of + // some sort... "Scripts" can include arbitrary data to be passed + // to an interpreter, so we need to decide whether we can call this + // text or whether it's data. + SNIFFER_ENTRY_WITH_FUNC("#!", &nsUnknownDecoder::LastDitchSniff), + + // XXXbz should (and can) we also include the various ways that <?xml can + // appear as UTF-16 and such? See http://www.w3.org/TR/REC-xml#sec-guessing + SNIFFER_ENTRY_WITH_FUNC("<?xml", &nsUnknownDecoder::SniffForXML)}; + +uint32_t nsUnknownDecoder::sSnifferEntryNum = + sizeof(nsUnknownDecoder::sSnifferEntries) / + sizeof(nsUnknownDecoder::nsSnifferEntry); + +void nsUnknownDecoder::DetermineContentType(nsIRequest* aRequest) { + { + MutexAutoLock lock(mMutex); + NS_ASSERTION(mContentType.IsEmpty(), "Content type is already known."); + if (!mContentType.IsEmpty()) return; + } + + nsCOMPtr<nsIChannel> channel(do_QueryInterface(aRequest)); + if (channel) { + nsCOMPtr<nsILoadInfo> loadInfo = channel->LoadInfo(); + if (loadInfo->GetSkipContentSniffing()) { + /* + * If we did not get a useful Content-Type from the server + * but also have sniffing disabled, just determine whether + * to use text/plain or octetstream and log an error to the Console + */ + LastDitchSniff(aRequest); + + nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(aRequest)); + if (httpChannel) { + nsAutoCString type; + httpChannel->GetContentType(type); + nsCOMPtr<nsIURI> requestUri; + httpChannel->GetURI(getter_AddRefs(requestUri)); + nsAutoCString spec; + requestUri->GetSpec(spec); + if (spec.Length() > 50) { + spec.Truncate(50); + spec.AppendLiteral("..."); + } + httpChannel->LogMimeTypeMismatch( + "XTCOWithMIMEValueMissing"_ns, false, NS_ConvertUTF8toUTF16(spec), + // Type is not used in the Error Message but required + NS_ConvertUTF8toUTF16(type)); + } + return; + } + } + + const char* testData = mBuffer; + uint32_t testDataLen = mBufferLen; + // Check if data are compressed. + nsAutoCString decodedData; + + if (channel) { + // ConvertEncodedData is always called only on a single thread for each + // instance of an object. + nsresult rv = ConvertEncodedData(aRequest, mBuffer, mBufferLen); + if (NS_SUCCEEDED(rv)) { + MutexAutoLock lock(mMutex); + decodedData = mDecodedData; + } + if (!decodedData.IsEmpty()) { + testData = decodedData.get(); + testDataLen = std::min<uint32_t>(decodedData.Length(), MAX_BUFFER_SIZE); + } + } + + // First, run through all the types we can detect reliably based on + // magic numbers + uint32_t i; + for (i = 0; i < sSnifferEntryNum; ++i) { + if (testDataLen >= sSnifferEntries[i].mByteLen && // enough data + memcmp(testData, sSnifferEntries[i].mBytes, + sSnifferEntries[i].mByteLen) == 0) { // and type matches + NS_ASSERTION( + sSnifferEntries[i].mMimeType || + sSnifferEntries[i].mContentTypeSniffer, + "Must have either a type string or a function to set the type"); + NS_ASSERTION(!sSnifferEntries[i].mMimeType || + !sSnifferEntries[i].mContentTypeSniffer, + "Both a type string and a type sniffing function set;" + " using type string"); + if (sSnifferEntries[i].mMimeType) { + MutexAutoLock lock(mMutex); + mContentType = sSnifferEntries[i].mMimeType; + NS_ASSERTION(!mContentType.IsEmpty(), + "Content type should be known by now."); + return; + } + if ((this->*(sSnifferEntries[i].mContentTypeSniffer))(aRequest)) { +#ifdef DEBUG + MutexAutoLock lock(mMutex); + NS_ASSERTION(!mContentType.IsEmpty(), + "Content type should be known by now."); +#endif + return; + } + } + } + + nsAutoCString sniffedType; + NS_SniffContent(NS_DATA_SNIFFER_CATEGORY, aRequest, (const uint8_t*)testData, + testDataLen, sniffedType); + { + MutexAutoLock lock(mMutex); + mContentType = sniffedType; + if (!mContentType.IsEmpty()) { + return; + } + } + + if (SniffForHTML(aRequest)) { +#ifdef DEBUG + MutexAutoLock lock(mMutex); + NS_ASSERTION(!mContentType.IsEmpty(), + "Content type should be known by now."); +#endif + return; + } + + // We don't know what this is yet. Before we just give up, try + // the URI from the request. + if (SniffURI(aRequest)) { +#ifdef DEBUG + MutexAutoLock lock(mMutex); + NS_ASSERTION(!mContentType.IsEmpty(), + "Content type should be known by now."); +#endif + return; + } + + LastDitchSniff(aRequest); +#ifdef DEBUG + MutexAutoLock lock(mMutex); + NS_ASSERTION(!mContentType.IsEmpty(), "Content type should be known by now."); +#endif +} + +bool nsUnknownDecoder::SniffForHTML(nsIRequest* aRequest) { + MutexAutoLock lock(mMutex); + + // Now look for HTML. + const char* str; + const char* end; + if (mDecodedData.IsEmpty()) { + str = mBuffer; + end = mBuffer + mBufferLen; + } else { + str = mDecodedData.get(); + end = mDecodedData.get() + + std::min<uint32_t>(mDecodedData.Length(), MAX_BUFFER_SIZE); + } + + // skip leading whitespace + while (str != end && nsCRT::IsAsciiSpace(*str)) { + ++str; + } + + // did we find something like a start tag? + if (str == end || *str != '<' || ++str == end) { + return false; + } + + // If we seem to be SGML or XML and we got down here, just pretend we're HTML + if (*str == '!' || *str == '?') { + mContentType = TEXT_HTML; + return true; + } + + uint32_t bufSize = end - str; + // We use sizeof(_tagstr) below because that's the length of _tagstr + // with the one char " " or ">" appended. +#define MATCHES_TAG(_tagstr) \ + (bufSize >= sizeof(_tagstr) && \ + (nsCRT::strncasecmp(str, _tagstr " ", sizeof(_tagstr)) == 0 || \ + nsCRT::strncasecmp(str, _tagstr ">", sizeof(_tagstr)) == 0)) + + if (MATCHES_TAG("html") || MATCHES_TAG("frameset") || MATCHES_TAG("body") || + MATCHES_TAG("head") || MATCHES_TAG("script") || MATCHES_TAG("iframe") || + MATCHES_TAG("a") || MATCHES_TAG("img") || MATCHES_TAG("table") || + MATCHES_TAG("title") || MATCHES_TAG("link") || MATCHES_TAG("base") || + MATCHES_TAG("style") || MATCHES_TAG("div") || MATCHES_TAG("p") || + MATCHES_TAG("font") || MATCHES_TAG("applet") || MATCHES_TAG("meta") || + MATCHES_TAG("center") || MATCHES_TAG("form") || MATCHES_TAG("isindex") || + MATCHES_TAG("h1") || MATCHES_TAG("h2") || MATCHES_TAG("h3") || + MATCHES_TAG("h4") || MATCHES_TAG("h5") || MATCHES_TAG("h6") || + MATCHES_TAG("b") || MATCHES_TAG("pre")) { + mContentType = TEXT_HTML; + return true; + } + +#undef MATCHES_TAG + + return false; +} + +bool nsUnknownDecoder::SniffForXML(nsIRequest* aRequest) { + // First see whether we can glean anything from the uri... + if (!SniffURI(aRequest)) { + // Oh well; just generic XML will have to do + MutexAutoLock lock(mMutex); + mContentType = TEXT_XML; + } + + return true; +} + +bool nsUnknownDecoder::SniffURI(nsIRequest* aRequest) { + nsCOMPtr<nsIChannel> channel(do_QueryInterface(aRequest)); + nsCOMPtr<nsILoadInfo> loadInfo = channel->LoadInfo(); + if (loadInfo->GetSkipContentSniffing()) { + return false; + } + nsCOMPtr<nsIMIMEService> mimeService(do_GetService("@mozilla.org/mime;1")); + if (mimeService) { + nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest); + if (channel) { + nsCOMPtr<nsIURI> uri; + nsresult result = channel->GetURI(getter_AddRefs(uri)); + if (NS_SUCCEEDED(result) && uri) { + nsAutoCString type; + result = mimeService->GetTypeFromURI(uri, type); + if (NS_SUCCEEDED(result)) { + MutexAutoLock lock(mMutex); + mContentType = type; + return true; + } + } + } + } + + return false; +} + +// This macro is based on RFC 2046 Section 4.1.2. Treat any char 0-31 +// except the 9-13 range (\t, \n, \v, \f, \r) and char 27 (used by +// encodings like Shift_JIS) as non-text +#define IS_TEXT_CHAR(ch) \ + (((unsigned char)(ch)) > 31 || (9 <= (ch) && (ch) <= 13) || (ch) == 27) + +bool nsUnknownDecoder::LastDitchSniff(nsIRequest* aRequest) { + // All we can do now is try to guess whether this is text/plain or + // application/octet-stream + + MutexAutoLock lock(mMutex); + + const char* testData; + uint32_t testDataLen; + if (mDecodedData.IsEmpty()) { + testData = mBuffer; + // Since some legacy text files end with 0x1A, reading the entire buffer + // will lead misdetection. + testDataLen = std::min<uint32_t>(mBufferLen, MAX_BUFFER_SIZE); + } else { + testData = mDecodedData.get(); + testDataLen = std::min<uint32_t>(mDecodedData.Length(), MAX_BUFFER_SIZE); + } + + // First, check for a BOM. If we see one, assume this is text/plain + // in whatever encoding. If there is a BOM _and_ text we will + // always have at least 4 bytes in the buffer (since the 2-byte BOMs + // are for 2-byte encodings and the UTF-8 BOM is 3 bytes). + if (testDataLen >= 4) { + const unsigned char* buf = (const unsigned char*)testData; + if ((buf[0] == 0xFE && buf[1] == 0xFF) || // UTF-16, Big Endian + (buf[0] == 0xFF && buf[1] == 0xFE) || // UTF-16 or UCS-4, Little Endian + (buf[0] == 0xEF && buf[1] == 0xBB && buf[2] == 0xBF) || // UTF-8 + (buf[0] == 0 && buf[1] == 0 && buf[2] == 0xFE && + buf[3] == 0xFF)) { // UCS-4, Big Endian + + mContentType = TEXT_PLAIN; + return true; + } + } + + // Now see whether the buffer has any non-text chars. If not, then let's + // just call it text/plain... + // + uint32_t i; + for (i = 0; i < testDataLen && IS_TEXT_CHAR(testData[i]); i++) { + } + + if (i == testDataLen) { + mContentType = TEXT_PLAIN; + } else { + mContentType = APPLICATION_OCTET_STREAM; + } + + return true; +} + +nsresult nsUnknownDecoder::FireListenerNotifications(nsIRequest* request, + nsISupports* aCtxt) { + nsresult rv = NS_OK; + + nsCOMPtr<nsIStreamListener> listener; + nsAutoCString contentType; + { + MutexAutoLock lock(mMutex); + if (!mNextListener) return NS_ERROR_FAILURE; + + listener = mNextListener; + contentType = mContentType; + } + + if (!contentType.IsEmpty()) { + nsCOMPtr<nsIViewSourceChannel> viewSourceChannel = + do_QueryInterface(request); + if (viewSourceChannel) { + rv = viewSourceChannel->SetOriginalContentType(contentType); + } else { + nsCOMPtr<nsIChannel> channel = do_QueryInterface(request, &rv); + if (NS_SUCCEEDED(rv)) { + // Set the new content type on the channel... + rv = channel->SetContentType(contentType); + } + } + + NS_ASSERTION(NS_SUCCEEDED(rv), "Unable to set content type on channel!"); + + if (NS_FAILED(rv)) { + // Cancel the request to make sure it has the correct status if + // mNextListener looks at it. + request->Cancel(rv); + listener->OnStartRequest(request); + return rv; + } + } + + // Fire the OnStartRequest(...) + rv = listener->OnStartRequest(request); + + if (NS_SUCCEEDED(rv)) { + // install stream converter if required + nsCOMPtr<nsIEncodedChannel> encodedChannel = do_QueryInterface(request); + if (encodedChannel) { + nsCOMPtr<nsIStreamListener> listenerNew; + rv = encodedChannel->DoApplyContentConversions( + listener, getter_AddRefs(listenerNew), aCtxt); + if (NS_SUCCEEDED(rv) && listenerNew) { + MutexAutoLock lock(mMutex); + mNextListener = listenerNew; + listener = listenerNew; + } + } + } + + if (!mBuffer) return NS_ERROR_OUT_OF_MEMORY; + + // If the request was canceled, then we need to treat that equivalently + // to an error returned by OnStartRequest. + if (NS_SUCCEEDED(rv)) request->GetStatus(&rv); + + // Fire the first OnDataAvailable for the data that was read from the + // stream into the sniffer buffer... + if (NS_SUCCEEDED(rv) && (mBufferLen > 0)) { + uint32_t len = 0; + nsCOMPtr<nsIInputStream> in; + nsCOMPtr<nsIOutputStream> out; + + // Create a pipe and fill it with the data from the sniffer buffer. + NS_NewPipe(getter_AddRefs(in), getter_AddRefs(out), MAX_BUFFER_SIZE, + MAX_BUFFER_SIZE); + + rv = out->Write(mBuffer, mBufferLen, &len); + if (NS_SUCCEEDED(rv)) { + if (len == mBufferLen) { + rv = listener->OnDataAvailable(request, in, 0, len); + } else { + NS_ERROR("Unable to write all the data into the pipe."); + rv = NS_ERROR_FAILURE; + } + } + } + + delete[] mBuffer; + mBuffer = nullptr; + mBufferLen = 0; + + return rv; +} + +nsresult nsUnknownDecoder::ConvertEncodedData(nsIRequest* request, + const char* data, + uint32_t length) { + nsresult rv = NS_OK; + + { + MutexAutoLock lock(mMutex); + mDecodedData = ""; + } + nsCOMPtr<nsIEncodedChannel> encodedChannel(do_QueryInterface(request)); + if (encodedChannel) { + RefPtr<ConvertedStreamListener> strListener = + new ConvertedStreamListener(this); + + nsCOMPtr<nsIStreamListener> listener; + rv = encodedChannel->DoApplyContentConversions( + strListener, getter_AddRefs(listener), nullptr); + + if (NS_FAILED(rv)) { + return rv; + } + + if (listener) { + listener->OnStartRequest(request); + + if (length) { + nsCOMPtr<nsIStringInputStream> rawStream = + do_CreateInstance(NS_STRINGINPUTSTREAM_CONTRACTID); + if (!rawStream) return NS_ERROR_FAILURE; + + rv = rawStream->SetData((const char*)data, length); + NS_ENSURE_SUCCESS(rv, rv); + + rv = listener->OnDataAvailable(request, rawStream, 0, length); + NS_ENSURE_SUCCESS(rv, rv); + } + + listener->OnStopRequest(request, NS_OK); + } + } + return rv; +} + +// +// nsIThreadRetargetableStreamListener methods +// +NS_IMETHODIMP +nsUnknownDecoder::CheckListenerChain() { + nsCOMPtr<nsIThreadRetargetableStreamListener> listener; + { + MutexAutoLock lock(mMutex); + listener = do_QueryInterface(mNextListener); + } + if (!listener) { + return NS_ERROR_NO_INTERFACE; + } + + return listener->CheckListenerChain(); +} + +NS_IMETHODIMP +nsUnknownDecoder::OnDataFinished(nsresult aStatus) { + nsCOMPtr<nsIThreadRetargetableStreamListener> listener; + { + MutexAutoLock lock(mMutex); + listener = do_QueryInterface(mNextListener); + } + if (listener) { + return listener->OnDataFinished(aStatus); + } + + return NS_OK; +} + +void nsBinaryDetector::DetermineContentType(nsIRequest* aRequest) { + nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aRequest); + if (!httpChannel) { + return; + } + + nsCOMPtr<nsILoadInfo> loadInfo = httpChannel->LoadInfo(); + if (loadInfo->GetSkipContentSniffing()) { + LastDitchSniff(aRequest); + return; + } + // It's an HTTP channel. Check for the text/plain mess + nsAutoCString contentTypeHdr; + Unused << httpChannel->GetResponseHeader("Content-Type"_ns, contentTypeHdr); + nsAutoCString contentType; + httpChannel->GetContentType(contentType); + + // Make sure to do a case-sensitive exact match comparison here. Apache + // 1.x just sends text/plain for "unknown", while Apache 2.x sends + // text/plain with a ISO-8859-1 charset. Debian's Apache version, just to + // be different, sends text/plain with iso-8859-1 charset. For extra fun, + // FC7, RHEL4, and Ubuntu Feisty send charset=UTF-8. Don't do general + // case-insensitive comparison, since we really want to apply this crap as + // rarely as we can. + if (!contentType.EqualsLiteral("text/plain") || + (!contentTypeHdr.EqualsLiteral("text/plain") && + !contentTypeHdr.EqualsLiteral("text/plain; charset=ISO-8859-1") && + !contentTypeHdr.EqualsLiteral("text/plain; charset=iso-8859-1") && + !contentTypeHdr.EqualsLiteral("text/plain; charset=UTF-8"))) { + return; + } + + // Check whether we have content-encoding. If we do, don't try to + // detect the type. + // XXXbz we could improve this by doing a local decompress if we + // wanted, I'm sure. + nsAutoCString contentEncoding; + Unused << httpChannel->GetResponseHeader("Content-Encoding"_ns, + contentEncoding); + if (!contentEncoding.IsEmpty()) { + return; + } + + LastDitchSniff(aRequest); + MutexAutoLock lock(mMutex); + if (mContentType.EqualsLiteral(APPLICATION_OCTET_STREAM)) { + // We want to guess at it instead + mContentType = APPLICATION_GUESS_FROM_EXT; + } else { + // Let the text/plain type we already have be, so that other content + // sniffers can also get a shot at this data. + mContentType.Truncate(); + } +} diff --git a/netwerk/streamconv/converters/nsUnknownDecoder.h b/netwerk/streamconv/converters/nsUnknownDecoder.h new file mode 100644 index 0000000000..3aed80639d --- /dev/null +++ b/netwerk/streamconv/converters/nsUnknownDecoder.h @@ -0,0 +1,150 @@ +/* -*- 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/. */ + +#ifndef nsUnknownDecoder_h__ +#define nsUnknownDecoder_h__ + +#include "nsIStreamConverter.h" +#include "nsIThreadRetargetableStreamListener.h" +#include "nsIContentSniffer.h" +#include "mozilla/Mutex.h" +#include "mozilla/Atomics.h" + +#include "nsCOMPtr.h" +#include "nsString.h" + +#define NS_UNKNOWNDECODER_CID \ + { /* 7d7008a0-c49a-11d3-9b22-0080c7cb1080 */ \ + 0x7d7008a0, 0xc49a, 0x11d3, { \ + 0x9b, 0x22, 0x00, 0x80, 0xc7, 0xcb, 0x10, 0x80 \ + } \ + } + +class nsUnknownDecoder : public nsIStreamConverter, public nsIContentSniffer { + public: + // nsISupports methods + NS_DECL_ISUPPORTS + + // nsIStreamConverter methods + NS_DECL_NSISTREAMCONVERTER + + // nsIStreamListener methods + NS_DECL_NSISTREAMLISTENER + + // nsIRequestObserver methods + NS_DECL_NSIREQUESTOBSERVER + + // nsIContentSniffer methods + NS_DECL_NSICONTENTSNIFFER + + // nsIThreadRetargetableStreamListener methods + NS_DECL_NSITHREADRETARGETABLESTREAMLISTENER + + explicit nsUnknownDecoder(nsIStreamListener* aListener = nullptr); + + protected: + virtual ~nsUnknownDecoder(); + + virtual void DetermineContentType(nsIRequest* aRequest); + nsresult FireListenerNotifications(nsIRequest* request, nsISupports* aCtxt); + + class ConvertedStreamListener : public nsIStreamListener { + public: + explicit ConvertedStreamListener(nsUnknownDecoder* aDecoder); + + NS_DECL_ISUPPORTS + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + + private: + virtual ~ConvertedStreamListener() = default; + static nsresult AppendDataToString(nsIInputStream* inputStream, + void* closure, const char* rawSegment, + uint32_t toOffset, uint32_t count, + uint32_t* writeCount); + nsUnknownDecoder* mDecoder; + }; + + protected: + nsCOMPtr<nsIStreamListener> mNextListener; + + // Various sniffer functions. Returning true means that a type + // was determined; false means no luck. + bool SniffForHTML(nsIRequest* aRequest); + bool SniffForXML(nsIRequest* aRequest); + + // SniffURI guesses at the content type based on the URI (typically + // using the extentsion) + bool SniffURI(nsIRequest* aRequest); + + // LastDitchSniff guesses at text/plain vs. application/octet-stream + // by just looking at whether the data contains null bytes, and + // maybe at the fraction of chars with high bit set. Use this only + // as a last-ditch attempt to decide a content type! + bool LastDitchSniff(nsIRequest* aRequest); + + /** + * An entry struct for our array of sniffers. Each entry has either + * a type associated with it (set these with the SNIFFER_ENTRY macro) + * or a function to be executed (set these with the + * SNIFFER_ENTRY_WITH_FUNC macro). The function should take a single + * nsIRequest* and returns bool -- true if it sets mContentType, + * false otherwise + */ + struct nsSnifferEntry { + using TypeSniffFunc = bool (nsUnknownDecoder::*)(nsIRequest*); + + const char* mBytes; + uint32_t mByteLen; + + // Exactly one of mMimeType and mContentTypeSniffer should be set non-null + const char* mMimeType; + TypeSniffFunc mContentTypeSniffer; + }; + +#define SNIFFER_ENTRY(_bytes, _type) \ + { _bytes, sizeof(_bytes) - 1, _type, nullptr } + +#define SNIFFER_ENTRY_WITH_FUNC(_bytes, _func) \ + { _bytes, sizeof(_bytes) - 1, nullptr, _func } + + static nsSnifferEntry sSnifferEntries[]; + static uint32_t sSnifferEntryNum; + + // We guarantee in order delivery of OnStart, OnStop and OnData, therefore + // we do not need proper locking for mBuffer. + mozilla::Atomic<char*> mBuffer; + mozilla::Atomic<uint32_t> mBufferLen; + + nsCString mContentType; + + // This mutex syncs: mContentType, mDecodedData and mNextListener. + mutable mozilla::Mutex mMutex MOZ_UNANNOTATED; + + protected: + nsresult ConvertEncodedData(nsIRequest* request, const char* data, + uint32_t length); + nsCString mDecodedData; // If data are encoded this will be uncompress data. +}; + +#define NS_BINARYDETECTOR_CID \ + { /* a2027ec6-ba0d-4c72-805d-148233f5f33c */ \ + 0xa2027ec6, 0xba0d, 0x4c72, { \ + 0x80, 0x5d, 0x14, 0x82, 0x33, 0xf5, 0xf3, 0x3c \ + } \ + } + +/** + * Class that detects whether a data stream is text or binary. This reuses + * most of nsUnknownDecoder except the actual content-type determination logic + * -- our overridden DetermineContentType simply calls LastDitchSniff and sets + * the type to APPLICATION_GUESS_FROM_EXT if the data is detected as binary. + */ +class nsBinaryDetector : public nsUnknownDecoder { + protected: + virtual void DetermineContentType(nsIRequest* aRequest) override; +}; + +#endif /* nsUnknownDecoder_h__ */ |