1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3  * License, v. 2.0. If a copy of the MPL was not distributed with this
4  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 
6 #include "nsIndexedToHTML.h"
7 
8 #include "DateTimeFormat.h"
9 #include "mozilla/Encoding.h"
10 #include "mozilla/intl/LocaleService.h"
11 #include "nsNetUtil.h"
12 #include "netCore.h"
13 #include "nsStringStream.h"
14 #include "nsIFile.h"
15 #include "nsIFileURL.h"
16 #include "nsEscape.h"
17 #include "nsIDirIndex.h"
18 #include "nsURLHelper.h"
19 #include "nsIStringBundle.h"
20 #include "nsDirIndexParser.h"
21 #include "nsNativeCharsetUtils.h"
22 #include "nsString.h"
23 #include "nsContentUtils.h"
24 #include <algorithm>
25 #include "nsIChannel.h"
26 #include "mozilla/Unused.h"
27 #include "nsIURIMutator.h"
28 
29 using mozilla::intl::LocaleService;
30 
NS_IMPL_ISUPPORTS(nsIndexedToHTML,nsIDirIndexListener,nsIStreamConverter,nsIRequestObserver,nsIStreamListener)31 NS_IMPL_ISUPPORTS(nsIndexedToHTML, nsIDirIndexListener, nsIStreamConverter,
32                   nsIRequestObserver, nsIStreamListener)
33 
34 static void AppendNonAsciiToNCR(const nsAString& in, nsCString& out) {
35   nsAString::const_iterator start, end;
36 
37   in.BeginReading(start);
38   in.EndReading(end);
39 
40   while (start != end) {
41     if (*start < 128) {
42       out.Append(*start++);
43     } else {
44       out.AppendLiteral("&#x");
45       out.AppendInt(*start++, 16);
46       out.Append(';');
47     }
48   }
49 }
50 
Create(nsISupports * aOuter,REFNSIID aIID,void ** aResult)51 nsresult nsIndexedToHTML::Create(nsISupports* aOuter, REFNSIID aIID,
52                                  void** aResult) {
53   nsresult rv;
54   if (aOuter) return NS_ERROR_NO_AGGREGATION;
55 
56   nsIndexedToHTML* _s = new nsIndexedToHTML();
57   if (_s == nullptr) return NS_ERROR_OUT_OF_MEMORY;
58 
59   rv = _s->QueryInterface(aIID, aResult);
60   return rv;
61 }
62 
Init(nsIStreamListener * aListener)63 nsresult nsIndexedToHTML::Init(nsIStreamListener* aListener) {
64   nsresult rv = NS_OK;
65 
66   mListener = aListener;
67 
68   nsCOMPtr<nsIStringBundleService> sbs =
69       do_GetService(NS_STRINGBUNDLE_CONTRACTID, &rv);
70   if (NS_FAILED(rv)) return rv;
71   rv = sbs->CreateBundle(NECKO_MSGS_URL, getter_AddRefs(mBundle));
72 
73   mExpectAbsLoc = false;
74 
75   return rv;
76 }
77 
78 NS_IMETHODIMP
Convert(nsIInputStream * aFromStream,const char * aFromType,const char * aToType,nsISupports * aCtxt,nsIInputStream ** res)79 nsIndexedToHTML::Convert(nsIInputStream* aFromStream, const char* aFromType,
80                          const char* aToType, nsISupports* aCtxt,
81                          nsIInputStream** res) {
82   return NS_ERROR_NOT_IMPLEMENTED;
83 }
84 
85 NS_IMETHODIMP
AsyncConvertData(const char * aFromType,const char * aToType,nsIStreamListener * aListener,nsISupports * aCtxt)86 nsIndexedToHTML::AsyncConvertData(const char* aFromType, const char* aToType,
87                                   nsIStreamListener* aListener,
88                                   nsISupports* aCtxt) {
89   return Init(aListener);
90 }
91 
92 NS_IMETHODIMP
GetConvertedType(const nsACString & aFromType,nsIChannel * aChannel,nsACString & aToType)93 nsIndexedToHTML::GetConvertedType(const nsACString& aFromType,
94                                   nsIChannel* aChannel, nsACString& aToType) {
95   return NS_ERROR_NOT_IMPLEMENTED;
96 }
97 
98 NS_IMETHODIMP
OnStartRequest(nsIRequest * request)99 nsIndexedToHTML::OnStartRequest(nsIRequest* request) {
100   nsCString buffer;
101   nsresult rv = DoOnStartRequest(request, nullptr, buffer);
102   if (NS_FAILED(rv)) {
103     request->Cancel(rv);
104   }
105 
106   rv = mListener->OnStartRequest(request);
107   if (NS_FAILED(rv)) return rv;
108 
109   // The request may have been canceled, and if that happens, we want to
110   // suppress calls to OnDataAvailable.
111   request->GetStatus(&rv);
112   if (NS_FAILED(rv)) return rv;
113 
114   // Push our buffer to the listener.
115 
116   rv = SendToListener(request, nullptr, buffer);
117   return rv;
118 }
119 
DoOnStartRequest(nsIRequest * request,nsISupports * aContext,nsCString & aBuffer)120 nsresult nsIndexedToHTML::DoOnStartRequest(nsIRequest* request,
121                                            nsISupports* aContext,
122                                            nsCString& aBuffer) {
123   nsresult rv;
124 
125   nsCOMPtr<nsIChannel> channel = do_QueryInterface(request);
126   nsCOMPtr<nsIURI> uri;
127   rv = channel->GetOriginalURI(getter_AddRefs(uri));
128   if (NS_FAILED(rv)) return rv;
129 
130   // We use the original URI for the title and parent link when it's a
131   // resource:// url, instead of the jar:file:// url it resolves to.
132   if (!uri->SchemeIs("resource")) {
133     rv = channel->GetURI(getter_AddRefs(uri));
134     if (NS_FAILED(rv)) return rv;
135   }
136 
137   channel->SetContentType("text/html"_ns);
138 
139   mParser = nsDirIndexParser::CreateInstance();
140   if (!mParser) return NS_ERROR_FAILURE;
141 
142   rv = mParser->SetListener(this);
143   if (NS_FAILED(rv)) return rv;
144 
145   rv = mParser->OnStartRequest(request);
146   if (NS_FAILED(rv)) return rv;
147 
148   nsAutoCString baseUri, titleUri;
149   rv = uri->GetAsciiSpec(baseUri);
150   if (NS_FAILED(rv)) return rv;
151 
152   nsCOMPtr<nsIURI> titleURL;
153   rv = NS_MutateURI(uri).SetQuery(""_ns).SetRef(""_ns).Finalize(titleURL);
154   if (NS_FAILED(rv)) {
155     titleURL = uri;
156   }
157 
158   nsCString parentStr;
159 
160   nsCString buffer;
161   buffer.AppendLiteral("<!DOCTYPE html>\n<html>\n<head>\n");
162 
163   // XXX - should be using the 300: line from the parser.
164   // We can't guarantee that that comes before any entry, so we'd have to
165   // buffer, and do other painful stuff.
166   // I'll deal with this when I make the changes to handle welcome messages
167   // The .. stuff should also come from the lower level protocols, but that
168   // would muck up the XUL display
169   // - bbaetz
170 
171   if (uri->SchemeIs("file")) {
172     nsCOMPtr<nsIFileURL> fileUrl = do_QueryInterface(uri);
173     nsCOMPtr<nsIFile> file;
174     rv = fileUrl->GetFile(getter_AddRefs(file));
175     if (NS_FAILED(rv)) return rv;
176 
177     nsAutoCString url;
178     rv = net_GetURLSpecFromFile(file, url);
179     if (NS_FAILED(rv)) return rv;
180     baseUri.Assign(url);
181 
182     nsCOMPtr<nsIFile> parent;
183     rv = file->GetParent(getter_AddRefs(parent));
184 
185     if (parent && NS_SUCCEEDED(rv)) {
186       net_GetURLSpecFromDir(parent, url);
187       if (NS_FAILED(rv)) return rv;
188       parentStr.Assign(url);
189     }
190 
191     // Directory index will be always encoded in UTF-8 if this is file url
192     buffer.AppendLiteral("<meta charset=\"UTF-8\">\n");
193 
194   } else if (uri->SchemeIs("jar")) {
195     nsAutoCString path;
196     rv = uri->GetPathQueryRef(path);
197     if (NS_FAILED(rv)) return rv;
198 
199     // a top-level jar directory URL is of the form jar:foo.zip!/
200     // path will be of the form foo.zip!/, and its last two characters
201     // will be "!/"
202     // XXX this won't work correctly when the name of the directory being
203     // XXX displayed ends with "!", but then again, jar: URIs don't deal
204     // XXX particularly well with such directories anyway
205     if (!StringEndsWith(path, "!/"_ns)) {
206       rv = uri->Resolve(".."_ns, parentStr);
207       if (NS_FAILED(rv)) return rv;
208     }
209   } else {
210     // default behavior for other protocols is to assume the channel's
211     // URL references a directory ending in '/' -- fixup if necessary.
212     nsAutoCString path;
213     rv = uri->GetPathQueryRef(path);
214     if (NS_FAILED(rv)) return rv;
215     if (baseUri.Last() != '/') {
216       baseUri.Append('/');
217       path.Append('/');
218       mozilla::Unused << NS_MutateURI(uri).SetPathQueryRef(path).Finalize(uri);
219     }
220     if (!path.EqualsLiteral("/")) {
221       rv = uri->Resolve(".."_ns, parentStr);
222       if (NS_FAILED(rv)) return rv;
223     }
224   }
225 
226   rv = titleURL->GetAsciiSpec(titleUri);
227   if (NS_FAILED(rv)) {
228     return rv;
229   }
230 
231   buffer.AppendLiteral(
232       "<style type=\"text/css\">\n"
233       ":root {\n"
234       "  font-family: sans-serif;\n"
235       "}\n"
236       "img {\n"
237       "  border: 0;\n"
238       "}\n"
239       "th {\n"
240       "  text-align: start;\n"
241       "  white-space: nowrap;\n"
242       "}\n"
243       "th > a {\n"
244       "  color: inherit;\n"
245       "}\n"
246       "table[order] > thead > tr > th {\n"
247       "  cursor: pointer;\n"
248       "}\n"
249       "table[order] > thead > tr > th::after {\n"
250       "  display: none;\n"
251       "  width: .8em;\n"
252       "  margin-inline-end: -.8em;\n"
253       "  text-align: end;\n"
254       "}\n"
255       "table[order=\"asc\"] > thead > tr > th::after {\n"
256       "  content: \"\\2193\"; /* DOWNWARDS ARROW (U+2193) */\n"
257       "}\n"
258       "table[order=\"desc\"] > thead > tr > th::after {\n"
259       "  content: \"\\2191\"; /* UPWARDS ARROW (U+2191) */\n"
260       "}\n"
261       "table[order][order-by=\"0\"] > thead > tr > th:first-child > a ,\n"
262       "table[order][order-by=\"1\"] > thead > tr > th:first-child + th > a ,\n"
263       "table[order][order-by=\"2\"] > thead > tr > th:first-child + th + th > "
264       "a {\n"
265       "  text-decoration: underline;\n"
266       "}\n"
267       "table[order][order-by=\"0\"] > thead > tr > th:first-child::after ,\n"
268       "table[order][order-by=\"1\"] > thead > tr > th:first-child + th::after "
269       ",\n"
270       "table[order][order-by=\"2\"] > thead > tr > th:first-child + th + "
271       "th::after {\n"
272       "  display: inline-block;\n"
273       "}\n"
274       "table.remove-hidden > tbody > tr.hidden-object {\n"
275       "  display: none;\n"
276       "}\n"
277       "td {\n"
278       "  white-space: nowrap;\n"
279       "}\n"
280       "table.ellipsis {\n"
281       "  width: 100%;\n"
282       "  table-layout: fixed;\n"
283       "  border-spacing: 0;\n"
284       "}\n"
285       "table.ellipsis > tbody > tr > td {\n"
286       "  overflow: hidden;\n"
287       "  text-overflow: ellipsis;\n"
288       "}\n"
289       "/* name */\n"
290       "/* name */\n"
291       "th:first-child {\n"
292       "  padding-inline-end: 2em;\n"
293       "}\n"
294       "/* size */\n"
295       "th:first-child + th {\n"
296       "  padding-inline-end: 1em;\n"
297       "}\n"
298       "td:first-child + td {\n"
299       "  text-align: end;\n"
300       "  padding-inline-end: 1em;\n"
301       "}\n"
302       "/* date */\n"
303       "td:first-child + td + td {\n"
304       "  padding-inline-start: 1em;\n"
305       "  padding-inline-end: .5em;\n"
306       "}\n"
307       "/* time */\n"
308       "td:first-child + td + td + td {\n"
309       "  padding-inline-start: .5em;\n"
310       "}\n"
311       ".symlink {\n"
312       "  font-style: italic;\n"
313       "}\n"
314       ".dir ,\n"
315       ".symlink ,\n"
316       ".file {\n"
317       "  margin-inline-start: 20px;\n"
318       "}\n"
319       ".dir::before ,\n"
320       ".file > img {\n"
321       "  margin-inline-end: 4px;\n"
322       "  margin-inline-start: -20px;\n"
323       "  max-width: 16px;\n"
324       "  max-height: 16px;\n"
325       "  vertical-align: middle;\n"
326       "}\n"
327       ".dir::before {\n"
328       "  content: url(resource://content-accessible/html/folder.png);\n"
329       "}\n"
330       "</style>\n"
331       "<link rel=\"stylesheet\" media=\"screen, projection\" type=\"text/css\""
332       " href=\"chrome://global/skin/dirListing/dirListing.css\">\n"
333       "<script type=\"application/javascript\">\n"
334       "'use strict';\n"
335       "var gTable, gOrderBy, gTBody, gRows, gUI_showHidden;\n"
336       "document.addEventListener(\"DOMContentLoaded\", function() {\n"
337       "  gTable = document.getElementsByTagName(\"table\")[0];\n"
338       "  gTBody = gTable.tBodies[0];\n"
339       "  if (gTBody.rows.length < 2)\n"
340       "    return;\n"
341       "  gUI_showHidden = document.getElementById(\"UI_showHidden\");\n"
342       "  var headCells = gTable.tHead.rows[0].cells,\n"
343       "      hiddenObjects = false;\n"
344       "  function rowAction(i) {\n"
345       "    return function(event) {\n"
346       "      event.preventDefault();\n"
347       "      orderBy(i);\n"
348       "    }\n"
349       "  }\n"
350       "  for (var i = headCells.length - 1; i >= 0; i--) {\n"
351       "    var anchor = document.createElement(\"a\");\n"
352       "    anchor.href = \"\";\n"
353       "    anchor.appendChild(headCells[i].firstChild);\n"
354       "    headCells[i].appendChild(anchor);\n"
355       "    headCells[i].addEventListener(\"click\", rowAction(i), true);\n"
356       "  }\n"
357       "  if (gUI_showHidden) {\n"
358       "    gRows = Array.from(gTBody.rows);\n"
359       "    hiddenObjects = gRows.some(row => row.className == "
360       "\"hidden-object\");\n"
361       "  }\n"
362       "  gTable.setAttribute(\"order\", \"\");\n"
363       "  if (hiddenObjects) {\n"
364       "    gUI_showHidden.style.display = \"block\";\n"
365       "    updateHidden();\n"
366       "  }\n"
367       "}, \"false\");\n"
368       "function compareRows(rowA, rowB) {\n"
369       "  var a = rowA.cells[gOrderBy].getAttribute(\"sortable-data\") || "
370       "\"\";\n"
371       "  var b = rowB.cells[gOrderBy].getAttribute(\"sortable-data\") || "
372       "\"\";\n"
373       "  var intA = +a;\n"
374       "  var intB = +b;\n"
375       "  if (a == intA && b == intB) {\n"
376       "    a = intA;\n"
377       "    b = intB;\n"
378       "  } else {\n"
379       "    a = a.toLowerCase();\n"
380       "    b = b.toLowerCase();\n"
381       "  }\n"
382       "  if (a < b)\n"
383       "    return -1;\n"
384       "  if (a > b)\n"
385       "    return 1;\n"
386       "  return 0;\n"
387       "}\n"
388       "function orderBy(column) {\n"
389       "  if (!gRows)\n"
390       "    gRows = Array.from(gTBody.rows);\n"
391       "  var order;\n"
392       "  if (gOrderBy == column) {\n"
393       "    order = gTable.getAttribute(\"order\") == \"asc\" ? \"desc\" : "
394       "\"asc\";\n"
395       "  } else {\n"
396       "    order = \"asc\";\n"
397       "    gOrderBy = column;\n"
398       "    gTable.setAttribute(\"order-by\", column);\n"
399       "    gRows.sort(compareRows);\n"
400       "  }\n"
401       "  gTable.removeChild(gTBody);\n"
402       "  gTable.setAttribute(\"order\", order);\n"
403       "  if (order == \"asc\")\n"
404       "    for (var i = 0; i < gRows.length; i++)\n"
405       "      gTBody.appendChild(gRows[i]);\n"
406       "  else\n"
407       "    for (var i = gRows.length - 1; i >= 0; i--)\n"
408       "      gTBody.appendChild(gRows[i]);\n"
409       "  gTable.appendChild(gTBody);\n"
410       "}\n"
411       "function updateHidden() {\n"
412       "  gTable.className = "
413       "gUI_showHidden.getElementsByTagName(\"input\")[0].checked ?\n"
414       "                     \"\" :\n"
415       "                     \"remove-hidden\";\n"
416       "}\n"
417       "</script>\n");
418 
419   buffer.AppendLiteral(R"(<link rel="icon" type="image/png" href=")");
420   nsCOMPtr<nsIURI> innerUri = NS_GetInnermostURI(uri);
421   if (!innerUri) return NS_ERROR_UNEXPECTED;
422   nsCOMPtr<nsIFileURL> fileURL(do_QueryInterface(innerUri));
423   // XXX bug 388553: can't use skinnable icons here due to security restrictions
424   if (fileURL) {
425     buffer.AppendLiteral(
426         "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB"
427         "AAAAAQCAYAAAAf8%2F9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9i"
428         "ZSBJbWFnZVJlYWR5ccllPAAAAjFJREFUeNqsU8uOElEQPffR"
429         "3XQ3ONASdBJCSBxHos5%2B3Bg3rvkCv8PElS78gPkO%2FATj"
430         "QoUdO2ftrJiRh6aneTb9sOpC4weMN6lcuFV16pxDIfI8x12O"
431         "YIDhcPiu2Wx%2B%2FHF5CW1Z6Jyegt%2FTNEWSJIjjGFEUIQ"
432         "xDrFYrWFSzXC4%2FdLvd95pRKpXKy%2BpRFZ7nwaWo1%2BsG"
433         "nQG2260BKJfLKJVKGI1GEEJw7ateryd0v993W63WEwjgxfn5"
434         "obGYzgCbzcaEbdsIggDj8Riu6z6iUk9SYZMSx8W0LMsM%2FS"
435         "KK75xnJlIq80anQXdbEp0OhcPJ0eiaJnGRMEyyPDsAKKUM9c"
436         "lkYoDo3SZJzzSdp0VSKYmfV1co%2Bz580kw5KDIM8RbRfEnU"
437         "f1HzxtQyMAGcaGruTKczMzEIaqhKifV6jd%2BzGQQB5llunF"
438         "%2FM52BizC2K5sYPYvZcu653tjOM9O93wnYc08gmkgg4VAxi"
439         "xfqFUJT36AYBZGd6PJkFCZnnlBxMp38gqIgLpZB0y4Nph18l"
440         "yWh5FFbrOSxbl3V4G%2BVB7T4ajYYxTyuLtO%2BCvWGgJE1M"
441         "c7JNsJEhvgw%2FQV4fo%2F24nbEsX2u1d5sVyn8sJO0ZAQiI"
442         "YnFh%2BxrfLz%2Fj29cBS%2FO14zg3i8XigW3ZkErDtmKoeM"
443         "%2BAJGRMnXeEPGKf0nCD1ydvkDzU9Jbc6OpR7WIw6L8lQ%2B"
444         "4pQ1%2FlPF0RGM9Ns91Wmptk0GfB4EJkt77vXYj%2F8m%2B8"
445         "y%2FkrwABHbz2H9V68DQAAAABJRU5ErkJggg%3D%3D");
446   } else {
447     buffer.AppendLiteral(
448         "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB"
449         "AAAAAQCAYAAAAf8%2F9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9i"
450         "ZSBJbWFnZVJlYWR5ccllPAAAAeBJREFUeNqcU81O20AQ%2Ft"
451         "Z2AgQSYQRqL1UPVG2hAUQkxLEStz4DrXpLpD5Drz31Cajax%"
452         "2Bghhx6qHIJURBTxIwQRwopCBbZjHMcOTrzermPipsSt1Iw0"
453         "3p3ZmW%2B%2B2R0TxhgOD34wjCHZlQ0iDYz9yvEfhxMTCYhE"
454         "QDIZhkxKd2sqzX2TOD2vBQCQhpPefng1ZP2dVPlLLdpL8SEM"
455         "cxng%2Fbs0RIHhtgs4twxOh%2BHjZxvzDx%2F3GQQiDFISiR"
456         "BLFMPKTRMollzcWECrDVhtxtdRVsL9youPxGj%2FbdfFlUZh"
457         "tDyYbYqWRUdai1oQRZ5oHeHl2gNM%2B01Uqio8RlH%2Bnsaz"
458         "JzNwXcq1B%2BiXPHprlEEymeBfXs1w8XxxihfyuXqoHqpoGj"
459         "ZM04bddgG%2F9%2B8WGj87qDdsrK9m%2BoA%2BpbhQTDh2l1"
460         "%2Bi2weNbSHMZyjvNXmVbqh9Fj5Oz27uEoP%2BSTxANruJs9"
461         "L%2FT6P0ewqPx5nmiAG5f6AoCtN1PbJzuRyJAyDBzzSQYvEr"
462         "f06yYxhGXlEa8H2KVGoasjwLx3Ewk858opQWXm%2B%2Fib9E"
463         "QrBzclLLLy89xYvlpchvtixcX6uo1y%2FzsiwHrkIsgKbp%2"
464         "BYWFOWicuqppoNTnStHzPFCPQhBEBOyGAX4JMADFetubi4BS"
465         "YAAAAABJRU5ErkJggg%3D%3D");
466   }
467   buffer.AppendLiteral("\">\n<title>");
468 
469   // Everything needs to end in a /,
470   // otherwise we end up linking to file:///foo/dirfile
471 
472   if (!mTextToSubURI) {
473     mTextToSubURI = do_GetService(NS_ITEXTTOSUBURI_CONTRACTID, &rv);
474     if (NS_FAILED(rv)) return rv;
475   }
476 
477   nsAutoString unEscapeSpec;
478   rv = mTextToSubURI->UnEscapeAndConvert("UTF-8"_ns, titleUri, unEscapeSpec);
479   if (NS_FAILED(rv)) {
480     return rv;
481   }
482 
483   nsCString htmlEscSpecUtf8;
484   nsAppendEscapedHTML(NS_ConvertUTF16toUTF8(unEscapeSpec), htmlEscSpecUtf8);
485   AutoTArray<nsString, 1> formatTitle;
486   CopyUTF8toUTF16(htmlEscSpecUtf8, *formatTitle.AppendElement());
487 
488   nsAutoString title;
489   rv = mBundle->FormatStringFromName("DirTitle", formatTitle, title);
490   if (NS_FAILED(rv)) return rv;
491 
492   // we want to convert string bundle to NCR
493   // to ensure they're shown in any charsets
494   AppendNonAsciiToNCR(title, buffer);
495 
496   buffer.AppendLiteral("</title>\n");
497 
498   // If there is a quote character in the baseUri, then
499   // lets not add a base URL.  The reason for this is that
500   // if we stick baseUri containing a quote into a quoted
501   // string, the quote character will prematurely close
502   // the base href string.  This is a fall-back check;
503   // that's why it is OK to not use a base rather than
504   // trying to play nice and escaping the quotes.  See bug
505   // 358128.
506 
507   if (!baseUri.Contains('"')) {
508     // Great, the baseUri does not contain a char that
509     // will prematurely close the string.  Go ahead an
510     // add a base href, but only do so if we're not
511     // dealing with a resource URI.
512     if (!uri->SchemeIs("resource")) {
513       buffer.AppendLiteral("<base href=\"");
514       nsAppendEscapedHTML(baseUri, buffer);
515       buffer.AppendLiteral("\" />\n");
516     }
517   } else {
518     NS_ERROR("broken protocol handler didn't escape double-quote.");
519   }
520 
521   nsCString direction("ltr"_ns);
522   if (LocaleService::GetInstance()->IsAppLocaleRTL()) {
523     direction.AssignLiteral("rtl");
524   }
525 
526   buffer.AppendLiteral("</head>\n<body dir=\"");
527   buffer.Append(direction);
528   buffer.AppendLiteral("\">\n<h1>");
529   AppendNonAsciiToNCR(title, buffer);
530   buffer.AppendLiteral("</h1>\n");
531 
532   if (!parentStr.IsEmpty()) {
533     nsAutoString parentText;
534     rv = mBundle->GetStringFromName("DirGoUp", parentText);
535     if (NS_FAILED(rv)) return rv;
536 
537     buffer.AppendLiteral(R"(<p id="UI_goUp"><a class="up" href=")");
538     nsAppendEscapedHTML(parentStr, buffer);
539     buffer.AppendLiteral("\">");
540     AppendNonAsciiToNCR(parentText, buffer);
541     buffer.AppendLiteral("</a></p>\n");
542   }
543 
544   if (uri->SchemeIs("file")) {
545     nsAutoString showHiddenText;
546     rv = mBundle->GetStringFromName("ShowHidden", showHiddenText);
547     if (NS_FAILED(rv)) return rv;
548 
549     buffer.AppendLiteral(
550         "<p id=\"UI_showHidden\" style=\"display:none\"><label><input "
551         "type=\"checkbox\" checked onchange=\"updateHidden()\">");
552     AppendNonAsciiToNCR(showHiddenText, buffer);
553     buffer.AppendLiteral("</label></p>\n");
554   }
555 
556   buffer.AppendLiteral(
557       "<table>\n"
558       " <thead>\n"
559       "  <tr>\n"
560       "   <th>");
561 
562   nsAutoString columnText;
563   rv = mBundle->GetStringFromName("DirColName", columnText);
564   if (NS_FAILED(rv)) return rv;
565   AppendNonAsciiToNCR(columnText, buffer);
566   buffer.AppendLiteral(
567       "</th>\n"
568       "   <th>");
569 
570   rv = mBundle->GetStringFromName("DirColSize", columnText);
571   if (NS_FAILED(rv)) return rv;
572   AppendNonAsciiToNCR(columnText, buffer);
573   buffer.AppendLiteral(
574       "</th>\n"
575       "   <th colspan=\"2\">");
576 
577   rv = mBundle->GetStringFromName("DirColMTime", columnText);
578   if (NS_FAILED(rv)) return rv;
579   AppendNonAsciiToNCR(columnText, buffer);
580   buffer.AppendLiteral(
581       "</th>\n"
582       "  </tr>\n"
583       " </thead>\n");
584   buffer.AppendLiteral(" <tbody>\n");
585 
586   aBuffer = buffer;
587   return rv;
588 }
589 
590 NS_IMETHODIMP
OnStopRequest(nsIRequest * request,nsresult aStatus)591 nsIndexedToHTML::OnStopRequest(nsIRequest* request, nsresult aStatus) {
592   if (NS_SUCCEEDED(aStatus)) {
593     nsCString buffer;
594     buffer.AssignLiteral("</tbody></table></body></html>\n");
595 
596     aStatus = SendToListener(request, nullptr, buffer);
597   }
598 
599   mParser->OnStopRequest(request, aStatus);
600   mParser = nullptr;
601 
602   return mListener->OnStopRequest(request, aStatus);
603 }
604 
SendToListener(nsIRequest * aRequest,nsISupports * aContext,const nsACString & aBuffer)605 nsresult nsIndexedToHTML::SendToListener(nsIRequest* aRequest,
606                                          nsISupports* aContext,
607                                          const nsACString& aBuffer) {
608   nsCOMPtr<nsIInputStream> inputData;
609   nsresult rv = NS_NewCStringInputStream(getter_AddRefs(inputData), aBuffer);
610   NS_ENSURE_SUCCESS(rv, rv);
611   return mListener->OnDataAvailable(aRequest, inputData, 0, aBuffer.Length());
612 }
613 
614 NS_IMETHODIMP
OnDataAvailable(nsIRequest * aRequest,nsIInputStream * aInput,uint64_t aOffset,uint32_t aCount)615 nsIndexedToHTML::OnDataAvailable(nsIRequest* aRequest, nsIInputStream* aInput,
616                                  uint64_t aOffset, uint32_t aCount) {
617   return mParser->OnDataAvailable(aRequest, aInput, aOffset, aCount);
618 }
619 
FormatTime(const nsDateFormatSelector aDateFormatSelector,const nsTimeFormatSelector aTimeFormatSelector,const PRTime aPrTime,nsAString & aStringOut)620 static nsresult FormatTime(const nsDateFormatSelector aDateFormatSelector,
621                            const nsTimeFormatSelector aTimeFormatSelector,
622                            const PRTime aPrTime, nsAString& aStringOut) {
623   // FormatPRExplodedTime will use GMT based formatted string (e.g. GMT+1)
624   // instead of local time zone name (e.g. CEST).
625   // To avoid this case when ResistFingerprinting is disabled, use
626   // |FormatPRTime| to show exact time zone name.
627   if (!nsContentUtils::ShouldResistFingerprinting()) {
628     return mozilla::DateTimeFormat::FormatPRTime(
629         aDateFormatSelector, aTimeFormatSelector, aPrTime, aStringOut);
630   }
631 
632   PRExplodedTime prExplodedTime;
633   PR_ExplodeTime(aPrTime, PR_GMTParameters, &prExplodedTime);
634   return mozilla::DateTimeFormat::FormatPRExplodedTime(
635       aDateFormatSelector, aTimeFormatSelector, &prExplodedTime, aStringOut);
636 }
637 
638 NS_IMETHODIMP
OnIndexAvailable(nsIRequest * aRequest,nsISupports * aCtxt,nsIDirIndex * aIndex)639 nsIndexedToHTML::OnIndexAvailable(nsIRequest* aRequest, nsISupports* aCtxt,
640                                   nsIDirIndex* aIndex) {
641   nsresult rv;
642   if (!aIndex) return NS_ERROR_NULL_POINTER;
643 
644   nsCString pushBuffer;
645   pushBuffer.AppendLiteral("<tr");
646 
647   // We don't know the file's character set yet, so retrieve the raw bytes
648   // which will be decoded by the HTML parser.
649   nsCString loc;
650   aIndex->GetLocation(loc);
651 
652   // Adjust the length in case unescaping shortened the string.
653   loc.Truncate(nsUnescapeCount(loc.BeginWriting()));
654 
655   if (loc.IsEmpty()) {
656     return NS_ERROR_ILLEGAL_VALUE;
657   }
658   if (loc.First() == char16_t('.')) {
659     pushBuffer.AppendLiteral(" class=\"hidden-object\"");
660   }
661 
662   pushBuffer.AppendLiteral(">\n <td sortable-data=\"");
663 
664   // The sort key is the name of the item, prepended by either 0, 1 or 2
665   // in order to group items.
666   uint32_t type;
667   aIndex->GetType(&type);
668   switch (type) {
669     case nsIDirIndex::TYPE_SYMLINK:
670       pushBuffer.Append('0');
671       break;
672     case nsIDirIndex::TYPE_DIRECTORY:
673       pushBuffer.Append('1');
674       break;
675     default:
676       pushBuffer.Append('2');
677       break;
678   }
679   nsCString escaped;
680   nsAppendEscapedHTML(loc, escaped);
681   pushBuffer.Append(escaped);
682 
683   pushBuffer.AppendLiteral(
684       R"("><table class="ellipsis"><tbody><tr><td><a class=")");
685   switch (type) {
686     case nsIDirIndex::TYPE_DIRECTORY:
687       pushBuffer.AppendLiteral("dir");
688       break;
689     case nsIDirIndex::TYPE_SYMLINK:
690       pushBuffer.AppendLiteral("symlink");
691       break;
692     default:
693       pushBuffer.AppendLiteral("file");
694       break;
695   }
696 
697   pushBuffer.AppendLiteral("\" href=\"");
698 
699   // need to escape links
700   nsAutoCString locEscaped;
701 
702   // Adding trailing slash helps to recognize whether the URL points to a file
703   // or a directory (bug #214405).
704   if ((type == nsIDirIndex::TYPE_DIRECTORY) && (loc.Last() != '/')) {
705     loc.Append('/');
706   }
707 
708   // now minimally re-escape the location...
709   uint32_t escFlags;
710   // for some protocols, we expect the location to be absolute.
711   // if so, and if the location indeed appears to be a valid URI, then go
712   // ahead and treat it like one.
713 
714   nsAutoCString scheme;
715   if (mExpectAbsLoc && NS_SUCCEEDED(net_ExtractURLScheme(loc, scheme))) {
716     // escape as absolute
717     escFlags = esc_Forced | esc_AlwaysCopy | esc_Minimal;
718   } else {
719     // escape as relative
720     // esc_Directory is needed because directories have a trailing slash.
721     // Without it, the trailing '/' will be escaped, and links from within
722     // that directory will be incorrect
723     escFlags = esc_Forced | esc_AlwaysCopy | esc_FileBaseName | esc_Colon |
724                esc_Directory;
725   }
726   NS_EscapeURL(loc.get(), loc.Length(), escFlags, locEscaped);
727   // esc_Directory does not escape the semicolons, so if a filename
728   // contains semicolons we need to manually escape them.
729   // This replacement should be removed in bug #473280
730   locEscaped.ReplaceSubstring(";", "%3b");
731   nsAppendEscapedHTML(locEscaped, pushBuffer);
732   pushBuffer.AppendLiteral("\">");
733 
734   if (type == nsIDirIndex::TYPE_FILE || type == nsIDirIndex::TYPE_UNKNOWN) {
735     pushBuffer.AppendLiteral("<img src=\"moz-icon://");
736     int32_t lastDot = locEscaped.RFindChar('.');
737     if (lastDot != kNotFound) {
738       locEscaped.Cut(0, lastDot);
739       nsAppendEscapedHTML(locEscaped, pushBuffer);
740     } else {
741       pushBuffer.AppendLiteral("unknown");
742     }
743     pushBuffer.AppendLiteral("?size=16\" alt=\"");
744 
745     nsAutoString altText;
746     rv = mBundle->GetStringFromName("DirFileLabel", altText);
747     if (NS_FAILED(rv)) return rv;
748     AppendNonAsciiToNCR(altText, pushBuffer);
749     pushBuffer.AppendLiteral("\">");
750   }
751 
752   pushBuffer.Append(escaped);
753   pushBuffer.AppendLiteral("</a></td></tr></tbody></table></td>\n <td");
754 
755   if (type == nsIDirIndex::TYPE_DIRECTORY ||
756       type == nsIDirIndex::TYPE_SYMLINK) {
757     pushBuffer.Append('>');
758   } else {
759     int64_t size;
760     aIndex->GetSize(&size);
761 
762     if (uint64_t(size) != UINT64_MAX) {
763       pushBuffer.AppendLiteral(" sortable-data=\"");
764       pushBuffer.AppendInt(size);
765       pushBuffer.AppendLiteral("\">");
766       nsAutoCString sizeString;
767       FormatSizeString(size, sizeString);
768       pushBuffer.Append(sizeString);
769     } else {
770       pushBuffer.Append('>');
771     }
772   }
773   pushBuffer.AppendLiteral("</td>\n <td");
774 
775   PRTime t;
776   aIndex->GetLastModified(&t);
777 
778   if (t == -1LL) {
779     pushBuffer.AppendLiteral("></td>\n <td>");
780   } else {
781     pushBuffer.AppendLiteral(" sortable-data=\"");
782     pushBuffer.AppendInt(static_cast<int64_t>(t));
783     pushBuffer.AppendLiteral("\">");
784     nsAutoString formatted;
785     FormatTime(kDateFormatShort, kTimeFormatNone, t, formatted);
786     AppendNonAsciiToNCR(formatted, pushBuffer);
787     pushBuffer.AppendLiteral("</td>\n <td>");
788     FormatTime(kDateFormatNone, kTimeFormatLong, t, formatted);
789     // use NCR to show date in any doc charset
790     AppendNonAsciiToNCR(formatted, pushBuffer);
791   }
792 
793   pushBuffer.AppendLiteral("</td>\n</tr>");
794 
795   return SendToListener(aRequest, aCtxt, pushBuffer);
796 }
797 
798 NS_IMETHODIMP
OnInformationAvailable(nsIRequest * aRequest,nsISupports * aCtxt,const nsAString & aInfo)799 nsIndexedToHTML::OnInformationAvailable(nsIRequest* aRequest,
800                                         nsISupports* aCtxt,
801                                         const nsAString& aInfo) {
802   nsAutoCString pushBuffer;
803   nsAutoCString escapedUtf8;
804   nsAppendEscapedHTML(NS_ConvertUTF16toUTF8(aInfo), escapedUtf8);
805   pushBuffer.AppendLiteral("<tr>\n <td>");
806   // escaped is provided in Unicode, so write hex NCRs as necessary
807   // to prevent the HTML parser from applying a character set.
808   AppendNonAsciiToNCR(NS_ConvertUTF8toUTF16(escapedUtf8), pushBuffer);
809   pushBuffer.AppendLiteral(
810       "</td>\n <td></td>\n <td></td>\n <td></td>\n</tr>\n");
811 
812   return SendToListener(aRequest, aCtxt, pushBuffer);
813 }
814 
FormatSizeString(int64_t inSize,nsCString & outSizeString)815 void nsIndexedToHTML::FormatSizeString(int64_t inSize,
816                                        nsCString& outSizeString) {
817   outSizeString.Truncate();
818   if (inSize > int64_t(0)) {
819     // round up to the nearest Kilobyte
820     int64_t upperSize = (inSize + int64_t(1023)) / int64_t(1024);
821     outSizeString.AppendInt(upperSize);
822     outSizeString.AppendLiteral(" KB");
823   }
824 }
825