/* -*- 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 "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 #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, 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 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 channel = do_QueryInterface(request); nsCOMPtr 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 titleURL; rv = NS_MutateURI(uri).SetQuery(""_ns).SetRef(""_ns).Finalize(titleURL); if (NS_FAILED(rv)) { titleURL = uri; } nsCString parentStr; nsCString buffer; buffer.AppendLiteral("\n\n\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 fileUrl = do_QueryInterface(uri); nsCOMPtr 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 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("\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( "\n" "\n" "\n"); buffer.AppendLiteral(R"( innerUri = NS_GetInnermostURI(uri); if (!innerUri) return NS_ERROR_UNEXPECTED; nsCOMPtr 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"); // 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("\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("\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("\n\n

"); AppendNonAsciiToNCR(title, buffer); buffer.AppendLiteral("

\n"); if (!parentStr.IsEmpty()) { nsAutoString parentText; rv = mBundle->GetStringFromName("DirGoUp", parentText); if (NS_FAILED(rv)) return rv; buffer.AppendLiteral(R"(

"); AppendNonAsciiToNCR(parentText, buffer); buffer.AppendLiteral("

\n"); } if (uri->SchemeIs("file")) { nsAutoString showHiddenText; rv = mBundle->GetStringFromName("ShowHidden", showHiddenText); if (NS_FAILED(rv)) return rv; buffer.AppendLiteral( "

\n"); } buffer.AppendLiteral( "\n" " \n" " \n" " \n" " \n" " \n" " \n" " \n"); buffer.AppendLiteral(" \n"); aBuffer = buffer; return rv; } NS_IMETHODIMP nsIndexedToHTML::OnStopRequest(nsIRequest* request, nsresult aStatus) { if (NS_SUCCEEDED(aStatus)) { nsCString buffer; buffer.AssignLiteral("
"); nsAutoString columnText; rv = mBundle->GetStringFromName("DirColName", columnText); if (NS_FAILED(rv)) return rv; AppendNonAsciiToNCR(columnText, buffer); buffer.AppendLiteral( ""); rv = mBundle->GetStringFromName("DirColSize", columnText); if (NS_FAILED(rv)) return rv; AppendNonAsciiToNCR(columnText, buffer); buffer.AppendLiteral( ""); rv = mBundle->GetStringFromName("DirColMTime", columnText); if (NS_FAILED(rv)) return rv; AppendNonAsciiToNCR(columnText, buffer); buffer.AppendLiteral( "
\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 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); } 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()) { 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("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 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"(">
"); if (type == nsIDirIndex::TYPE_FILE || type == nsIDirIndex::TYPE_UNKNOWN) { pushBuffer.AppendLiteral("\"");GetStringFromName("DirFileLabel", altText); if (NS_FAILED(rv)) return rv; AppendNonAsciiToNCR(altText, pushBuffer); pushBuffer.AppendLiteral("\">"); } pushBuffer.Append(escaped); pushBuffer.AppendLiteral("
\n '); } 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("\n GetLastModified(&t); if (t == -1LL) { pushBuffer.AppendLiteral(">\n "); } else { pushBuffer.AppendLiteral(" sortable-data=\""); pushBuffer.AppendInt(static_cast(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("\n "); // 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("\n"); return SendToListener(aRequest, pushBuffer); } NS_IMETHODIMP nsIndexedToHTML::OnInformationAvailable(nsIRequest* aRequest, const nsAString& aInfo) { nsAutoCString pushBuffer; nsAutoCString escapedUtf8; nsAppendEscapedHTML(NS_ConvertUTF16toUTF8(aInfo), escapedUtf8); pushBuffer.AppendLiteral("\n "); // escaped is provided in Unicode, so write hex NCRs as necessary // to prevent the HTML parser from applying a character set. AppendNonAsciiToNCR(NS_ConvertUTF8toUTF16(escapedUtf8), pushBuffer); pushBuffer.AppendLiteral( "\n \n \n \n\n"); 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"); } }